You've already forked winget-pkgs
mirror of
https://github.com/TermoraDev/winget-pkgs.git
synced 2026-04-22 15:31:19 -07:00
3244 lines
159 KiB
PowerShell
3244 lines
159 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
WinGet Manifest creation helper script
|
|
.DESCRIPTION
|
|
This file intends to help you generate a manifest for publishing
|
|
to the Windows Package Manager repository.
|
|
|
|
It'll attempt to download an installer from the user-provided URL to calculate
|
|
a checksum. That checksum and the rest of the input data will be compiled into
|
|
a set of .YAML files.
|
|
.EXAMPLE
|
|
PS C:\Projects\winget-pkgs> Get-Help .\Tools\YamlCreate.ps1 -Full
|
|
Show this script's help
|
|
.EXAMPLE
|
|
PS C:\Projects\winget-pkgs> .\Tools\YamlCreate.ps1
|
|
Run the script to create a manifest file
|
|
.NOTES
|
|
Please file an issue if you run into errors with this script:
|
|
https://github.com/microsoft/winget-pkgs/issues
|
|
.LINK
|
|
https://github.com/microsoft/winget-pkgs/blob/master/Tools/YamlCreate.ps1
|
|
#>
|
|
#Requires -Version 5
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'This script is not intended to have any outputs piped')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Preserve', Justification = 'The variable is used in a conditional but ScriptAnalyser does not recognize the scope')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Scope = 'Function', Target = 'Read-AppsAndFeaturesEntries',
|
|
Justification = 'Ths function is a wrapper which calls the singular Read-AppsAndFeaturesEntry as many times as necessary. It corresponds exactly to a pluralized manifest field')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Scope = 'Function', Target = '*Metadata',
|
|
Justification = 'Metadata is used as a mass noun and is therefore singular in the cases used in this script')]
|
|
|
|
Param
|
|
(
|
|
[switch] $Settings,
|
|
[switch] $AutoUpgrade,
|
|
[switch] $help,
|
|
[switch] $SkipPRCheck,
|
|
[switch] $Preserve,
|
|
[Parameter(Mandatory = $false)]
|
|
[string] $PackageIdentifier,
|
|
[Parameter(Mandatory = $false)]
|
|
[string] $PackageVersion,
|
|
[Parameter(Mandatory = $false)]
|
|
[string] $Mode
|
|
)
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
|
|
if ($help) {
|
|
Write-Host -ForegroundColor 'Green' 'For full documentation of the script, see https://github.com/microsoft/winget-pkgs/tree/master/doc/tools/YamlCreate.md'
|
|
Write-Host -ForegroundColor 'Yellow' 'Usage: ' -NoNewline
|
|
Write-Host -ForegroundColor 'White' '.\YamlCreate.ps1 [-PackageIdentifier <identifier>] [-PackageVersion <version>] [-Mode <1-5>] [-Settings] [-SkipPRCheck]'
|
|
Write-Host
|
|
exit
|
|
}
|
|
|
|
# Custom menu prompt that listens for key presses. Requires a prompt and array of entries at minimum. Entries preceeded with `*` are shown in green
|
|
# Returns a console key value
|
|
Function Invoke-KeypressMenu {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $Prompt,
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[string[]] $Entries,
|
|
[Parameter(Mandatory = $false)]
|
|
[string] $HelpText,
|
|
[Parameter(Mandatory = $false)]
|
|
[string] $HelpTextColor,
|
|
[Parameter(Mandatory = $false)]
|
|
[string] $DefaultString,
|
|
[Parameter(Mandatory = $false)]
|
|
[string[]] $AllowedCharacters
|
|
)
|
|
|
|
if (!$PSBoundParameters.ContainsKey('AllowedCharacters')) {
|
|
$AllowedCharacters = @($Entries.TrimStart('*').Chars(1))
|
|
}
|
|
|
|
Write-Host "`n"
|
|
Write-Host -ForegroundColor 'Yellow' "$Prompt"
|
|
if ($PSBoundParameters.ContainsKey('HelpText') -and (![string]::IsNullOrWhiteSpace($HelpText))) {
|
|
if ($PSBoundParameters.ContainsKey('HelpTextColor') -and (![string]::IsNullOrWhiteSpace($HelpTextColor))) {
|
|
Write-Host -ForegroundColor $HelpTextColor $HelpText
|
|
} else {
|
|
Write-Host -ForegroundColor 'Blue' $HelpText
|
|
}
|
|
}
|
|
foreach ($entry in $Entries) {
|
|
$_isDefault = $entry.StartsWith('*')
|
|
if ($_isDefault) {
|
|
$_entry = ' ' + $entry.Substring(1)
|
|
$_color = 'Green'
|
|
} else {
|
|
$_entry = ' ' + $entry
|
|
$_color = 'White'
|
|
}
|
|
Write-Host -ForegroundColor $_color $_entry
|
|
}
|
|
Write-Host
|
|
if ($PSBoundParameters.ContainsKey('DefaultString') -and (![string]::IsNullOrWhiteSpace($DefaultString))) {
|
|
Write-Host -NoNewline "Enter Choice (default is '$DefaultString'): "
|
|
} else {
|
|
Write-Host -NoNewline 'Enter Choice ('
|
|
Write-Host -NoNewline -ForegroundColor 'Green' 'Green'
|
|
Write-Host -NoNewline ' is default): '
|
|
}
|
|
|
|
do {
|
|
$keyInfo = [Console]::ReadKey($false)
|
|
if ($keyInfo.KeyChar -notin $AllowedCharacters -and $ScriptSettings.ExplicitMenuOptions -eq $true -and $AllowedCharacters.Length -gt 0) {
|
|
if ($keyInfo.Key -eq 'Enter') { Write-Host }
|
|
$keyInfo = $null
|
|
}
|
|
} until ($keyInfo.Key)
|
|
|
|
return $keyInfo.Key
|
|
}
|
|
|
|
#If the user has git installed, make sure it is a patched version
|
|
if (Get-Command 'git' -ErrorAction SilentlyContinue) {
|
|
$GitMinimumVersion = [System.Version]::Parse('2.39.1')
|
|
$gitVersionString = ((git version) | Select-String '([0-9]{1,}\.?){3,}').Matches.Value.Trim(' ', '.')
|
|
$gitVersion = [System.Version]::Parse($gitVersionString)
|
|
if ($gitVersion -lt $GitMinimumVersion) {
|
|
# Prompt user to install git
|
|
if (Get-Command 'winget' -ErrorAction SilentlyContinue) {
|
|
$_menu = @{
|
|
entries = @('[Y] Upgrade Git'; '*[N] Do not upgrade')
|
|
Prompt = 'The version of git installed on your machine does not satisfy the requirement of version >= 2.39.1; Would you like to upgrade?'
|
|
HelpText = "Upgrading will attempt to upgrade git using winget`n"
|
|
DefaultString = ''
|
|
}
|
|
switch (Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText']) {
|
|
'Y' {
|
|
Write-Host
|
|
try {
|
|
winget upgrade --id Git.Git --exact
|
|
} catch {
|
|
throw [UnmetDependencyException]::new('Git could not be upgraded sucessfully', $_)
|
|
} finally {
|
|
$gitVersionString = ((git version) | Select-String '([0-9]{1,}\.?){3,}').Matches.Value.Trim(' ', '.')
|
|
$gitVersion = [System.Version]::Parse($gitVersionString)
|
|
if ($gitVersion -lt $GitMinimumVersion) {
|
|
throw [UnmetDependencyException]::new('Git could not be upgraded sucessfully')
|
|
}
|
|
}
|
|
}
|
|
default { Write-Host; throw [UnmetDependencyException]::new('The version of git installed on your machine does not satisfy the requirement of version >= 2.39.1') }
|
|
}
|
|
} else {
|
|
throw [UnmetDependencyException]::new('The version of git installed on your machine does not satisfy the requirement of version >= 2.39.1')
|
|
}
|
|
}
|
|
# Check whether the script is present inside a fork/clone of microsoft/winget-pkgs repository
|
|
try {
|
|
$script:gitTopLevel = (Resolve-Path $(git rev-parse --show-toplevel)).Path
|
|
} catch {
|
|
# If there was an exception, the user isn't in a git repo. Throw a custom exception and pass the original exception as an InternalException
|
|
throw [UnmetDependencyException]::new('This script must be run from inside a clone of the winget-pkgs repository', $_.Exception)
|
|
}
|
|
}
|
|
|
|
####
|
|
# Description: Ensures a PowerShell module is installed
|
|
# Inputs: PowerShell Module Name
|
|
# Outputs: None
|
|
####
|
|
function Initialize-Module {
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[String] $Name,
|
|
[Parameter(Mandatory = $false)]
|
|
[String[]] $Cmdlet,
|
|
[Parameter(Mandatory = $false)]
|
|
[String[]] $Function
|
|
)
|
|
|
|
$NuGetVersion = (Get-PackageProvider).Where({ $_.Name -ceq 'NuGet' }).Version
|
|
$installedModules = Get-Module -ListAvailable -Name $Name
|
|
|
|
# Ensure NuGet is installed and up to date
|
|
# If the NuGet Package Provider is not installed, the version will be null, which will satisfy the conditional
|
|
if ($NuGetVersion -lt $script:NuGetMinimumVersion) {
|
|
try {
|
|
Write-Debug 'NuGet Package Provider was not found, it will be installed'
|
|
# This might fail if the user is not an administrator, so catch the errors
|
|
Install-PackageProvider -Name NuGet -MinimumVersion $script:NuGetMinimumVersion.ToString() -Force -Scope CurrentUser
|
|
} catch {
|
|
Write-Error 'Could not install the NuGet package provider which is required to install script dependencies.' -ErrorAction Continue
|
|
Write-Error "You may be able to resolve this by running: Install-PackageProvider -Name NuGet -MinimumVersion $($script:NuGetMinimumVersion.ToString())"
|
|
}
|
|
}
|
|
|
|
Write-Verbose "Ensuring PowerShell module '$Name' is installed"
|
|
if ($installedModules) {
|
|
# If the module is installed, attempt to upgrade it
|
|
Write-Debug "Found $Name in installed modules"
|
|
} else {
|
|
# If the module is not installed, attempt to install it
|
|
try {
|
|
Install-Module -Name $Name -Force -Repository PSGallery -Scope CurrentUser
|
|
} catch {
|
|
Write-Error "$Name was unable to be installed successfully"
|
|
}
|
|
}
|
|
# Verify the module is installed and present
|
|
try {
|
|
if (!(Get-Module -Name $Name)) {
|
|
$importParameters = @{Name = $Name; Scope = 'Local' } # Force the module to be imported into the local scope to avoid changing the global scope
|
|
if ($PSBoundParameters.ContainsKey('Cmdlet')) { $importParameters['Cmdlet'] = $Cmdlet }
|
|
if ($PSBoundParameters.ContainsKey('Function')) { $importParameters['Function'] = $Function }
|
|
|
|
Import-Module @importParameters
|
|
}
|
|
} catch {
|
|
Write-Error "$Name was found in available modules, but could not be imported"
|
|
}
|
|
}
|
|
|
|
$script:NuGetMinimumVersion = [System.Version]::Parse('2.8.5.201')
|
|
Initialize-Module -Name 'powershell-yaml' # Used for parsing YAML files
|
|
Initialize-Module -Name 'MSI' -Cmdlet @('Get-MSITable'; 'Get-MSIProperty') # Used for fetching MSI Properties
|
|
Initialize-Module -Name 'NtObjectManager' -Function @('Get-Win32ModuleResource'; 'Get-Win32ModuleManifest') # Used for checking installer type inno
|
|
|
|
# Set settings directory on basis of Operating System
|
|
$script:SettingsPath = Join-Path $(if ([System.Environment]::OSVersion.Platform -match 'Win') { $env:LOCALAPPDATA } else { $env:HOME + '/.config' } ) -ChildPath 'YamlCreate'
|
|
# Check for settings directory and create it if none exists
|
|
if (!(Test-Path $script:SettingsPath)) { New-Item -ItemType 'Directory' -Force -Path $script:SettingsPath | Out-Null }
|
|
# Check for settings file and create it if none exists
|
|
$script:SettingsPath = $(Join-Path $script:SettingsPath -ChildPath 'Settings.yaml')
|
|
if (!(Test-Path $script:SettingsPath)) { '# See https://github.com/microsoft/winget-pkgs/tree/master/doc/tools/YamlCreate.md for a list of available settings' > $script:SettingsPath }
|
|
# Load settings from file
|
|
$ScriptSettings = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $script:SettingsPath -Encoding UTF8) -join "`n")
|
|
|
|
if ($Settings) {
|
|
Invoke-Item -Path $script:SettingsPath
|
|
exit
|
|
}
|
|
|
|
$ScriptHeader = '# Created with YamlCreate.ps1 v2.5.0'
|
|
$ManifestVersion = '1.10.0'
|
|
$PSDefaultParameterValues = @{ '*:Encoding' = 'UTF8' }
|
|
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
|
|
$ofs = ', '
|
|
$callingUICulture = [Threading.Thread]::CurrentThread.CurrentUICulture
|
|
$callingCulture = [Threading.Thread]::CurrentThread.CurrentCulture
|
|
[Threading.Thread]::CurrentThread.CurrentUICulture = 'en-US'
|
|
[Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'
|
|
if (-not ([System.Environment]::OSVersion.Platform -match 'Win')) { $env:TEMP = '/tmp/' }
|
|
$wingetUpstream = 'https://github.com/microsoft/winget-pkgs.git'
|
|
$RunHash = $(Get-FileHash -InputStream $([IO.MemoryStream]::new([byte[]][char[]]$(Get-Date).Ticks.ToString()))).Hash.Substring(0, 8)
|
|
$script:UserAgent = 'Microsoft-Delivery-Optimization/10.1'
|
|
$script:CleanupPaths = @()
|
|
|
|
$script:OriginalPSModulePath = $env:PSModulePath
|
|
Write-Debug 'Setting up module paths for YamlCreate'
|
|
Write-Debug "Adding $(Join-Path -Path $PSScriptRoot -ChildPath 'Modules') to PSModulePath"
|
|
$env:PSModulePath = $env:PSModulePath + ';' + (Join-Path -Path $PSScriptRoot -ChildPath 'Modules') # Add the local modules to the PSModulePath
|
|
|
|
Import-Module -Name 'YamlCreate' -Scope Global -Force -ErrorAction 'Stop' # Parent module that loads the rest of the modules required for the script
|
|
|
|
$_wingetVersion = 1.0.0
|
|
$_appInstallerVersion = (Get-AppxPackage Microsoft.DesktopAppInstaller).version
|
|
if (Get-Command 'winget' -ErrorAction SilentlyContinue) { $_wingetVersion = (winget -v).TrimStart('v') }
|
|
$script:backupUserAgent = "winget-cli WindowsPackageManager/$_wingetVersion DesktopAppInstaller/Microsoft.DesktopAppInstaller v$_appInstallerVersion"
|
|
|
|
if ($ScriptSettings.EnableDeveloperOptions -eq $true -and $null -ne $ScriptSettings.OverrideManifestVersion) {
|
|
$script:UsesPrerelease = $ScriptSettings.OverrideManifestVersion -gt $ManifestVersion
|
|
$ManifestVersion = $ScriptSettings.OverrideManifestVersion
|
|
}
|
|
|
|
$useDirectSchemaLink = if ($env:GITHUB_ACTIONS -eq $true) {
|
|
$true
|
|
} else {
|
|
(Invoke-WebRequest "https://aka.ms/winget-manifest.version.$ManifestVersion.schema.json" -UseBasicParsing).Content -match '<!doctype html>'
|
|
}
|
|
$SchemaUrls = @{
|
|
version = if ($useDirectSchemaLink) { "https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v$ManifestVersion/manifest.version.$ManifestVersion.json" } else { "https://aka.ms/winget-manifest.version.$ManifestVersion.schema.json" }
|
|
defaultLocale = if ($useDirectSchemaLink) { "https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v$ManifestVersion/manifest.defaultLocale.$ManifestVersion.json" } else { "https://aka.ms/winget-manifest.defaultLocale.$ManifestVersion.schema.json" }
|
|
locale = if ($useDirectSchemaLink) { "https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v$ManifestVersion/manifest.locale.$ManifestVersion.json" } else { "https://aka.ms/winget-manifest.locale.$ManifestVersion.schema.json" }
|
|
installer = if ($useDirectSchemaLink) { "https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v$ManifestVersion/manifest.installer.$ManifestVersion.json" } else { "https://aka.ms/winget-manifest.installer.$ManifestVersion.schema.json" }
|
|
}
|
|
|
|
# Fetch Schema data from github for entry validation, key ordering, and automatic commenting
|
|
try {
|
|
$LocaleSchema = @(Invoke-WebRequest $SchemaUrls.defaultLocale -UseBasicParsing | ConvertFrom-Json)
|
|
$LocaleProperties = (ConvertTo-Yaml $LocaleSchema.properties | ConvertFrom-Yaml -Ordered).Keys
|
|
$VersionSchema = @(Invoke-WebRequest $SchemaUrls.version -UseBasicParsing | ConvertFrom-Json)
|
|
$VersionProperties = (ConvertTo-Yaml $VersionSchema.properties | ConvertFrom-Yaml -Ordered).Keys
|
|
$InstallerSchema = @(Invoke-WebRequest $SchemaUrls.installer -UseBasicParsing | ConvertFrom-Json)
|
|
$InstallerProperties = (ConvertTo-Yaml $InstallerSchema.properties | ConvertFrom-Yaml -Ordered).Keys
|
|
$InstallerSwitchProperties = (ConvertTo-Yaml $InstallerSchema.definitions.InstallerSwitches.properties | ConvertFrom-Yaml -Ordered).Keys
|
|
$InstallerEntryProperties = (ConvertTo-Yaml $InstallerSchema.definitions.Installer.properties | ConvertFrom-Yaml -Ordered).Keys
|
|
$InstallerDependencyProperties = (ConvertTo-Yaml $InstallerSchema.definitions.Dependencies.properties | ConvertFrom-Yaml -Ordered).Keys
|
|
$AppsAndFeaturesEntryProperties = (ConvertTo-Yaml $InstallerSchema.definitions.AppsAndFeaturesEntry.properties | ConvertFrom-Yaml -Ordered).Keys
|
|
} catch {
|
|
# Here we want to pass the exception as an inner exception for debugging if necessary
|
|
throw [System.Net.WebException]::new('Manifest schemas could not be downloaded. Try running the script again', $_.Exception)
|
|
}
|
|
|
|
filter TrimString {
|
|
$_.Trim()
|
|
}
|
|
|
|
filter RightTrimString {
|
|
$_.TrimEnd()
|
|
}
|
|
|
|
filter UniqueItems {
|
|
[string]$($_.Split(',').Trim() | Select-Object -Unique)
|
|
}
|
|
|
|
filter ToLower {
|
|
[string]$_.ToLower()
|
|
}
|
|
|
|
filter NoWhitespace {
|
|
[string]$_ -replace '\s{1,}', '-'
|
|
}
|
|
|
|
$ToNatural = { [regex]::Replace($_, '\d+', { $args[0].Value.PadLeft(20) }) }
|
|
|
|
# Various patterns used in validation to simplify the validation logic
|
|
$Patterns = @{
|
|
PackageIdentifier = $VersionSchema.properties.PackageIdentifier.pattern
|
|
IdentifierMaxLength = $VersionSchema.properties.PackageIdentifier.maxLength
|
|
PackageVersion = $InstallerSchema.definitions.PackageVersion.pattern
|
|
VersionMaxLength = $VersionSchema.properties.PackageVersion.maxLength
|
|
InstallerSha256 = $InstallerSchema.definitions.Installer.properties.InstallerSha256.pattern
|
|
InstallerUrl = $InstallerSchema.definitions.Installer.properties.InstallerUrl.pattern
|
|
InstallerUrlMaxLength = $InstallerSchema.definitions.Installer.properties.InstallerUrl.maxLength
|
|
ValidArchitectures = $InstallerSchema.definitions.Architecture.enum
|
|
ValidInstallerTypes = $InstallerSchema.definitions.InstallerType.enum
|
|
ValidNestedInstallerTypes = $InstallerSchema.definitions.NestedInstallerType.enum
|
|
SilentSwitchMaxLength = $InstallerSchema.definitions.InstallerSwitches.properties.Silent.maxLength
|
|
ProgressSwitchMaxLength = $InstallerSchema.definitions.InstallerSwitches.properties.SilentWithProgress.maxLength
|
|
CustomSwitchMaxLength = $InstallerSchema.definitions.InstallerSwitches.properties.Custom.maxLength
|
|
SignatureSha256 = $InstallerSchema.definitions.Installer.properties.SignatureSha256.pattern
|
|
FamilyName = $InstallerSchema.definitions.PackageFamilyName.pattern
|
|
FamilyNameMaxLength = $InstallerSchema.definitions.PackageFamilyName.maxLength
|
|
PackageLocale = $LocaleSchema.properties.PackageLocale.pattern
|
|
InstallerLocaleMaxLength = $InstallerSchema.definitions.Locale.maxLength
|
|
ProductCodeMinLength = $InstallerSchema.definitions.ProductCode.minLength
|
|
ProductCodeMaxLength = $InstallerSchema.definitions.ProductCode.maxLength
|
|
MaxItemsFileExtensions = $InstallerSchema.definitions.FileExtensions.maxItems
|
|
MaxItemsProtocols = $InstallerSchema.definitions.Protocols.maxItems
|
|
MaxItemsCommands = $InstallerSchema.definitions.Commands.maxItems
|
|
MaxItemsSuccessCodes = $InstallerSchema.definitions.InstallerSuccessCodes.maxItems
|
|
MaxItemsInstallModes = $InstallerSchema.definitions.InstallModes.maxItems
|
|
PackageLocaleMaxLength = $LocaleSchema.properties.PackageLocale.maxLength
|
|
PublisherMaxLength = $LocaleSchema.properties.Publisher.maxLength
|
|
PackageNameMaxLength = $LocaleSchema.properties.PackageName.maxLength
|
|
MonikerMaxLength = $LocaleSchema.definitions.Tag.maxLength
|
|
GenericUrl = $LocaleSchema.definitions.Url.pattern
|
|
GenericUrlMaxLength = $LocaleSchema.definitions.Url.maxLength
|
|
AuthorMinLength = $LocaleSchema.properties.Author.minLength
|
|
AuthorMaxLength = $LocaleSchema.properties.Author.maxLength
|
|
LicenseMaxLength = $LocaleSchema.properties.License.maxLength
|
|
CopyrightMinLength = $LocaleSchema.properties.Copyright.minLength
|
|
CopyrightMaxLength = $LocaleSchema.properties.Copyright.maxLength
|
|
TagsMaxItems = $LocaleSchema.properties.Tags.maxItems
|
|
ShortDescriptionMaxLength = $LocaleSchema.properties.ShortDescription.maxLength
|
|
DescriptionMinLength = $LocaleSchema.properties.Description.minLength
|
|
DescriptionMaxLength = $LocaleSchema.properties.Description.maxLength
|
|
ValidInstallModes = $InstallerSchema.definitions.InstallModes.items.enum
|
|
FileExtension = $InstallerSchema.definitions.FileExtensions.items.pattern
|
|
FileExtensionMaxLength = $InstallerSchema.definitions.FileExtensions.items.maxLength
|
|
ReleaseNotesMinLength = $LocaleSchema.properties.ReleaseNotes.MinLength
|
|
ReleaseNotesMaxLength = $LocaleSchema.properties.ReleaseNotes.MaxLength
|
|
RelativeFilePathMinLength = $InstallerSchema.Definitions.NestedInstallerFiles.items.properties.RelativeFilePath.minLength
|
|
RelativeFilePathMaxLength = $InstallerSchema.Definitions.NestedInstallerFiles.items.properties.RelativeFilePath.maxLength
|
|
PortableCommandAliasMinLength = $InstallerSchema.Definitions.NestedInstallerFiles.items.properties.PortableCommandAlias.minLength
|
|
PortableCommandAliasMaxLength = $InstallerSchema.Definitions.NestedInstallerFiles.items.properties.PortableCommandAlias.maxLength
|
|
ArchiveInstallerTypes = @('zip')
|
|
ARP_DisplayNameMinLength = $InstallerSchema.Definitions.AppsAndFeaturesEntry.properties.DisplayName.minLength
|
|
ARP_DisplayNameMaxLength = $InstallerSchema.Definitions.AppsAndFeaturesEntry.properties.DisplayName.maxLength
|
|
ARP_PublisherMinLength = $InstallerSchema.Definitions.AppsAndFeaturesEntry.properties.Publisher.minLength
|
|
ARP_PublisherMaxLength = $InstallerSchema.Definitions.AppsAndFeaturesEntry.properties.Publisher.maxLength
|
|
ARP_DisplayVersionMinLength = $InstallerSchema.Definitions.AppsAndFeaturesEntry.properties.DisplayVersion.minLength
|
|
ARP_DisplayVersionMaxLength = $InstallerSchema.Definitions.AppsAndFeaturesEntry.properties.DisplayVersion.maxLength
|
|
}
|
|
|
|
# check if upstream exists
|
|
($remoteUpstreamUrl = $(git remote get-url upstream)) *> $null
|
|
if ($remoteUpstreamUrl -and $remoteUpstreamUrl -ne $wingetUpstream) {
|
|
git remote set-url upstream $wingetUpstream
|
|
} elseif (!$remoteUpstreamUrl) {
|
|
Write-Host -ForegroundColor 'Yellow' 'Upstream does not exist. Permanently adding https://github.com/microsoft/winget-pkgs as remote upstream'
|
|
git remote add upstream $wingetUpstream
|
|
}
|
|
|
|
####
|
|
# Description: Removes files and folders from the file system
|
|
# Inputs: List of paths to remove
|
|
# Outputs: None
|
|
####
|
|
function Invoke-FileCleanup {
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyString()]
|
|
[AllowEmptyCollection()]
|
|
[String[]] $FilePaths
|
|
)
|
|
if (!$FilePaths) { return }
|
|
foreach ($path in $FilePaths) {
|
|
Write-Debug "Removing $path"
|
|
if (Test-Path $path) { Remove-Item -Path $path -Recurse }
|
|
else { Write-Warning "Could not remove $path as it does not exist" }
|
|
}
|
|
}
|
|
|
|
# Since this script changes the UI Calling Culture, a clean exit should set it back to the user preference
|
|
# If the remote upstream was changed, that should also be set back
|
|
Function Invoke-CleanExit {
|
|
|
|
if ($remoteUpstreamUrl -and $remoteUpstreamUrl -ne $wingetUpstream) {
|
|
git remote set-url upstream $remoteUpstreamUrl
|
|
}
|
|
|
|
Invoke-FileCleanup -FilePaths $script:CleanupPaths
|
|
|
|
Write-Host
|
|
[Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
|
|
[Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
|
|
$env:PSModulePath = $script:OriginalPSModulePath
|
|
exit
|
|
}
|
|
|
|
# This function validates whether a string matches Minimum Length, Maximum Length, and Regex pattern
|
|
# The switches can be used to specify if null values are allowed regardless of validation
|
|
Function Test-String {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[AllowEmptyString()]
|
|
[string] $InputString,
|
|
[Parameter(Mandatory = $false)]
|
|
[regex] $MatchPattern,
|
|
[Parameter(Mandatory = $false)]
|
|
[int] $MinLength,
|
|
[Parameter(Mandatory = $false)]
|
|
[int] $MaxLength,
|
|
[switch] $AllowNull,
|
|
[switch] $NotNull,
|
|
[switch] $IsNull,
|
|
[switch] $Not
|
|
)
|
|
|
|
$_isValid = $true
|
|
|
|
if ($PSBoundParameters.ContainsKey('MinLength')) {
|
|
$_isValid = $_isValid -and ($InputString.Length -ge $MinLength)
|
|
}
|
|
if ($PSBoundParameters.ContainsKey('MaxLength')) {
|
|
$_isValid = $_isValid -and ($InputString.Length -le $MaxLength)
|
|
}
|
|
if ($PSBoundParameters.ContainsKey('MatchPattern')) {
|
|
$_isValid = $_isValid -and ($InputString -match $MatchPattern)
|
|
}
|
|
if ($AllowNull -and [string]::IsNullOrWhiteSpace($InputString)) {
|
|
$_isValid = $true
|
|
} elseif ($NotNull -and [string]::IsNullOrWhiteSpace($InputString)) {
|
|
$_isValid = $false
|
|
}
|
|
if ($IsNull) {
|
|
$_isValid = [string]::IsNullOrWhiteSpace($InputString)
|
|
}
|
|
|
|
if ($Not) {
|
|
return !$_isValid
|
|
} else {
|
|
return $_isValid
|
|
}
|
|
}
|
|
|
|
# Gets the effective installer type from an installer
|
|
Function Get-EffectiveInstallerType {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[PSCustomObject] $Installer
|
|
)
|
|
if ($Installer.Keys -notcontains 'InstallerType') {
|
|
throw [System.ArgumentException]::new('Invalid Function Parameters. Installer must contain `InstallerType` key')
|
|
}
|
|
if ($Installer.InstallerType -notin $Patterns.ArchiveInstallerTypes) {
|
|
return $Installer.InstallerType
|
|
}
|
|
if ($Installer.Keys -notcontains 'NestedInstallerType') {
|
|
throw [System.ArgumentException]::new("Invalid Function Parameters. Installer type $($Installer.InstallerType) must contain `NestedInstallerType` key")
|
|
}
|
|
return $Installer.NestedInstallerType
|
|
}
|
|
|
|
# Checks a URL and returns the status code received from the URL
|
|
Function Test-Url {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $URL
|
|
)
|
|
try {
|
|
$HTTP_Request = [System.Net.WebRequest]::Create($URL)
|
|
$HTTP_Request.UserAgent = $script:UserAgent
|
|
$HTTP_Response = $HTTP_Request.GetResponse()
|
|
$script:ResponseUri = $HTTP_Response.ResponseUri.AbsoluteUri
|
|
$HTTP_Status = [int]$HTTP_Response.StatusCode
|
|
} catch {
|
|
# Failed to download with the Delivery-Optimization User Agent, so try again with the WinINet User Agent
|
|
try {
|
|
$HTTP_Request = [System.Net.WebRequest]::Create($URL)
|
|
$HTTP_Request.UserAgent = $script:backupUserAgent
|
|
$HTTP_Response = $HTTP_Request.GetResponse()
|
|
$script:ResponseUri = $HTTP_Response.ResponseUri.AbsoluteUri
|
|
$HTTP_Status = [int]$HTTP_Response.StatusCode
|
|
} catch {
|
|
$HTTP_Status = 404
|
|
}
|
|
}
|
|
If ($null -eq $HTTP_Response) { $HTTP_Status = 404 }
|
|
Else { $HTTP_Response.Close() }
|
|
|
|
return $HTTP_Status
|
|
}
|
|
|
|
# Checks a file name for validity and returns a boolean value
|
|
Function Test-ValidFileName {
|
|
param([string]$FileName)
|
|
$IndexOfInvalidChar = $FileName.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars())
|
|
# IndexOfAny() returns the value -1 to indicate no such character was found
|
|
return $IndexOfInvalidChar -eq -1
|
|
}
|
|
|
|
# Prompts user to enter an Installer URL, Tests the URL to ensure it results in a response code of 200, validates it against the manifest schema
|
|
# Returns the validated URL which was entered
|
|
Function Request-InstallerUrl {
|
|
do {
|
|
Write-Host -ForegroundColor $(if ($script:_returnValue.Severity -gt 1) { 'red' } else { 'yellow' }) $script:_returnValue.ErrorString()
|
|
if ($script:_returnValue.StatusCode -ne 409) {
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the download url to the installer.'
|
|
$NewInstallerUrl = Read-Host -Prompt 'Url' | TrimString
|
|
}
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
if ((Test-Url $NewInstallerUrl) -ne 200) {
|
|
$script:_returnValue = [ReturnValue]::new(502, 'Invalid URL Response', 'The URL did not return a successful response from the server', 2)
|
|
} else {
|
|
if (($script:ResponseUri -ne $NewInstallerUrl) -and ($ScriptSettings.UseRedirectedURL -ne 'never') -and ($NewInstallerUrl -notmatch 'github')) {
|
|
#If urls don't match, ask to update; If they do update, set custom error and check for validity;
|
|
$_menu = @{
|
|
entries = @('*[Y] Use detected URL'; '[N] Use original URL')
|
|
Prompt = 'The URL provided appears to be redirected. Would you like to use the destination URL instead?'
|
|
HelpText = "Discovered URL: $($script:ResponseUri)"
|
|
DefaultString = 'Y'
|
|
}
|
|
switch ($(if ($ScriptSettings.UseRedirectedURL -eq 'always') { 'Y' } else { Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] })) {
|
|
'N' { Write-Host -ForegroundColor 'Green' "`nOriginal URL Retained - Proceeding with $NewInstallerUrl`n" } #Continue without replacing URL
|
|
default {
|
|
$NewInstallerUrl = $script:ResponseUri
|
|
$script:_returnValue = [ReturnValue]::new(409, 'URL Changed', 'The URL was changed during processing and will be re-validated', 1)
|
|
Write-Host
|
|
}
|
|
}
|
|
}
|
|
$NewInstallerUrl = [System.Web.HttpUtility]::UrlDecode($NewInstallerUrl.Replace('+', '%2B'))
|
|
$NewInstallerUrl = $NewInstallerUrl.Replace(' ', '%20')
|
|
if ($script:_returnValue.StatusCode -ne 409) {
|
|
if (Test-String $NewInstallerUrl -MaxLength $Patterns.InstallerUrlMaxLength -MatchPattern $Patterns.InstallerUrl -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $NewInstallerUrl -MaxLength $Patterns.InstallerUrlMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.InstallerUrlMaxLength)
|
|
} elseif (Test-String -not $NewInstallerUrl -MatchPattern $Patterns.InstallerUrl) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
return $NewInstallerUrl
|
|
}
|
|
|
|
Function Get-InstallerFile {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $URI,
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[string] $PackageIdentifier,
|
|
[Parameter(Mandatory = $true, Position = 2)]
|
|
[string] $PackageVersion
|
|
|
|
)
|
|
# Create a filename based on the Package Identifier and Version; Try to get the extension from the URL
|
|
# If the extension isn't found, use a custom one
|
|
$_URIPath = $URI.Split('?')[0]
|
|
$_Filename = "$PackageIdentifier v$PackageVersion - $(Get-Date -f 'yyyy.MM.dd-hh.mm.ss')" + $(if ([System.IO.Path]::HasExtension($_URIPath)) { [System.IO.Path]::GetExtension($_URIPath) } else { '.winget-tmp' })
|
|
if (Test-ValidFileName $_Filename) { $_OutFile = Join-Path $env:TEMP -ChildPath $_Filename }
|
|
else { $_OutFile = (New-TemporaryFile).FullName }
|
|
|
|
# Create a new web client for downloading the file
|
|
$_WebClient = [System.Net.WebClient]::new()
|
|
$_WebClient.Headers.Add('User-Agent', $script:UserAgent)
|
|
# If the system has a default proxy set, use it
|
|
# Powershell Core will automatically use this, so it's only necessary for PS5
|
|
if ($PSVersionTable.PSVersion.Major -lt 6) { $_WebClient.Proxy = [System.Net.WebProxy]::GetDefaultProxy() }
|
|
# Download the file
|
|
try {
|
|
$_WebClient.DownloadFile($URI, $_OutFile)
|
|
} catch {
|
|
# Failed to download with the Delivery-Optimization User Agent, so try again with the WinINet User Agent
|
|
$_WebClient.Headers.Clear()
|
|
$_WebClient.Headers.Add('User-Agent', $script:backupUserAgent)
|
|
$_WebClient.DownloadFile($URI, $_OutFile)
|
|
} finally {
|
|
# Dispose of the web client to release the resources it uses
|
|
$_WebClient.Dispose()
|
|
}
|
|
|
|
return $_OutFile
|
|
}
|
|
|
|
Function Get-UserSavePreference {
|
|
switch ($ScriptSettings.SaveToTemporaryFolder) {
|
|
'always' { $_Preference = '0' }
|
|
'never' { $_Preference = '1' }
|
|
'manual' { $_Preference = '2' }
|
|
default {
|
|
$_menu = @{
|
|
entries = @('[Y] Yes'; '*[N] No'; '[M] Manually Enter SHA256')
|
|
Prompt = 'Do you want to save the files to the Temp folder?'
|
|
DefaultString = 'N'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'Y' { $_Preference = '0' }
|
|
'N' { $_Preference = '1' }
|
|
'M' { $_Preference = '2' }
|
|
default { $_Preference = '1' }
|
|
}
|
|
}
|
|
}
|
|
return $_Preference
|
|
}
|
|
|
|
Function Get-UriArchitecture {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $URI
|
|
)
|
|
|
|
if ($URI -match '\b(x|win){0,1}64\b') { return 'x64' }
|
|
if ($URI -match '\b((win|ia)32)|(x{0,1}86)\b') { return 'x86' }
|
|
if ($URI -match '\b(arm|aarch)64\b') { return 'arm64' }
|
|
if ($URI -match '\barm\b') { return 'arm' }
|
|
return $null
|
|
}
|
|
|
|
Function Get-UriScope {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $URI
|
|
)
|
|
|
|
if ($URI -match '\buser\b') { return 'user' }
|
|
if ($URI -match '\bmachine\b') { return 'machine' }
|
|
return $null
|
|
}
|
|
|
|
function Get-PublisherHash($publisherName) {
|
|
# Sourced from https://marcinotorowski.com/2021/12/19/calculating-hash-part-of-msix-package-family-name
|
|
$publisherNameAsUnicode = [System.Text.Encoding]::Unicode.GetBytes($publisherName);
|
|
$publisherSha256 = [System.Security.Cryptography.HashAlgorithm]::Create('SHA256').ComputeHash($publisherNameAsUnicode);
|
|
$publisherSha256First8Bytes = $publisherSha256 | Select-Object -First 8;
|
|
$publisherSha256AsBinary = $publisherSha256First8Bytes | ForEach-Object { [System.Convert]::ToString($_, 2).PadLeft(8, '0') };
|
|
$asBinaryStringWithPadding = [System.String]::Concat($publisherSha256AsBinary).PadRight(65, '0');
|
|
|
|
$encodingTable = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
|
|
$result = '';
|
|
for ($i = 0; $i -lt $asBinaryStringWithPadding.Length; $i += 5) {
|
|
$asIndex = [System.Convert]::ToInt32($asBinaryStringWithPadding.Substring($i, 5), 2);
|
|
$result += $encodingTable[$asIndex];
|
|
}
|
|
|
|
return $result.ToLower();
|
|
}
|
|
|
|
Function Get-PackageFamilyName {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $FilePath
|
|
)
|
|
if ($FilePath -notmatch '\.(msix|appx)(bundle){0,1}$') { return $null }
|
|
|
|
# Make the downloaded installer a zip file
|
|
$_MSIX = Get-Item $FilePath
|
|
$_Zip = Join-Path $_MSIX.Directory.FullName -ChildPath 'MSIX_YamlCreate.zip'
|
|
$_ZipFolder = [System.IO.Path]::GetDirectoryName($_ZIp) + '\' + [System.IO.Path]::GetFileNameWithoutExtension($_Zip)
|
|
Copy-Item -Path $_MSIX.FullName -Destination $_Zip
|
|
# Progress preference has to be set globally for Expand-Archive
|
|
# https://github.com/PowerShell/Microsoft.PowerShell.Archive/issues/77#issuecomment-601947496
|
|
$globalPreference = $global:ProgressPreference
|
|
$global:ProgressPreference = 'SilentlyContinue'
|
|
# Expand the zip file to access the manifest inside
|
|
Expand-Archive $_Zip -DestinationPath $_ZipFolder -Force
|
|
# Restore the old progress preference
|
|
$global:ProgressPreference = $globalPreference
|
|
# Package could be a single package or a bundle, so regex search for either of them
|
|
$_AppxManifest = Get-ChildItem $_ZipFolder -Recurse -File -Filter '*.xml' | Where-Object { $_.Name -match '^Appx(Bundle)?Manifest.xml$' } | Select-Object -First 1
|
|
[XML] $_XMLContent = Get-Content $_AppxManifest.FullName -Raw
|
|
# The path to the node is different between single package and bundles, this should work to get either
|
|
$_Identity = @($_XMLContent.Bundle.Identity) + @($_XMLContent.Package.Identity)
|
|
# Cleanup the files that were created
|
|
Remove-Item $_Zip -Force
|
|
Remove-Item $_ZipFolder -Recurse -Force
|
|
# Return the PFN
|
|
return $_Identity.Name + '_' + $(Get-PublisherHash $_Identity.Publisher)
|
|
}
|
|
|
|
# Prompts the user to enter the Package Identifier if it has not been set
|
|
# Validates that the package identifier matches the schema
|
|
# Returns the package identifier
|
|
Function Read-PackageIdentifier {
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[AllowEmptyString()]
|
|
[string] $PackageIdentifier
|
|
)
|
|
$_EnteredIdentifier = $PackageIdentifier
|
|
do {
|
|
if ((Test-String $_EnteredIdentifier -IsNull) -or ($script:_returnValue.StatusCode -ne [ReturnValue]::Success().StatusCode)) {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the Package Identifier, in the following format <Publisher shortname.Application shortname>. For example: Microsoft.Excel'
|
|
$_EnteredIdentifier = Read-Host -Prompt 'PackageIdentifier' | TrimString
|
|
}
|
|
|
|
$script:PackageIdentifierFolder = $_EnteredIdentifier.Replace('.', '\')
|
|
if (Test-String $_EnteredIdentifier -MinLength 4 -MaxLength $Patterns.IdentifierMaxLength -MatchPattern $Patterns.PackageIdentifier) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $_EnteredIdentifier -MinLength 4 -MaxLength $Patterns.IdentifierMaxLength) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(4, $Patterns.IdentifierMaxLength)
|
|
} elseif (Test-String -not $_EnteredIdentifier -MatchPattern $Patterns.PackageIdentifier) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
return $_EnteredIdentifier
|
|
}
|
|
|
|
# Prompts the user to enter the details for an archive Installer
|
|
# Takes the installer as an input
|
|
# Returns the modified installer
|
|
Function Read-NestedInstaller {
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[PSCustomObject] $_Installer
|
|
)
|
|
|
|
if ($_Installer['InstallerType'] -CIn @($Patterns.ArchiveInstallerTypes)) {
|
|
# Manual Entry of Nested Installer Type with validation
|
|
if ($_Installer['NestedInstallerType'] -CNotIn @($Patterns.ValidInstallerTypes)) {
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the NestedInstallerType. Options:' , @($Patterns.ValidNestedInstallerTypes -join ', ' )
|
|
$_Installer['NestedInstallerType'] = Read-Host -Prompt 'NestedInstallerType' | TrimString
|
|
if ($_Installer['NestedInstallerType'] -Cin @($Patterns.ValidNestedInstallerTypes)) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Invalid Installer Type', "Value must exist in the enum - $(@($Patterns.ValidNestedInstallerTypes -join ', '))", 2)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
$_EffectiveType = Get-EffectiveInstallerType $_Installer
|
|
|
|
$_NestedInstallerFiles = @()
|
|
do {
|
|
$_InstallerFile = [ordered] @{}
|
|
$AnotherNestedInstaller = $false
|
|
$_RelativePath = $null
|
|
$_Alias = $null
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the relative path to the installer file'
|
|
if (Test-String -not $_RelativePath -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $_RelativePath" }
|
|
$_RelativePath = Read-Host -Prompt 'RelativeFilePath' | TrimString
|
|
if (Test-String -not $_RelativePath -IsNull) { $_InstallerFile['RelativeFilePath'] = $_RelativePath }
|
|
|
|
if (Test-String $_RelativePath -MinLength $Patterns.RelativeFilePathMinLength -MaxLength $Patterns.RelativeFilePathMaxLength) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.RelativeFilePathMinLength, $Patterns.RelativeFilePathMaxLength)
|
|
}
|
|
if ($_RelativePath -in @($_NestedInstallerFiles.RelativeFilePath)) {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Path Collision', 'Relative file path must be unique', 2)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
if ($_EffectiveType -eq 'portable') {
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the portable command alias'
|
|
if (Test-String -not "$($_InstallerFile['PortableCommandAlias'])" -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $($_InstallerFile['PortableCommandAlias'])" }
|
|
$_Alias = Read-Host -Prompt 'PortableCommandAlias' | TrimString
|
|
if (Test-String -not $_Alias -IsNull) { $_InstallerFile['PortableCommandAlias'] = $_Alias }
|
|
|
|
if (Test-String $_InstallerFile['PortableCommandAlias'] -MinLength $Patterns.PortableCommandAliasMinLength -MaxLength $Patterns.PortableCommandAliasMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.PortableCommandAliasMinLength, $Patterns.PortableCommandAliasMaxLength)
|
|
}
|
|
if ("$($_InstallerFile['PortableCommandAlias'])" -in @($_NestedInstallerFiles.PortableCommandAlias)) {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Alias Collision', 'Aliases must be unique', 2)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Prompt to see if multiple entries are needed
|
|
$_menu = @{
|
|
entries = @(
|
|
'[Y] Yes'
|
|
'*[N] No'
|
|
)
|
|
Prompt = 'Do you want to create another portable installer entry?'
|
|
DefaultString = 'N'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'Y' { $AnotherNestedInstaller = $true }
|
|
default { $AnotherNestedInstaller = $false }
|
|
}
|
|
|
|
if (!$AnotherNestedInstaller -and $script:Option -eq 'New') {
|
|
# Prompt to see if the package depends on binaries being in the path
|
|
$_menu = @{
|
|
entries = @(
|
|
'[Y] Yes'
|
|
'*[N] No'
|
|
)
|
|
Prompt = 'Does this executable depend on DLLs or other files that are not available through Symlink?'
|
|
DefaultString = 'N'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'Y' { $_Installer['ArchiveBinariesDependOnPath'] = $true }
|
|
|
|
# Not required to explicitly set as CLI defaults to false
|
|
default { }
|
|
}
|
|
}
|
|
}
|
|
$_NestedInstallerFiles += $_InstallerFile
|
|
} until (!$AnotherNestedInstaller)
|
|
$_Installer['NestedInstallerFiles'] = $_NestedInstallerFiles
|
|
}
|
|
return $_Installer
|
|
}
|
|
|
|
Function Read-AppsAndFeaturesEntries {
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[PSCustomObject] $_Installer
|
|
)
|
|
|
|
$_AppsAndFeaturesEntries = @()
|
|
# TODO: Support adding AppsAndFeaturesEntries if they don't exist
|
|
if (!$_Installer.AppsAndFeaturesEntries) {
|
|
return
|
|
}
|
|
|
|
# TODO: Support Multiple AppsAndFeaturesEntries once WinGet supports it
|
|
# For now, only select and retain the first entry
|
|
foreach ($_AppsAndFeaturesEntry in @($_Installer.AppsAndFeaturesEntries[0])) {
|
|
$_AppsAndFeaturesEntries += Read-AppsAndFeaturesEntry $_AppsAndFeaturesEntry
|
|
}
|
|
return $_AppsAndFeaturesEntries
|
|
}
|
|
|
|
Function Read-AppsAndFeaturesEntry {
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[PSCustomObject] $_AppsAndFeaturesEntry
|
|
)
|
|
|
|
# TODO: Support adding new fields instead of only editing existing ones
|
|
if ($_AppsAndFeaturesEntry.DisplayName) { $_AppsAndFeaturesEntry['DisplayName'] = Read-ARPDisplayName $_AppsAndFeaturesEntry.DisplayName }
|
|
if ($_AppsAndFeaturesEntry.DisplayVersion) { $_AppsAndFeaturesEntry['DisplayVersion'] = Read-ARPDisplayVersion $_AppsAndFeaturesEntry.DisplayVersion }
|
|
if ($_AppsAndFeaturesEntry.Publisher) { $_AppsAndFeaturesEntry['Publisher'] = Read-ARPPublisher $_AppsAndFeaturesEntry.Publisher }
|
|
# TODO: Support ProductCode, UpgradeCode, and InstallerType
|
|
return Restore-YamlKeyOrder $_AppsAndFeaturesEntry $AppsAndFeaturesEntryProperties -NoComments
|
|
}
|
|
|
|
Function Read-ARPDisplayName {
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $_DisplayName
|
|
)
|
|
# Request DisplayName and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Recommended] Enter the application name as it appears in control panel'
|
|
if (Test-String -not $_DisplayName -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $_DisplayName" }
|
|
$NewValue = Read-Host -Prompt 'DisplayName' | TrimString
|
|
if (Test-String -not $NewValue -IsNull) { $_DisplayName = $NewValue }
|
|
|
|
if (Test-String $_DisplayName -MinLength $Patterns.ARP_DisplayNameMinLength -MaxLength $Patterns.ARP_DisplayNameMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.ARP_DisplayNameMinLength, $Patterns.ARP_DisplayNameMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
return $_DisplayName
|
|
}
|
|
|
|
Function Read-ARPPublisher {
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $_Publisher
|
|
)
|
|
# Request Publisher Name and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Recommended] Enter the Publisher name as it appears in control panel'
|
|
if (Test-String -not $_Publisher -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $_Publisher" }
|
|
$NewValue = Read-Host -Prompt 'Publisher' | TrimString
|
|
if (Test-String -not $NewValue -IsNull) { $_Publisher = $NewValue }
|
|
|
|
if (Test-String $_Publisher -MinLength $Patterns.ARP_PublisherMinLength -MaxLength $Patterns.ARP_PublisherMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.ARP_PublisherMinLength, $Patterns.ARP_PublisherMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
return $_Publisher
|
|
}
|
|
|
|
Function Read-ARPDisplayVersion {
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $_DisplayVersion
|
|
)
|
|
# Request DisplayVersion and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Recommended] Enter the application version as it appears in control panel'
|
|
if (Test-String -not $_DisplayVersion -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $_DisplayVersion" }
|
|
$NewValue = Read-Host -Prompt 'DisplayVersion' | TrimString
|
|
if (Test-String -not $NewValue -IsNull) { $_DisplayVersion = $NewValue }
|
|
|
|
if (Test-String $_DisplayVersion -MinLength $Patterns.ARP_DisplayVersionMinLength -MaxLength $Patterns.ARP_DisplayVersionMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.ARP_DisplayVersionMinLength, $Patterns.ARP_DisplayVersionMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
return $_DisplayVersion
|
|
}
|
|
|
|
# Prompts the user to enter installer values
|
|
# Sets the $script:Installers value as an output
|
|
# Returns void
|
|
Function Read-InstallerEntry {
|
|
$_Installer = [ordered] @{}
|
|
# Request user enter Installer URL
|
|
$_Installer['InstallerUrl'] = Request-InstallerUrl
|
|
|
|
if ($_Installer.InstallerUrl -in ($script:Installers).InstallerUrl) {
|
|
$_MatchingInstaller = $script:Installers | Where-Object { $_.InstallerUrl -eq $_Installer.InstallerUrl } | Select-Object -First 1
|
|
if ($_MatchingInstaller.InstallerSha256) { $_Installer['InstallerSha256'] = $_MatchingInstaller.InstallerSha256 }
|
|
if ($_MatchingInstaller.InstallerType) { $_Installer['InstallerType'] = $_MatchingInstaller.InstallerType }
|
|
if ($_MatchingInstaller.ProductCode) { $_Installer['ProductCode'] = $_MatchingInstaller.ProductCode }
|
|
if ($_MatchingInstaller.PackageFamilyName) { $_Installer['PackageFamilyName'] = $_MatchingInstaller.PackageFamilyName }
|
|
if ($_MatchingInstaller.SignatureSha256) { $_Installer['SignatureSha256'] = $_MatchingInstaller.SignatureSha256 }
|
|
}
|
|
|
|
# Get or request Installer Sha256
|
|
# Check the settings to see if we need to display this menu
|
|
if ($_Installer.Keys -notcontains 'InstallerSha256') {
|
|
|
|
$script:SaveOption = Get-UserSavePreference
|
|
# If user did not select manual entry for Sha256, download file and calculate hash
|
|
# Also attempt to detect installer type and architecture
|
|
if ($script:SaveOption -ne '2') {
|
|
Write-Host
|
|
$start_time = Get-Date
|
|
Write-Host $NewLine
|
|
Write-Host 'Downloading URL. This will take a while...' -ForegroundColor Blue
|
|
try {
|
|
$script:dest = Get-InstallerFile -URI $_Installer['InstallerUrl'] -PackageIdentifier $PackageIdentifier -PackageVersion $PackageVersion
|
|
} catch {
|
|
# Here we also want to pass any exceptions through for potential debugging
|
|
throw [System.Net.WebException]::new('The file could not be downloaded. Try running the script again', $_.Exception)
|
|
}
|
|
Write-Host "Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)" -ForegroundColor Green
|
|
$_Installer['InstallerSha256'] = (Get-FileHash -Path $script:dest -Algorithm SHA256).Hash
|
|
Resolve-InstallerType -Path $script:dest -OutVariable _ | Out-Null
|
|
if ($_) { $_Installer['InstallerType'] = $_ | Select-Object -First 1 }
|
|
Get-UriArchitecture -URI $_Installer['InstallerUrl'] -OutVariable _ | Out-Null
|
|
if ($_) { $_Installer['Architecture'] = $_ | Select-Object -First 1 }
|
|
Get-UriScope -URI $_Installer['InstallerUrl'] -OutVariable _ | Out-Null
|
|
if ($_) { $_Installer['Scope'] = $_ | Select-Object -First 1 }
|
|
if ([System.Environment]::OSVersion.Platform -match 'Win' -and ($script:dest).EndsWith('.msi')) {
|
|
$ProductCode = ([string](Get-MSIProperty -Path $script:dest -Property 'ProductCode') | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
|
|
} elseif ([System.Environment]::OSVersion.Platform -match 'Unix' -and (Get-Item $script:dest).Name.EndsWith('.msi')) {
|
|
$ProductCode = ([string](file $script:dest) | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
|
|
}
|
|
if (Test-String -Not "$ProductCode" -IsNull) { $_Installer['ProductCode'] = "$ProductCode" }
|
|
}
|
|
# Manual Entry of Sha256 with validation
|
|
else {
|
|
Write-Host
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the installer SHA256 Hash'
|
|
$_Installer['InstallerSha256'] = Read-Host -Prompt 'InstallerSha256' | TrimString
|
|
$_Installer['InstallerSha256'] = $_Installer['InstallerSha256'].toUpper()
|
|
if ($_Installer['InstallerSha256'] -match $Patterns.InstallerSha256) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
}
|
|
|
|
# Manual Entry of Architecture with validation
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
if (Test-String $_Installer['Architecture'] -IsNull) { Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the architecture. Options:' , @($Patterns.ValidArchitectures -join ', ') }
|
|
else {
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the architecture. Options:' , @($Patterns.ValidArchitectures -join ', ')
|
|
Write-Host -ForegroundColor 'DarkGray' -Object "Old Variable: $($_Installer['Architecture'])"
|
|
}
|
|
Read-Host -Prompt 'Architecture' -OutVariable _ | Out-Null
|
|
if (Test-String $_ -Not -IsNull) { $_Installer['Architecture'] = $_ | TrimString }
|
|
|
|
if ($_Installer['Architecture'] -Cin @($Patterns.ValidArchitectures)) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Invalid Architecture', "Value must exist in the enum - $(@($Patterns.ValidArchitectures -join ', '))", 2)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Manual Entry of Installer Type with validation
|
|
if ($_Installer['InstallerType'] -CNotIn @($Patterns.ValidInstallerTypes)) {
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the InstallerType. Options:' , @($Patterns.ValidInstallerTypes -join ', ' )
|
|
$_Installer['InstallerType'] = Read-Host -Prompt 'InstallerType' | TrimString
|
|
if ($_Installer['InstallerType'] -Cin @($Patterns.ValidInstallerTypes)) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Invalid Installer Type', "Value must exist in the enum - $(@($Patterns.ValidInstallerTypes -join ', '))", 2)
|
|
}
|
|
if ($_Installer['InstallerType'] -eq 'zip' -and [version]$ManifestVersion -lt [version]'1.4.0') {
|
|
$script:_returnValue = [ReturnValue]::new(500, 'Zip Installer Not Supported', "Zip installers are only supported with ManifestVersion 1.4.0 or later. Current ManifestVersion: $ManifestVersion", 2)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
|
|
# If the installer requires nested installer files, get them
|
|
$_Installer = Read-NestedInstaller $_Installer
|
|
|
|
$_Switches = [ordered] @{}
|
|
# If Installer Type is `exe`, require the silent switches to be entered
|
|
if ((Get-EffectiveInstallerType $_Installer) -ne 'portable') {
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
if ((Get-EffectiveInstallerType $_Installer) -ieq 'exe') { Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the silent install switch. For example: /S, -verysilent, /qn, --silent, /exenoui' }
|
|
else { Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the silent install switch. For example: /S, -verysilent, /qn, --silent, /exenoui' }
|
|
Read-Host -Prompt 'Silent switch' -OutVariable _ | Out-Null
|
|
if ($_) { $_Switches['Silent'] = $_ | TrimString }
|
|
|
|
if (Test-String $_Switches['Silent'] -MaxLength $Patterns.SilentSwitchMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} elseif ((Get-EffectiveInstallerType $_Installer) -ne 'exe' -and (Test-String $_Switches['Silent'] -MaxLength $Patterns.SilentSwitchMaxLength -AllowNull)) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.SilentSwitchMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
if ((Get-EffectiveInstallerType $_Installer) -ieq 'exe') { Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the silent with progress install switch. For example: /S, -silent, /qb, /exebasicui' }
|
|
else { Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the silent with progress install switch. For example: /S, -silent, /qb, /exebasicui' }
|
|
Read-Host -Prompt 'Silent with progress switch' -OutVariable _ | Out-Null
|
|
if ($_) { $_Switches['SilentWithProgress'] = $_ | TrimString }
|
|
|
|
if (Test-String $_Switches['SilentWithProgress'] -MaxLength $Patterns.ProgressSwitchMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} elseif ((Get-EffectiveInstallerType $_Installer) -ne 'exe' -and (Test-String $_Switches['SilentWithProgress'] -MaxLength $Patterns.ProgressSwitchMaxLength -AllowNull)) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.ProgressSwitchMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
|
|
# Optional entry of `Custom` switches with validation for all installer types
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter any custom switches for the installer. For example: /norestart, -norestart'
|
|
Read-Host -Prompt 'Custom Switch' -OutVariable _ | Out-Null
|
|
if ($_) { $_Switches['Custom'] = $_ | TrimString }
|
|
if (Test-String $_Switches['Custom'] -MaxLength $Patterns.CustomSwitchMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.CustomSwitchMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
if ($_Switches.Keys.Count -gt 0) { $_Installer['InstallerSwitches'] = $_Switches }
|
|
|
|
# If the installer is `msix` or `appx`, prompt for or detect additional fields
|
|
if ($_Installer['InstallerType'] -in @('msix'; 'appx')) {
|
|
# Detect or prompt for Signature Sha256
|
|
if (Get-Command 'winget' -ErrorAction SilentlyContinue) { $SignatureSha256 = winget hash -m $script:dest | Select-String -Pattern 'SignatureSha256:' | ConvertFrom-String; if ($SignatureSha256.P2) { $SignatureSha256 = $SignatureSha256.P2.ToUpper() } }
|
|
if ($SignatureSha256) { $_Installer['SignatureSha256'] = $SignatureSha256 }
|
|
if (Test-String $_Installer['SignatureSha256'] -IsNull) {
|
|
# Manual entry of Signature Sha256 with validation
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Recommended] Enter the installer SignatureSha256'
|
|
Read-Host -Prompt 'SignatureSha256' -OutVariable _ | Out-Null
|
|
if ($_) { $_Installer['SignatureSha256'] = $_ | TrimString }
|
|
if (Test-String $_Installer['SignatureSha256'] -MatchPattern $Patterns.SignatureSha256 -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
|
|
# Prompt user to find package name automatically, unless package was not downloaded
|
|
if ($script:SaveOption -eq '2' -or (!$(Test-Path $script:dest))) {
|
|
$ChoicePfn = '1'
|
|
} else {
|
|
$_menu = @{
|
|
entries = @('*[F] Find Automatically'; '[M] Manually Enter PackageFamilyName')
|
|
Prompt = 'Discover the package family name?'
|
|
DefaultString = 'F'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'M' { $ChoicePfn = '1' }
|
|
default { $ChoicePfn = '0' }
|
|
}
|
|
}
|
|
|
|
# If user selected to find automatically -
|
|
# Install package, get family name, uninstall package
|
|
if ($ChoicePfn -eq '0') {
|
|
$_Installer['PackageFamilyName'] = Get-PackageFamilyName $script:dest
|
|
if (Test-String -not $_Installer['PackageFamilyName'] -MatchPattern $Patterns.FamilyName) {
|
|
$script:_returnValue = [ReturnValue]::new(500, 'Could not find PackageFamilyName', 'Value should be entered manually', 1)
|
|
}
|
|
}
|
|
|
|
# Validate Package Family Name if found automatically
|
|
# Allow for manual entry if selected or if validation failed
|
|
do {
|
|
if (($ChoicePfn -ne '0') -or ($script:_returnValue.StatusCode -ne [ReturnValue]::Success().StatusCode)) {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Recommended] Enter the PackageFamilyName'
|
|
Read-Host -Prompt 'PackageFamilyName' -OutVariable _ | Out-Null
|
|
if ($_) { $_Installer['PackageFamilyName'] = $_ | TrimString }
|
|
}
|
|
if (Test-String $_Installer['PackageFamilyName'] -MaxLength $Patterns.FamilyNameMaxLength -MatchPattern $Patterns.FamilyName -AllowNull) {
|
|
if (Test-String $_Installer['PackageFamilyName'] -IsNull) { $_Installer['PackageFamilyName'] = "$([char]0x2370)" }
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $_Installer['PackageFamilyName'] -MaxLength $Patterns.FamilyNameMaxLength) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.FamilyNameMaxLength)
|
|
} elseif (Test-String -not $_Installer['PackageFamilyName'] -MatchPattern $Patterns.FamilyName) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
|
|
# Request installer locale with validation as optional
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the installer locale. For example: en-US, en-CA'
|
|
Write-Host -ForegroundColor 'Blue' -Object 'https://docs.microsoft.com/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a'
|
|
Read-Host -Prompt 'InstallerLocale' -OutVariable _
|
|
if ($_) { $_Installer['InstallerLocale'] = $_ | TrimString }
|
|
# If user defined a default locale, add it
|
|
if ((Test-String $_Installer['InstallerLocale'] -IsNull) -and (Test-String -not $ScriptSettings.DefaultInstallerLocale -IsNull)) { $_Installer['InstallerLocale'] = $ScriptSettings.DefaultInstallerLocale }
|
|
|
|
if (Test-String $_Installer['InstallerLocale'] -MaxLength $Patterns.InstallerLocaleMaxLength -MatchPattern $Patterns.PackageLocale -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $_Installer['InstallerLocale'] -MaxLength $Patterns.InstallerLocaleMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(0, $Patterns.InstallerLocaleMaxLength)
|
|
} elseif (Test-String -not $_Installer['InstallerLocale'] -MatchPattern $Patterns.PackageLocale) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request product code with validation
|
|
if ((Get-EffectiveInstallerType $_Installer) -notmatch 'portable') {
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application product code. Looks like {CF8E6E00-9C03-4440-81C0-21FACB921A6B}'
|
|
Write-Host -ForegroundColor 'White' -Object "ProductCode found from installer: $($_Installer['ProductCode'])"
|
|
Write-Host -ForegroundColor 'White' -Object 'Can be found with ' -NoNewline; Write-Host -ForegroundColor 'DarkYellow' 'get-wmiobject Win32_Product | Sort-Object Name | Format-Table IdentifyingNumber, Name -AutoSize'
|
|
$NewProductCode = Read-Host -Prompt 'ProductCode' | TrimString
|
|
if (Test-String $NewProductCode -Not -IsNull) { $_Installer['ProductCode'] = $NewProductCode }
|
|
elseif (Test-String $_Installer['ProductCode'] -Not -IsNull) { $_Installer['ProductCode'] = "$($_Installer['ProductCode'])" }
|
|
|
|
if (Test-String $_Installer['ProductCode'] -MinLength $Patterns.ProductCodeMinLength -MaxLength $Patterns.ProductCodeMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.ProductCodeMinLength, $Patterns.ProductCodeMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Manual Entry of Scope
|
|
if (Test-String $_Installer['Scope'] -IsNull) {
|
|
$_menu = @{
|
|
entries = @('[M] Machine'; '[U] User'; '*[N] No idea')
|
|
Prompt = '[Optional] Enter the Installer Scope'
|
|
DefaultString = 'N'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'M' { $_Installer['Scope'] = 'machine' }
|
|
'U' { $_Installer['Scope'] = 'user' }
|
|
default { }
|
|
}
|
|
}
|
|
|
|
# Request upgrade behavior
|
|
$_menu = @{
|
|
entries = @('*[I] Install'; '[U] Uninstall Previous')
|
|
Prompt = '[Optional] Enter the Upgrade Behavior'
|
|
DefaultString = 'I'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'U' { $_Installer['UpgradeBehavior'] = 'uninstallPrevious' }
|
|
default { $_Installer['UpgradeBehavior'] = 'install' }
|
|
}
|
|
Write-Host
|
|
}
|
|
|
|
# Request release date
|
|
$script:ReleaseDatePrompted = $true
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application release date. Example: 2021-11-17'
|
|
Read-Host -Prompt 'ReleaseDate' -OutVariable ReleaseDate | Out-Null
|
|
try {
|
|
Get-Date([datetime]$($ReleaseDate | TrimString)) -f 'yyyy-MM-dd' -OutVariable _ValidDate | Out-Null
|
|
if ($_ValidDate) { $_Installer['ReleaseDate'] = $_ValidDate | TrimString }
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} catch {
|
|
if (Test-String $ReleaseDate -IsNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Invalid Date', 'Input could not be resolved to a date', 2)
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
$AppsAndFeaturesEntries = Read-AppsAndFeaturesEntries $_Installer
|
|
if ($AppsAndFeaturesEntries) {
|
|
$_Installer['AppsAndFeaturesEntries'] = @($AppsAndFeaturesEntries)
|
|
}
|
|
|
|
if ($script:SaveOption -eq '1' -and (Test-Path -Path $script:dest)) { $script:CleanupPaths += $script:dest }
|
|
|
|
# If the installers array is empty, create it
|
|
if (!$script:Installers) {
|
|
$script:Installers = @()
|
|
}
|
|
|
|
# Add the completed installer to the installers array
|
|
$_Installer = Restore-YamlKeyOrder $_Installer $InstallerEntryProperties -NoComments
|
|
$script:Installers += $_Installer
|
|
|
|
# Prompt the user for additional intaller entries
|
|
$_menu = @{
|
|
entries = @(
|
|
'[Y] Yes'
|
|
'*[N] No'
|
|
)
|
|
Prompt = 'Do you want to create another installer?'
|
|
DefaultString = 'N'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'Y' { $AnotherInstaller = '0' }
|
|
'N' { $AnotherInstaller = '1' }
|
|
default { $AnotherInstaller = '1' }
|
|
}
|
|
|
|
# If there are additional entries, run this function again to fetch the values and add them to the installers array
|
|
if ($AnotherInstaller -eq '0') {
|
|
Write-Host; Read-InstallerEntry
|
|
}
|
|
}
|
|
|
|
# Prompts user for Installer Values using the `Quick Update` Method
|
|
# Sets the $script:Installers value as an output
|
|
# Returns void
|
|
Function Read-QuickInstallerEntry {
|
|
# We know old manifests exist if we got here without error
|
|
# Fetch the old installers based on the manifest type
|
|
if ($script:OldInstallerManifest) { $_OldInstallers = $script:OldInstallerManifest['Installers'] } else {
|
|
$_OldInstallers = $script:OldVersionManifest['Installers']
|
|
}
|
|
|
|
$_iteration = 0
|
|
$_NewInstallers = @()
|
|
foreach ($_OldInstaller in $_OldInstallers) {
|
|
# Create the new installer as an exact copy of the old installer entry
|
|
# This is to ensure all previously entered and un-modified parameters are retained
|
|
$_iteration += 1
|
|
$_NewInstaller = $_OldInstaller
|
|
$_NewInstaller.Remove('InstallerSha256');
|
|
|
|
# Show the user which installer entry they should be entering information for
|
|
Write-Host -ForegroundColor 'Green' "Installer Entry #$_iteration`:`n"
|
|
if ($_OldInstaller.InstallerLocale) { Write-Host -ForegroundColor 'Yellow' "`tInstallerLocale: $($_OldInstaller.InstallerLocale)" }
|
|
if ($_OldInstaller.Architecture) { Write-Host -ForegroundColor 'Yellow' "`tArchitecture: $($_OldInstaller.Architecture)" }
|
|
if ($_OldInstaller.InstallerType) { Write-Host -ForegroundColor 'Yellow' "`tInstallerType: $($_OldInstaller.InstallerType)" }
|
|
if ($_OldInstaller.NestedInstallerType ) { Write-Host -ForegroundColor 'Yellow' "`tNestedInstallerType: $($_OldInstaller.NestedInstallerType)" }
|
|
if ($_OldInstaller.Scope) { Write-Host -ForegroundColor 'Yellow' "`tScope: $($_OldInstaller.Scope)" }
|
|
Write-Host
|
|
|
|
# Request user enter the new Installer URL
|
|
$_NewInstaller['InstallerUrl'] = Request-InstallerUrl
|
|
|
|
if ($_NewInstaller.InstallerUrl -in ($_NewInstallers).InstallerUrl) {
|
|
$_MatchingInstaller = $_NewInstallers | Where-Object { $_.InstallerUrl -eq $_NewInstaller.InstallerUrl } | Select-Object -First 1
|
|
if ($_MatchingInstaller.InstallerSha256) { $_NewInstaller['InstallerSha256'] = $_MatchingInstaller.InstallerSha256 }
|
|
if ($_MatchingInstaller.InstallerType) { $_NewInstaller['InstallerType'] = $_MatchingInstaller.InstallerType }
|
|
if ($_MatchingInstaller.ProductCode) { $_NewInstaller['ProductCode'] = $_MatchingInstaller.ProductCode }
|
|
elseif ( ($_NewInstaller.Keys -contains 'ProductCode') -and ($script:dest -notmatch '.exe$')) { $_NewInstaller.Remove('ProductCode') }
|
|
if ($_MatchingInstaller.PackageFamilyName) { $_NewInstaller['PackageFamilyName'] = $_MatchingInstaller.PackageFamilyName }
|
|
elseif ($_NewInstaller.Keys -contains 'PackageFamilyName') { $_NewInstaller.Remove('PackageFamilyName') }
|
|
if ($_MatchingInstaller.SignatureSha256) { $_NewInstaller['SignatureSha256'] = $_MatchingInstaller.SignatureSha256 }
|
|
elseif ($_NewInstaller.Keys -contains 'SignatureSha256') { $_NewInstaller.Remove('SignatureSha256') }
|
|
}
|
|
|
|
if ($_NewInstaller.Keys -notcontains 'InstallerSha256') {
|
|
try {
|
|
Write-Host -ForegroundColor 'Green' 'Downloading Installer. . .'
|
|
$script:dest = Get-InstallerFile -URI $_NewInstaller['InstallerUrl'] -PackageIdentifier $PackageIdentifier -PackageVersion $PackageVersion
|
|
} catch {
|
|
# Here we also want to pass any exceptions through for potential debugging
|
|
throw [System.Net.WebException]::new('The file could not be downloaded. Try running the script again', $_.Exception)
|
|
}
|
|
# Check that MSI's aren't actually WIX, and EXE's aren't NSIS, INNO or BURN
|
|
Write-Host -ForegroundColor 'Green' "Installer Downloaded!`nProcessing installer data. . . "
|
|
if ($_NewInstaller['InstallerType'] -in @('msi'; 'exe')) {
|
|
$DetectedType = Resolve-InstallerType $script:dest
|
|
if ($DetectedType -in @('msi'; 'wix'; 'nullsoft'; 'inno'; 'burn')) { $_NewInstaller['InstallerType'] = $DetectedType }
|
|
}
|
|
# Get the Sha256
|
|
$_NewInstaller['InstallerSha256'] = (Get-FileHash -Path $script:dest -Algorithm SHA256).Hash
|
|
# Update the product code, if a new one exists
|
|
# If a new product code doesn't exist, and the installer isn't an `.exe` file, remove the product code if it exists
|
|
$MSIProductCode = $null
|
|
if ([System.Environment]::OSVersion.Platform -match 'Win' -and ($script:dest).EndsWith('.msi')) {
|
|
$MSIProductCode = ([string](Get-MSIProperty -Path $script:dest -Property 'ProductCode') | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
|
|
} elseif ([System.Environment]::OSVersion.Platform -match 'Unix' -and (Get-Item $script:dest).Name.EndsWith('.msi')) {
|
|
$MSIProductCode = ([string](file $script:dest) | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
|
|
}
|
|
if (Test-String -not $MSIProductCode -IsNull) {
|
|
$_NewInstaller['ProductCode'] = $MSIProductCode
|
|
} elseif ( ($_NewInstaller.Keys -contains 'ProductCode') -and ((Get-EffectiveInstallerType $_NewInstaller) -in @('appx'; 'msi'; 'msix'; 'wix'; 'burn'))) {
|
|
$_NewInstaller.Remove('ProductCode')
|
|
}
|
|
# If the installer is msix or appx, try getting the new SignatureSha256
|
|
# If the new SignatureSha256 can't be found, remove it if it exists
|
|
$NewSignatureSha256 = $null
|
|
if ($_NewInstaller.InstallerType -in @('msix', 'appx')) {
|
|
if (Get-Command 'winget' -ErrorAction SilentlyContinue) { $NewSignatureSha256 = winget hash -m $script:dest | Select-String -Pattern 'SignatureSha256:' | ConvertFrom-String; if ($NewSignatureSha256.P2) { $NewSignatureSha256 = $NewSignatureSha256.P2.ToUpper() } }
|
|
}
|
|
if (Test-String -not $NewSignatureSha256 -IsNull) {
|
|
$_NewInstaller['SignatureSha256'] = $NewSignatureSha256
|
|
} elseif ($_NewInstaller.Keys -contains 'SignatureSha256') {
|
|
$_NewInstaller.Remove('SignatureSha256')
|
|
}
|
|
# If the installer is msix or appx, try getting the new package family name
|
|
# If the new package family name can't be found, remove it if it exists
|
|
if ($script:dest -match '\.(msix|appx)(bundle){0,1}$') {
|
|
$PackageFamilyName = Get-PackageFamilyName $script:dest
|
|
if (Test-String $PackageFamilyName -MatchPattern $Patterns.FamilyName) {
|
|
$_NewInstaller['PackageFamilyName'] = $PackageFamilyName
|
|
} elseif ($_NewInstaller.Keys -contains 'PackageFamilyName') {
|
|
$_NewInstaller.Remove('PackageFamilyName')
|
|
}
|
|
}
|
|
# Remove the downloaded files
|
|
Remove-Item -Path $script:dest
|
|
Write-Host -ForegroundColor 'Green' "Installer updated!`n"
|
|
}
|
|
|
|
# Force a re-check of the Nested Installer Paths in case they changed between versions
|
|
$_NewInstaller = Read-NestedInstaller $_NewInstaller
|
|
|
|
# Force a re-check of the ARP entries in case they changed between versions
|
|
$AppsAndFeaturesEntries = Read-AppsAndFeaturesEntries $_NewInstaller
|
|
if ($AppsAndFeaturesEntries) {
|
|
$_NewInstaller['AppsAndFeaturesEntries'] = @($AppsAndFeaturesEntries)
|
|
}
|
|
|
|
#Add the updated installer to the new installers array
|
|
$_NewInstaller = Restore-YamlKeyOrder $_NewInstaller $InstallerEntryProperties -NoComments
|
|
$_NewInstallers += $_NewInstaller
|
|
}
|
|
$script:Installers = $_NewInstallers
|
|
}
|
|
|
|
# Requests the user enter an optional value with a prompt
|
|
# If the value already exists, also print the existing value
|
|
# Returns the new value if entered, Returns the existing value if no new value was entered
|
|
Function Read-InstallerMetadataValue {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[AllowNull()]
|
|
[PSCustomObject] $Variable,
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[string] $Key,
|
|
[Parameter(Mandatory = $true, Position = 2)]
|
|
[string] $Prompt
|
|
)
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object $Prompt
|
|
if (Test-String -not $Variable -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $Variable" }
|
|
$NewValue = Read-Host -Prompt $Key | TrimString
|
|
|
|
if (Test-String -not $NewValue -IsNull) {
|
|
return $NewValue
|
|
} else {
|
|
return $Variable
|
|
}
|
|
}
|
|
|
|
# Sorts keys within an object based on a reference ordered dictionary
|
|
# If a key does not exist, it sets the value to a special character to be removed / commented later
|
|
# Returns the result as a new object
|
|
Function Restore-YamlKeyOrder {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[PSCustomObject] $InputObject,
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[PSCustomObject] $SortOrder,
|
|
[switch] $NoComments
|
|
)
|
|
|
|
$_ExcludedKeys = @(
|
|
'InstallerSwitches'
|
|
'Capabilities'
|
|
'RestrictedCapabilities'
|
|
'InstallerSuccessCodes'
|
|
'ProductCode'
|
|
'UpgradeCode'
|
|
'PackageFamilyName'
|
|
'InstallerLocale'
|
|
'InstallerType'
|
|
'NestedInstallerType'
|
|
'NestedInstallerFiles'
|
|
'Scope'
|
|
'UpgradeBehavior'
|
|
'Dependencies'
|
|
'InstallationMetadata'
|
|
'Platform'
|
|
'Icons'
|
|
'Agreements'
|
|
)
|
|
|
|
$_Temp = [ordered] @{}
|
|
$SortOrder.GetEnumerator() | ForEach-Object {
|
|
if ($InputObject.Contains($_)) {
|
|
$_Temp.Add($_, $InputObject[$_])
|
|
} else {
|
|
if (!$NoComments -and $_ -notin $_ExcludedKeys) {
|
|
$_Temp.Add($_, "$([char]0x2370)")
|
|
}
|
|
}
|
|
}
|
|
return $_Temp
|
|
}
|
|
|
|
# Requests the user to input optional values for the Installer Manifest file
|
|
Function Read-InstallerMetadata {
|
|
Write-Host
|
|
|
|
# Request File Extensions and validate
|
|
do {
|
|
if (!$FileExtensions) { $FileExtensions = '' }
|
|
else { $FileExtensions = $FileExtensions | ToLower | UniqueItems }
|
|
$script:FileExtensions = Read-InstallerMetadataValue -Variable $FileExtensions -Key 'FileExtensions' -Prompt "[Optional] Enter any File Extensions the application could support. For example: html, htm, url (Max $($Patterns.MaxItemsFileExtensions))" | ToLower | UniqueItems
|
|
|
|
if (($script:FileExtensions -split ',').Count -le $Patterns.MaxItemsFileExtensions -and $($script:FileExtensions.Split(',').Trim() | Where-Object { Test-String -Not $_ -MaxLength $Patterns.FileExtensionMaxLength -MatchPattern $Patterns.FileExtension -AllowNull }).Count -eq 0) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (($script:FileExtensions -split ',').Count -gt $Patterns.MaxItemsFileExtensions ) {
|
|
$script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsFileExtensions)
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Invalid Entries', "Some entries do not match the requirements defined in the manifest schema - $($script:FileExtensions.Split(',').Trim() | Where-Object { Test-String -Not $_ -MaxLength $Patterns.FileExtensionMaxLength -MatchPattern $Patterns.FileExtension })", 2)
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Protocols and validate
|
|
do {
|
|
if (!$Protocols) { $Protocols = '' }
|
|
else { $Protocols = $Protocols | ToLower | UniqueItems }
|
|
$script:Protocols = Read-InstallerMetadataValue -Variable $Protocols -Key 'Protocols' -Prompt "[Optional] Enter any Protocols the application provides a handler for. For example: http, https (Max $($Patterns.MaxItemsProtocols))" | ToLower | UniqueItems
|
|
if (($script:Protocols -split ',').Count -le $Patterns.MaxItemsProtocols) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsProtocols)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Commands and validate
|
|
do {
|
|
if (!$Commands) { $Commands = '' }
|
|
else { $Commands = $Commands | UniqueItems }
|
|
$script:Commands = Read-InstallerMetadataValue -Variable $Commands -Key 'Commands' -Prompt "[Optional] Enter any Commands or aliases to run the application. For example: msedge (Max $($Patterns.MaxItemsCommands))" | UniqueItems
|
|
if (($script:Commands -split ',').Count -le $Patterns.MaxItemsCommands) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsCommands)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Installer Success Codes and validate
|
|
do {
|
|
if (!$InstallerSuccessCodes) { $InstallerSuccessCodes = '' }
|
|
$script:InstallerSuccessCodes = Read-InstallerMetadataValue -Variable $InstallerSuccessCodes -Key 'InstallerSuccessCodes' -Prompt "[Optional] List of additional non-zero installer success exit codes other than known default values by winget (Max $($Patterns.MaxItemsSuccessCodes))" | UniqueItems
|
|
if (($script:InstallerSuccessCodes -split ',').Count -le $Patterns.MaxItemsSuccessCodes) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
try {
|
|
#Ensure all values are integers
|
|
$script:InstallerSuccessCodes.Split(',').Trim() | ForEach-Object { [long]$_ }
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} catch {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Invalid Data Type', 'The value entered does not match the type requirements defined in the manifest schema', 2)
|
|
}
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsSuccessCodes)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Install Modes and validate
|
|
do {
|
|
if ($script:InstallModes) { $script:InstallModes = $script:InstallModes | UniqueItems }
|
|
$script:InstallModes = Read-InstallerMetadataValue -Variable $script:InstallModes -Key 'InstallModes' -Prompt "[Optional] List of supported installer modes. Options: $($Patterns.ValidInstallModes -join ', ')"
|
|
if ($script:InstallModes) { $script:InstallModes = $script:InstallModes | UniqueItems }
|
|
if ( (Test-String $script:InstallModes -IsNull) -or (($script:InstallModes -split ',').Count -le $Patterns.MaxItemsInstallModes -and $($script:InstallModes.Split(',').Trim() | Where-Object { $_ -CNotIn $Patterns.ValidInstallModes }).Count -eq 0)) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (($script:InstallModes -split ',').Count -gt $Patterns.MaxItemsInstallModes ) {
|
|
$script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsInstallModes)
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Invalid Entries', "Some entries do not match the requirements defined in the manifest schema - $($script:InstallModes.Split(',').Trim() | Where-Object { $_ -CNotIn $Patterns.ValidInstallModes })", 2)
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
|
|
# Requests the user to input values for the Locale Manifest file
|
|
Function Read-LocaleMetadata {
|
|
# Request Package Locale and Validate
|
|
if (Test-String -not $script:PackageLocale -MaxLength $Patterns.PackageLocaleMaxLength -MatchPattern $Patterns.PackageLocale -NotNull) {
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the Package Locale. For example: en-US, en-CA'
|
|
Write-Host -ForegroundColor 'Blue' 'Reference Link: https://docs.microsoft.com/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a'
|
|
$script:PackageLocale = Read-Host -Prompt 'PackageLocale' | TrimString
|
|
if (Test-String $script:PackageLocale -MaxLength $Patterns.PackageLocaleMaxLength -MatchPattern $Patterns.PackageLocale -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String $script:PackageLocale -not -MaxLength $Patterns.PackageLocaleMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.PackageLocaleMaxLength)
|
|
} elseif (Test-String $script:PackageLocale -not -MatchPattern $Patterns.PackageLocale ) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
|
|
# Request Publisher Name and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
if (Test-String $script:Publisher -IsNull) {
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the full publisher name. For example: Microsoft Corporation'
|
|
} else {
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the full publisher name. For example: Microsoft Corporation'
|
|
Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Publisher"
|
|
}
|
|
$NewPublisher = Read-Host -Prompt 'Publisher' | TrimString
|
|
if (Test-String $NewPublisher -NotNull) {
|
|
$script:Publisher = $NewPublisher
|
|
}
|
|
if (Test-String $script:Publisher -MaxLength $Patterns.PublisherMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.PublisherMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Application Name and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
if (Test-String $script:PackageName -IsNull) {
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the full application name. For example: Microsoft Teams'
|
|
} else {
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the full application name. For example: Microsoft Teams'
|
|
Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PackageName"
|
|
}
|
|
$NewPackageName = Read-Host -Prompt 'PackageName' | TrimString
|
|
if (Test-String -not $NewPackageName -IsNull) { $script:PackageName = $NewPackageName }
|
|
|
|
if (Test-String $script:PackageName -MaxLength $Patterns.PackageNameMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.PackageNameMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# If the option is `NewLocale` then these moniker should already exist
|
|
# If the option is not `NewLocale`, Request Moniker and Validate
|
|
if ($Option -ne 'NewLocale') {
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Moniker (friendly name/alias). For example: vscode'
|
|
if (Test-String -not $script:Moniker -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Moniker" }
|
|
$NewMoniker = Read-Host -Prompt 'Moniker' | ToLower | TrimString | NoWhitespace
|
|
if (Test-String -not $NewMoniker -IsNull) { $script:Moniker = $NewMoniker }
|
|
|
|
if (Test-String $script:Moniker -MaxLength $Patterns.MonikerMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.MonikerMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
|
|
#Request Publisher URL and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Publisher Url.'
|
|
if (Test-String -not $script:PublisherUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PublisherUrl" }
|
|
$NewPublisherUrl = Read-Host -Prompt 'Publisher Url' | TrimString
|
|
if (Test-String -not $NewPublisherUrl -IsNull) { $script:PublisherUrl = $NewPublisherUrl }
|
|
if (Test-String $script:PublisherUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $script:PublisherUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
|
|
} elseif (Test-String -not $script:PublisherUrl -MatchPattern $Patterns.GenericUrl) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Publisher Support URL and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Publisher Support Url.'
|
|
if (Test-String -not $script:PublisherSupportUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PublisherSupportUrl" }
|
|
$NewPublisherSupportUrl = Read-Host -Prompt 'Publisher Support Url' | TrimString
|
|
if (Test-String -not $NewPublisherSupportUrl -IsNull) { $script:PublisherSupportUrl = $NewPublisherSupportUrl }
|
|
if (Test-String $script:PublisherSupportUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $script:PublisherSupportUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
|
|
} elseif (Test-String -not $script:PublisherSupportUrl -MatchPattern $Patterns.GenericUrl) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Publisher Privacy URL and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Publisher Privacy Url.'
|
|
if (Test-String -not $script:PrivacyUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PrivacyUrl" }
|
|
$NewPrivacyUrl = Read-Host -Prompt 'Publisher Privacy Url' | TrimString
|
|
if (Test-String -not $NewPrivacyUrl -IsNull) { $script:PrivacyUrl = $NewPrivacyUrl }
|
|
|
|
if (Test-String $script:PrivacyUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $script:PrivacyUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
|
|
} elseif (Test-String -not $script:PrivacyUrl -MatchPattern $Patterns.GenericUrl) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Author and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application Author.'
|
|
if (Test-String -not $script:Author -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Author" }
|
|
$NewAuthor = Read-Host -Prompt 'Author' | TrimString
|
|
if (Test-String -not $NewAuthor -IsNull) { $script:Author = $NewAuthor }
|
|
|
|
if (Test-String $script:Author -MinLength $Patterns.AuthorMinLength -MaxLength $Patterns.AuthorMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.AuthorMinLength, $Patterns.AuthorMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Package URL and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Url to the homepage of the application.'
|
|
if (Test-String -not $script:PackageUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PackageUrl" }
|
|
$NewPackageUrl = Read-Host -Prompt 'Homepage' | TrimString
|
|
if (Test-String -not $NewPackageUrl -IsNull) { $script:PackageUrl = $NewPackageUrl }
|
|
if (Test-String $script:PackageUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $script:PackageUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
|
|
} elseif (Test-String -not $script:PackageUrl -MatchPattern $Patterns.GenericUrl) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request License and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
if (Test-String $script:License -IsNull) {
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the application License. For example: MIT, GPL, Freeware, Proprietary'
|
|
} else {
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application License. For example: MIT, GPL, Freeware, Proprietary'
|
|
Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:License"
|
|
}
|
|
$NewLicense = Read-Host -Prompt 'License' | TrimString
|
|
if (Test-String -not $NewLicense -IsNull) { $script:License = $NewLicense }
|
|
if (Test-String $script:License -MinLength $Patterns.LicenseMinLength -MaxLength $Patterns.LicenseMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} elseif (Test-String $script:License -IsNull) {
|
|
$script:_returnValue = [ReturnValue]::new(400, 'Required Field', 'The value entered cannot be null or empty', 2)
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.LicenseMinLength, $Patterns.LicenseMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request License URL and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application License URL.'
|
|
if (Test-String -not $script:LicenseUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:LicenseUrl" }
|
|
$NewLicenseUrl = Read-Host -Prompt 'License URL' | TrimString
|
|
if (Test-String -not $NewLicenseUrl -IsNull) { $script:LicenseUrl = $NewLicenseUrl }
|
|
|
|
if (Test-String $script:LicenseUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $script:LicenseUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
|
|
} elseif (Test-String -not $script:LicenseUrl -MatchPattern $Patterns.GenericUrl) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Copyright and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application Copyright.'
|
|
Write-Host -ForegroundColor 'Blue' 'Example: Copyright (c) Microsoft Corporation'
|
|
if (Test-String -not $script:Copyright -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Copyright" }
|
|
$NewCopyright = Read-Host -Prompt 'Copyright' | TrimString
|
|
if (Test-String -not $NewCopyright -IsNull) { $script:Copyright = $NewCopyright }
|
|
if (Test-String $script:Copyright -MinLength $Patterns.CopyrightMinLength -MaxLength $Patterns.CopyrightMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.CopyrightMinLength, $Patterns.CopyrightMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Copyright URL and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application Copyright Url.'
|
|
if (Test-String -not $script:CopyrightUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:CopyrightUrl" }
|
|
$NewCopyrightUrl = Read-Host -Prompt 'CopyrightUrl' | TrimString
|
|
if (Test-String -not $NewCopyrightUrl -IsNull) { $script:CopyrightUrl = $NewCopyrightUrl }
|
|
if (Test-String $script:CopyrightUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $script:CopyrightUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
|
|
} elseif (Test-String -not $script:CopyrightUrl -MatchPattern $Patterns.GenericUrl) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Tags and Validate
|
|
do {
|
|
$script:Tags = [string]$script:Tags
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter any tags that would be useful to discover this tool.'
|
|
Write-Host -ForegroundColor 'Blue' -Object 'Example: zip, c++, photos, OBS (Max', ($Patterns.TagsMaxItems), 'items)'
|
|
if (Test-String -not $script:Tags -IsNull) {
|
|
$script:Tags = $script:Tags | ToLower | UniqueItems
|
|
Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Tags"
|
|
}
|
|
$NewTags = Read-Host -Prompt 'Tags' | TrimString | ToLower | UniqueItems
|
|
if (Test-String -not $NewTags -IsNull) { $script:Tags = $NewTags }
|
|
if (($script:Tags -split ',').Count -le $Patterns.TagsMaxItems) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.TagsMaxItems)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Short Description and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
if (Test-String $script:ShortDescription -IsNull) {
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter a short description of the application.'
|
|
} else {
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter a short description of the application.'
|
|
Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:ShortDescription"
|
|
}
|
|
$NewShortDescription = Read-Host -Prompt 'Short Description' | TrimString
|
|
if (Test-String -not $NewShortDescription -IsNull) { $script:ShortDescription = $NewShortDescription }
|
|
if (Test-String $script:ShortDescription -MaxLength $Patterns.ShortDescriptionMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.ShortDescriptionMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request Long Description and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter a long description of the application.'
|
|
if (Test-String -not $script:Description -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Description" }
|
|
$NewDescription = Read-Host -Prompt 'Description' | TrimString
|
|
if (Test-String -not $NewDescription -IsNull) { $script:Description = $NewDescription }
|
|
if (Test-String $script:Description -MinLength $Patterns.DescriptionMinLength -MaxLength $Patterns.DescriptionMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.DescriptionMinLength, $Patterns.DescriptionMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request ReleaseNotes and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter release notes for this version of the package.'
|
|
$script:ReleaseNotes = Read-Host -Prompt 'ReleaseNotes' | TrimString
|
|
if (Test-String $script:ReleaseNotes -MinLength $Patterns.ReleaseNotesMinLength -MaxLength $Patterns.ReleaseNotesMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError($Patterns.ReleaseNotesMinLength, $Patterns.ReleaseNotesMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Request ReleaseNotes URL and Validate
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the release notes URL for this version of the package.'
|
|
$script:ReleaseNotesUrl = Read-Host -Prompt 'ReleaseNotesUrl' | TrimString
|
|
if (Test-String $script:ReleaseNotesUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $script:ReleaseNotesUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
|
|
} elseif (Test-String -not $script:ReleaseNotesUrl -MatchPattern $Patterns.GenericUrl) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
}
|
|
|
|
# Requests the user to answer the prompts found in the winget-pkgs pull request template
|
|
# Uses this template and responses to create a PR
|
|
Function Read-PRBody {
|
|
$PrBodyContent = Get-Content $args[0]
|
|
ForEach ($_line in $PrBodyContent) {
|
|
# | Where-Object { $_ -like '-*[ ]*' }))
|
|
if ($_line -like '-*[ ]*' ) {
|
|
$_showMenu = $true
|
|
switch -Wildcard ( $_line ) {
|
|
'*CLA*' {
|
|
if ($ScriptSettings.SignedCLA -eq 'true') {
|
|
$PrBodyContent = $PrBodyContent.Replace($_line, $_line.Replace('[ ]', '[x]'))
|
|
$_showMenu = $false
|
|
} else {
|
|
$_menu = @{
|
|
Prompt = 'Have you signed the Contributor License Agreement (CLA)?'
|
|
Entries = @('[Y] Yes'; '*[N] No')
|
|
HelpText = 'Reference Link: https://cla.opensource.microsoft.com/microsoft/winget-pkgs'
|
|
HelpTextColor = ''
|
|
DefaultString = 'N'
|
|
}
|
|
}
|
|
}
|
|
|
|
'*open `[pull requests`]*' {
|
|
$_menu = @{
|
|
Prompt = "Have you checked that there aren't other open pull requests for the same manifest update/change?"
|
|
Entries = @('[Y] Yes'; '*[N] No')
|
|
HelpText = 'Reference Link: https://github.com/microsoft/winget-pkgs/pulls'
|
|
HelpTextColor = ''
|
|
DefaultString = 'N'
|
|
}
|
|
}
|
|
|
|
'*winget validate*' {
|
|
if ($? -and $(Get-Command 'winget' -ErrorAction SilentlyContinue)) {
|
|
$PrBodyContent = $PrBodyContent.Replace($_line, $_line.Replace('[ ]', '[x]'))
|
|
$_showMenu = $false
|
|
} elseif ($script:Option -ne 'RemoveManifest') {
|
|
$_menu = @{
|
|
Prompt = "Have you validated your manifest locally with 'winget validate --manifest <path>'?"
|
|
Entries = @('[Y] Yes'; '*[N] No')
|
|
HelpText = 'Automatic manifest validation failed. Check your manifest and try again'
|
|
HelpTextColor = 'Red'
|
|
DefaultString = 'N'
|
|
}
|
|
} else {
|
|
$_showMenu = $false
|
|
}
|
|
}
|
|
|
|
'*tested your manifest*' {
|
|
if ($script:SandboxTest -eq '0') {
|
|
$PrBodyContent = $PrBodyContent.Replace($_line, $_line.Replace('[ ]', '[x]'))
|
|
$_showMenu = $false
|
|
} elseif ($script:Option -ne 'RemoveManifest') {
|
|
$_menu = @{
|
|
Prompt = "Have you tested your manifest locally with 'winget install --manifest <path>'?"
|
|
Entries = @('[Y] Yes'; '*[N] No')
|
|
HelpText = 'You did not test your Manifest in Windows Sandbox previously.'
|
|
HelpTextColor = 'Red'
|
|
DefaultString = 'N'
|
|
}
|
|
} else {
|
|
$_showMenu = $false
|
|
}
|
|
}
|
|
|
|
'*schema*' {
|
|
if ($script:Option -ne 'RemoveManifest') {
|
|
$_Match = ($_line | Select-String -Pattern 'https://+.+(?=\))').Matches.Value
|
|
$_menu = @{
|
|
Prompt = $_line.TrimStart('- [ ]') -replace '\[|\]|\(.+\)', ''
|
|
Entries = @('[Y] Yes'; '*[N] No')
|
|
HelpText = "Reference Link: $_Match"
|
|
HelpTextColor = ''
|
|
DefaultString = 'N'
|
|
}
|
|
} else {
|
|
$_showMenu = $false
|
|
}
|
|
}
|
|
|
|
'*only modifies one*' {
|
|
$PrBodyContent = $PrBodyContent.Replace($_line, $_line.Replace('[ ]', '[x]'))
|
|
$_showMenu = $false
|
|
}
|
|
|
|
'*linked issue*' {
|
|
# Linked issues is handled as a separate prompt below so that the issue numbers can be gathered
|
|
$_showMenu = $false
|
|
}
|
|
|
|
Default {
|
|
$_menu = @{
|
|
Prompt = $_line.TrimStart('- [ ]')
|
|
Entries = @('[Y] Yes'; '*[N] No')
|
|
HelpText = ''
|
|
HelpTextColor = ''
|
|
DefaultString = 'N'
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($_showMenu) {
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor']) {
|
|
'Y' { $PrBodyContent = $PrBodyContent.Replace($_line, $_line.Replace('[ ]', '[x]')) }
|
|
default { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Request user to enter if there were any issues resolved by the PR
|
|
$_menu = @{
|
|
entries = @('[Y] Yes'; '*[N] No')
|
|
Prompt = 'Does this pull request resolve any issues?'
|
|
DefaultString = 'N'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'Y' {
|
|
$_line = ($PrBodyContent | Select-String 'linked issue').Line
|
|
if ($_line) { $PrBodyContent = $PrBodyContent.Replace($_line, $_line.Replace('[ ]', '[x]')) }
|
|
|
|
# If there were issues resolved by the PR, request user to enter them
|
|
Write-Host
|
|
Write-Host "Enter issue number. For example`: 21983, 43509"
|
|
$ResolvedIssues = Read-Host -Prompt 'Resolved Issues' | UniqueItems
|
|
$PrBodyContent += @('')
|
|
|
|
# Validate each of the issues entered by checking the URL to ensure it returns a 200 status code
|
|
Foreach ($i in ($ResolvedIssues.Split(',').Trim())) {
|
|
if ($i.Contains('#')) {
|
|
$_UrlParameters = $i.Split('#')
|
|
switch ($_UrlParameters.Count) {
|
|
2 {
|
|
if ([string]::IsNullOrWhiteSpace($_urlParameters[0])) {
|
|
$_checkedURL = "https://github.com/microsoft/winget-pkgs/issues/$($_urlParameters[1])"
|
|
} else {
|
|
$_checkedURL = "https://github.com/$($_urlParameters[0])/issues/$($_urlParameters[1])"
|
|
}
|
|
}
|
|
default {
|
|
Write-Host -ForegroundColor 'Red' "Invalid Issue: $i"
|
|
continue
|
|
}
|
|
}
|
|
$_responseCode = Test-Url $_checkedURL
|
|
if ($_responseCode -ne 200) {
|
|
Write-Host -ForegroundColor 'Red' "Invalid Issue: $i"
|
|
continue
|
|
}
|
|
$PrBodyContent += @("Resolves $i")
|
|
} else {
|
|
$_checkedURL = "https://github.com/microsoft/winget-pkgs/issues/$i"
|
|
$_responseCode = Test-Url $_checkedURL
|
|
if ($_responseCode -ne 200) {
|
|
Write-Host -ForegroundColor 'Red' "Invalid Issue: $i"
|
|
continue
|
|
}
|
|
$PrBodyContent += @("* Resolves #$i")
|
|
}
|
|
}
|
|
}
|
|
default { Write-Host }
|
|
}
|
|
|
|
# If we are removing a manifest, we need to include the reason
|
|
if ($CommitType -eq 'Remove') {
|
|
$PrBodyContent = @("## $($script:RemovalReason)"; '') + $PrBodyContent
|
|
}
|
|
|
|
# Write the PR using a temporary file
|
|
Set-Content -Path PrBodyFile -Value $PrBodyContent | Out-Null
|
|
gh pr create --body-file PrBodyFile -f
|
|
Remove-Item PrBodyFile
|
|
}
|
|
|
|
# Takes a comma separated list of values, converts it to an array object, and adds the result to a specified object-key
|
|
Function Add-YamlListParameter {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[PSCustomObject] $Object,
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[string] $Parameter,
|
|
[Parameter(Mandatory = $true, Position = 2)]
|
|
$Values
|
|
)
|
|
$_Values = @()
|
|
Foreach ($Value in $Values.Split(',').Trim()) {
|
|
$_Values += $Value
|
|
}
|
|
$Object[$Parameter] = $_Values
|
|
}
|
|
|
|
# Takes a single value and adds it to a specified object-key
|
|
Function Add-YamlParameter {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[PSCustomObject] $Object,
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[string] $Parameter,
|
|
[Parameter(Mandatory = $true, Position = 2)]
|
|
[string] $Value
|
|
)
|
|
$Object[$Parameter] = $Value
|
|
}
|
|
|
|
# Fetch the value of a manifest value regardless of which manifest file it exists in
|
|
Function Get-MultiManifestParameter {
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[string] $Parameter
|
|
)
|
|
$_vals = $($script:OldInstallerManifest[$Parameter] + $script:OldLocaleManifest[$Parameter] + $script:OldVersionManifest[$Parameter] | Where-Object { $_ })
|
|
return ($_vals -join ', ')
|
|
}
|
|
|
|
Function Get-DebugString {
|
|
$debug = ' $debug='
|
|
$debug += $(switch ($script:Option) {
|
|
'New' { 'NV' }
|
|
'QuickUpdateVersion' { 'QU' }
|
|
'EditMetadata' { 'MD' }
|
|
'NewLocale' { 'NL' }
|
|
'Auto' { 'AU' }
|
|
Default { 'XX' }
|
|
})
|
|
$debug += $(
|
|
switch ($script:SaveOption) {
|
|
'0' { 'S0.' }
|
|
'1' { 'S1.' }
|
|
'2' { 'S2.' }
|
|
Default { 'SU.' }
|
|
}
|
|
)
|
|
$debug += $(switch (([System.Environment]::NewLine).Length) {
|
|
1 { 'LF.' }
|
|
2 { 'CRLF.' }
|
|
Default { 'XX.' }
|
|
})
|
|
$debug += $PSVersionTable.PSVersion -Replace '\.', '-'
|
|
$debug += '.'
|
|
$debug += [System.Environment]::OSVersion.Platform
|
|
return $debug
|
|
}
|
|
|
|
Function Write-ManifestContent {
|
|
Param
|
|
(
|
|
[Parameter(Mandatory = $true, Position = 0)]
|
|
[string] $FilePath,
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[PSCustomObject] $YamlContent,
|
|
[Parameter(Mandatory = $true, Position = 2)]
|
|
[string] $Schema
|
|
)
|
|
[System.IO.File]::WriteAllLines($FilePath, @(
|
|
$ScriptHeader + $(Get-DebugString);
|
|
"# yaml-language-server: `$schema=$Schema";
|
|
'';
|
|
# This regex looks for lines with the special character ⍰ and comments them out
|
|
$(ConvertTo-Yaml $YamlContent).TrimEnd() -replace "(.*)\s+$([char]0x2370)", "# `$1"
|
|
), $Utf8NoBomEncoding)
|
|
|
|
Write-Host "Yaml file created: $FilePath"
|
|
}
|
|
|
|
# Take all the entered values and write the version manifest file
|
|
Function Write-VersionManifest {
|
|
# Create new empty manifest
|
|
[PSCustomObject]$VersionManifest = [ordered]@{}
|
|
|
|
# Write these values into the manifest
|
|
$_Singletons = [ordered]@{
|
|
'PackageIdentifier' = $PackageIdentifier
|
|
'PackageVersion' = $PackageVersion
|
|
'DefaultLocale' = if ($PackageLocale) { $PackageLocale } else { 'en-US' }
|
|
'ManifestType' = 'version'
|
|
'ManifestVersion' = $ManifestVersion
|
|
}
|
|
foreach ($_Item in $_Singletons.GetEnumerator()) {
|
|
If ($_Item.Value) { Add-YamlParameter -Object $VersionManifest -Parameter $_Item.Name -Value $_Item.Value }
|
|
}
|
|
$VersionManifest = Restore-YamlKeyOrder $VersionManifest $VersionProperties
|
|
|
|
# Create the folder for the file if it doesn't exist
|
|
New-Item -ItemType 'Directory' -Force -Path $AppFolder | Out-Null
|
|
$script:VersionManifestPath = Join-Path $AppFolder -ChildPath "$PackageIdentifier.yaml"
|
|
|
|
# Write the manifest to the file
|
|
Write-ManifestContent -FilePath $VersionManifestPath -YamlContent $VersionManifest -Schema $SchemaUrls.version
|
|
}
|
|
|
|
# Take all the entered values and write the installer manifest file
|
|
Function Write-InstallerManifest {
|
|
# If the old manifests exist, copy it so it can be updated in place, otherwise, create a new empty manifest
|
|
if ($script:OldManifestType -eq 'MultiManifest') {
|
|
$InstallerManifest = $script:OldInstallerManifest
|
|
}
|
|
if (!$InstallerManifest) { [PSCustomObject]$InstallerManifest = [ordered]@{} }
|
|
|
|
#Add the properties to the manifest
|
|
Add-YamlParameter -Object $InstallerManifest -Parameter 'PackageIdentifier' -Value $PackageIdentifier
|
|
Add-YamlParameter -Object $InstallerManifest -Parameter 'PackageVersion' -Value $PackageVersion
|
|
If ($MinimumOSVersion) {
|
|
$InstallerManifest['MinimumOSVersion'] = $MinimumOSVersion
|
|
} Else {
|
|
If ($InstallerManifest['MinimumOSVersion']) { $_InstallerManifest.Remove('MinimumOSVersion') }
|
|
}
|
|
|
|
$_ListSections = [ordered]@{
|
|
'FileExtensions' = $FileExtensions
|
|
'Protocols' = $Protocols
|
|
'Commands' = $Commands
|
|
'InstallerSuccessCodes' = $InstallerSuccessCodes
|
|
'InstallModes' = $InstallModes
|
|
}
|
|
foreach ($Section in $_ListSections.GetEnumerator()) {
|
|
If ($Section.Value) { Add-YamlListParameter -Object $InstallerManifest -Parameter $Section.Name -Values $Section.Value }
|
|
}
|
|
|
|
if ($Option -ne 'EditMetadata') {
|
|
$InstallerManifest['Installers'] = $script:Installers
|
|
} elseif ($script:OldInstallerManifest) {
|
|
$InstallerManifest['Installers'] = $script:OldInstallerManifest['Installers']
|
|
} else {
|
|
$InstallerManifest['Installers'] = $script:OldVersionManifest['Installers']
|
|
}
|
|
|
|
foreach ($_Installer in $InstallerManifest.Installers) {
|
|
if ($_Installer['ReleaseDate'] -and !$script:ReleaseDatePrompted -and !$Preserve) { $_Installer.Remove('ReleaseDate') }
|
|
elseif ($Preserve) {
|
|
try {
|
|
Get-Date([datetime]$($_Installer['ReleaseDate'])) -f 'yyyy-MM-dd' -OutVariable _ValidDate | Out-Null
|
|
if ($_ValidDate) { $_Installer['ReleaseDate'] = $_ValidDate | TrimString }
|
|
} catch {
|
|
# Release date isn't valid
|
|
$_Installer.Remove('ReleaseDate')
|
|
}
|
|
}
|
|
}
|
|
|
|
Add-YamlParameter -Object $InstallerManifest -Parameter 'ManifestType' -Value 'installer'
|
|
Add-YamlParameter -Object $InstallerManifest -Parameter 'ManifestVersion' -Value $ManifestVersion
|
|
If ($InstallerManifest['Dependencies']) {
|
|
$InstallerManifest['Dependencies'] = Restore-YamlKeyOrder $InstallerManifest['Dependencies'] $InstallerDependencyProperties -NoComments
|
|
}
|
|
# Move Installer Level Keys to Manifest Level
|
|
$_KeysToMove = $InstallerEntryProperties | Where-Object { $_ -in $InstallerProperties -and $_ -notin @('ProductCode', 'NestedInstallerFiles', 'NestedInstallerType') }
|
|
foreach ($_Key in $_KeysToMove) {
|
|
if ($_Key -in $InstallerManifest.Installers[0].Keys) {
|
|
# Handle the switches specially
|
|
if ($_Key -eq 'InstallerSwitches') {
|
|
# Go into each of the subkeys to see if they are the same
|
|
foreach ($_InstallerSwitchKey in $InstallerManifest.Installers[0].$_Key.Keys) {
|
|
$_AllAreSame = $true
|
|
$_FirstInstallerSwitchKeyValue = ConvertTo-Json($InstallerManifest.Installers[0].$_Key.$_InstallerSwitchKey)
|
|
foreach ($_Installer in $InstallerManifest.Installers) {
|
|
$_CurrentInstallerSwitchKeyValue = ConvertTo-Json($_Installer.$_Key.$_InstallerSwitchKey)
|
|
if (Test-String $_CurrentInstallerSwitchKeyValue -IsNull) { $_AllAreSame = $false }
|
|
else { $_AllAreSame = $_AllAreSame -and (@(Compare-Object $_CurrentInstallerSwitchKeyValue $_FirstInstallerSwitchKeyValue).Length -eq 0) }
|
|
}
|
|
if ($_AllAreSame) {
|
|
if ($_Key -notin $InstallerManifest.Keys) { $InstallerManifest[$_Key] = @{} }
|
|
$InstallerManifest.$_Key[$_InstallerSwitchKey] = $InstallerManifest.Installers[0].$_Key.$_InstallerSwitchKey
|
|
}
|
|
}
|
|
# Remove them from the individual installer switches if we moved them to the manifest level
|
|
if ($_Key -in $InstallerManifest.Keys) {
|
|
foreach ($_InstallerSwitchKey in $InstallerManifest.$_Key.Keys) {
|
|
foreach ($_Installer in $InstallerManifest.Installers) {
|
|
if ($_Installer.Keys -contains $_Key) {
|
|
if ($_Installer.$_Key.Keys -contains $_InstallerSwitchKey) { $_Installer.$_Key.Remove($_InstallerSwitchKey) }
|
|
if (@($_Installer.$_Key.Keys).Count -eq 0) { $_Installer.Remove($_Key) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
# Check if all installers are the same
|
|
$_AllAreSame = $true
|
|
$_FirstInstallerKeyValue = ConvertTo-Json($InstallerManifest.Installers[0].$_Key)
|
|
foreach ($_Installer in $InstallerManifest.Installers) {
|
|
$_CurrentInstallerKeyValue = ConvertTo-Json($_Installer.$_Key)
|
|
if (Test-String $_CurrentInstallerKeyValue -IsNull) { $_AllAreSame = $false }
|
|
else { $_AllAreSame = $_AllAreSame -and (@(Compare-Object $_CurrentInstallerKeyValue $_FirstInstallerKeyValue).Length -eq 0) }
|
|
}
|
|
# If all installers are the same move the key to the manifest level
|
|
if ($_AllAreSame) {
|
|
$InstallerManifest[$_Key] = $InstallerManifest.Installers[0].$_Key
|
|
foreach ($_Installer in $InstallerManifest.Installers) {
|
|
$_Installer.Remove($_Key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($InstallerManifest.Keys -contains 'InstallerSwitches') { $InstallerManifest['InstallerSwitches'] = Restore-YamlKeyOrder $InstallerManifest.InstallerSwitches $InstallerSwitchProperties -NoComments }
|
|
foreach ($_Installer in $InstallerManifest.Installers) {
|
|
if ($_Installer.Keys -contains 'InstallerSwitches') { $_Installer['InstallerSwitches'] = Restore-YamlKeyOrder $_Installer.InstallerSwitches $InstallerSwitchProperties -NoComments }
|
|
}
|
|
|
|
# Clean up the existing files just in case
|
|
if ($InstallerManifest['Commands']) { $InstallerManifest['Commands'] = @($InstallerManifest['Commands'] | NoWhitespace | UniqueItems | Sort-Object) }
|
|
if ($InstallerManifest['Protocols']) { $InstallerManifest['Protocols'] = @($InstallerManifest['Protocols'] | ToLower | NoWhitespace | UniqueItems | Sort-Object) }
|
|
if ($InstallerManifest['FileExtensions']) { $InstallerManifest['FileExtensions'] = @($InstallerManifest['FileExtensions'] | ToLower | NoWhitespace | UniqueItems | Sort-Object) }
|
|
|
|
$InstallerManifest = Restore-YamlKeyOrder $InstallerManifest $InstallerProperties -NoComments
|
|
|
|
# Create the folder for the file if it doesn't exist
|
|
New-Item -ItemType 'Directory' -Force -Path $AppFolder | Out-Null
|
|
$script:InstallerManifestPath = Join-Path $AppFolder -ChildPath "$PackageIdentifier.installer.yaml"
|
|
|
|
# Write the manifest to the file
|
|
Write-ManifestContent -FilePath $InstallerManifestPath -YamlContent $InstallerManifest -Schema $SchemaUrls.installer
|
|
}
|
|
|
|
# Take all the entered values and write the locale manifest file
|
|
Function Write-LocaleManifest {
|
|
# If the old manifests exist, copy it so it can be updated in place, otherwise, create a new empty manifest
|
|
if ($script:OldManifestType -eq 'MultiManifest') {
|
|
$LocaleManifest = $script:OldLocaleManifest
|
|
}
|
|
if (!$LocaleManifest) { [PSCustomObject]$LocaleManifest = [ordered]@{} }
|
|
|
|
# Add the properties to the manifest
|
|
$_Singletons = [ordered]@{
|
|
'PackageIdentifier' = $PackageIdentifier
|
|
'PackageVersion' = $PackageVersion
|
|
'PackageLocale' = $PackageLocale
|
|
'Publisher' = $Publisher
|
|
'PublisherUrl' = $PublisherUrl
|
|
'PublisherSupportUrl' = $PublisherSupportUrl
|
|
'PrivacyUrl' = $PrivacyUrl
|
|
'Author' = $Author
|
|
'PackageName' = $PackageName
|
|
'PackageUrl' = $PackageUrl
|
|
'License' = $License
|
|
'LicenseUrl' = $LicenseUrl
|
|
'Copyright' = $Copyright
|
|
'CopyrightUrl' = $CopyrightUrl
|
|
'ShortDescription' = $ShortDescription
|
|
'Description' = $Description
|
|
'ReleaseNotes' = $ReleaseNotes
|
|
'ReleaseNotesUrl' = $ReleaseNotesUrl
|
|
}
|
|
foreach ($_Item in $_Singletons.GetEnumerator()) {
|
|
If ($_Item.Value) { Add-YamlParameter -Object $LocaleManifest -Parameter $_Item.Name -Value $_Item.Value }
|
|
}
|
|
|
|
If ($Tags) { Add-YamlListParameter -Object $LocaleManifest -Parameter 'Tags' -Values $Tags }
|
|
If (!$LocaleManifest.ManifestType) { $LocaleManifest['ManifestType'] = 'defaultLocale' }
|
|
If ($Moniker -and $($LocaleManifest.ManifestType -eq 'defaultLocale')) { Add-YamlParameter -Object $LocaleManifest -Parameter 'Moniker' -Value $Moniker }
|
|
Add-YamlParameter -Object $LocaleManifest -Parameter 'ManifestVersion' -Value $ManifestVersion
|
|
|
|
# Clean up the existing files just in case
|
|
if ($LocaleManifest['Tags']) { $LocaleManifest['Tags'] = @($LocaleManifest['Tags'] | ToLower | NoWhitespace | UniqueItems | Sort-Object) }
|
|
if ($LocaleManifest['Moniker']) { $LocaleManifest['Moniker'] = $LocaleManifest['Moniker'] | ToLower | NoWhitespace }
|
|
|
|
# Clean up the volatile fields
|
|
if ($LocaleManifest['ReleaseNotes'] -and (Test-String $script:ReleaseNotes -IsNull) -and !$Preserve) { $LocaleManifest.Remove('ReleaseNotes') }
|
|
if ($LocaleManifest['ReleaseNotesUrl'] -and (Test-String $script:ReleaseNotesUrl -IsNull) -and !$Preserve) { $LocaleManifest.Remove('ReleaseNotesUrl') }
|
|
|
|
$LocaleManifest = Restore-YamlKeyOrder $LocaleManifest $LocaleProperties
|
|
|
|
# Set the appropriate langage server depending on if it is a default locale file or generic locale file
|
|
if ($LocaleManifest.ManifestType -eq 'defaultLocale') { $yamlServer = $SchemaUrls.defaultLocale } else { $yamlServer = $SchemaUrls.locale }
|
|
|
|
# Create the folder for the file if it doesn't exist
|
|
New-Item -ItemType 'Directory' -Force -Path $AppFolder | Out-Null
|
|
$script:LocaleManifestPath = Join-Path $AppFolder -ChildPath "$PackageIdentifier.locale.$PackageLocale.yaml"
|
|
|
|
# Write the manifest to the file
|
|
Write-ManifestContent -FilePath $LocaleManifestPath -YamlContent $LocaleManifest -Schema $yamlServer
|
|
|
|
# Copy over all locale files from previous version that aren't the same
|
|
if ($OldManifests) {
|
|
ForEach ($DifLocale in $OldManifests) {
|
|
if ($DifLocale.Name -notin @("$PackageIdentifier.yaml", "$PackageIdentifier.installer.yaml", "$PackageIdentifier.locale.$PackageLocale.yaml")) {
|
|
if (!(Test-Path $AppFolder)) { New-Item -ItemType 'Directory' -Force -Path $AppFolder | Out-Null }
|
|
$script:OldLocaleManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $DifLocale.FullName -Encoding UTF8) -join "`n") -Ordered
|
|
$script:OldLocaleManifest['PackageVersion'] = $PackageVersion
|
|
if ($script:OldLocaleManifest.Keys -contains 'Moniker') { $script:OldLocaleManifest.Remove('Moniker') }
|
|
$script:OldLocaleManifest['ManifestVersion'] = $ManifestVersion
|
|
# Clean up the existing files just in case
|
|
if ($script:OldLocaleManifest['Tags']) { $script:OldLocaleManifest['Tags'] = @($script:OldLocaleManifest['Tags'] | ToLower | NoWhitespace | UniqueItems | Sort-Object) }
|
|
|
|
# Clean up the volatile fields
|
|
if ($OldLocaleManifest['ReleaseNotes'] -and (Test-String $script:ReleaseNotes -IsNull) -and !$Preserve) { $OldLocaleManifest.Remove('ReleaseNotes') }
|
|
if ($OldLocaleManifest['ReleaseNotesUrl'] -and (Test-String $script:ReleaseNotesUrl -IsNull) -and !$Preserve) { $OldLocaleManifest.Remove('ReleaseNotesUrl') }
|
|
|
|
$script:OldLocaleManifest = Restore-YamlKeyOrder $script:OldLocaleManifest $LocaleProperties
|
|
Write-ManifestContent -FilePath $(Join-Path $AppFolder -ChildPath $DifLocale.Name) -YamlContent $OldLocaleManifest -Schema $SchemaUrls.locale
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Remove-ManifestVersion {
|
|
[CmdletBinding(SupportsShouldProcess)]
|
|
Param(
|
|
[Parameter(Mandatory = $true, Position = 1)]
|
|
[string] $PathToVersion
|
|
)
|
|
|
|
# Remove the manifest, and then any parent folders so long as the parent folders are empty
|
|
do {
|
|
Remove-Item -Path $PathToVersion -Recurse -Force
|
|
$PathToVersion = Split-Path $PathToVersion
|
|
} while (@(Get-ChildItem $PathToVersion).Count -eq 0)
|
|
return $PathToVersion
|
|
}
|
|
|
|
## START OF MAIN SCRIPT ##
|
|
|
|
# Set the root folder where new manifests should be created
|
|
if (Test-Path -Path "$PSScriptRoot\..\manifests") {
|
|
$ManifestsFolder = (Resolve-Path "$PSScriptRoot\..\manifests").Path
|
|
} else {
|
|
$ManifestsFolder = (Resolve-Path '.\').Path
|
|
}
|
|
|
|
# Initialize the return value to be a success
|
|
$script:_returnValue = [ReturnValue]::new(200)
|
|
|
|
$script:UsingAdvancedOption = ($ScriptSettings.EnableDeveloperOptions -eq 'true') -and ($AutoUpgrade)
|
|
|
|
if (!$script:UsingAdvancedOption) {
|
|
# Request the user to choose an operation mode
|
|
Clear-Host
|
|
if ($Mode -in 1..6) {
|
|
$UserChoice = $Mode
|
|
} else {
|
|
Write-Host @"
|
|
${vtForegroundYellow} Select Mode:
|
|
${vtForegroundCyan}[${vtForegroundWhite}1${vtForegroundCyan}] New Manifest or Package Version
|
|
${vtForegroundCyan}[${vtForegroundWhite}2${vtForegroundCyan}] Quick Update Package Version
|
|
${vtForegroundCyan}[${vtForegroundWhite}3${vtForegroundCyan}] Update Package Metadata ${vtForegroundGreen}(Note: Must be used only when previous version's metadata is complete.)
|
|
${vtForegroundCyan}[${vtForegroundWhite}4${vtForegroundCyan}] New Locale
|
|
${vtForegroundCyan}[${vtForegroundWhite}5${vtForegroundCyan}] Remove a manifest
|
|
${vtForegroundCyan}[${vtForegroundWhite}6${vtForegroundCyan}] Move package to a new identifier
|
|
${vtForegroundCyan}[${vtForegroundWhite}Q${vtForegroundCyan}] ${vtForegroundRed}Any key to quit
|
|
${vtForegroundDefault}
|
|
"@
|
|
Write-Host "Selection: " -NoNewLine
|
|
|
|
# Listen for keypress and set operation mode based on keypress
|
|
$Keys = @{
|
|
[ConsoleKey]::D1 = '1';
|
|
[ConsoleKey]::D2 = '2';
|
|
[ConsoleKey]::D3 = '3';
|
|
[ConsoleKey]::D4 = '4';
|
|
[ConsoleKey]::D5 = '5';
|
|
[ConsoleKey]::D6 = '6';
|
|
[ConsoleKey]::NumPad1 = '1';
|
|
[ConsoleKey]::NumPad2 = '2';
|
|
[ConsoleKey]::NumPad3 = '3';
|
|
[ConsoleKey]::NumPad4 = '4';
|
|
[ConsoleKey]::NumPad5 = '5';
|
|
[ConsoleKey]::NumPad6 = '6';
|
|
}
|
|
do {
|
|
$keyInfo = [Console]::ReadKey($false)
|
|
} until ($keyInfo.Key)
|
|
|
|
$UserChoice = $Keys[$keyInfo.Key]
|
|
}
|
|
switch ($UserChoice) {
|
|
'1' { $script:Option = 'New' }
|
|
'2' { $script:Option = 'QuickUpdateVersion' }
|
|
'3' { $script:Option = 'EditMetadata' }
|
|
'4' { $script:Option = 'NewLocale' }
|
|
'5' { $script:Option = 'RemoveManifest' }
|
|
'6' { $script:Option = 'MovePackageIdentifier' }
|
|
default {
|
|
Write-Host
|
|
[Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
|
|
exit
|
|
}
|
|
}
|
|
} else {
|
|
if ($AutoUpgrade) { $script:Option = 'Auto' }
|
|
}
|
|
|
|
# Confirm the user understands the implications of using the quick update mode
|
|
if (($script:Option -eq 'QuickUpdateVersion') -and ($ScriptSettings.SuppressQuickUpdateWarning -ne 'true')) {
|
|
$_menu = @{
|
|
entries = @('[Y] Continue with Quick Update'; '[N] Use Full Update Experience'; '*[Q] Exit Script')
|
|
Prompt = 'Quick Updates only allow for changes to the existing Installer URLs, Sha256 Values, and Product Codes. Are you sure you want to continue?'
|
|
HelpText = 'This mode should be used with caution. If you are not 100% certain this is correct, please use Option 1 to go through the full update experience'
|
|
HelpTextColor = 'Red'
|
|
DefaultString = 'Q'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor']) {
|
|
'Y' { Write-Host -ForegroundColor DarkYellow -Object "`n`nContinuing with Quick Update" }
|
|
'N' { $script:Option = 'New'; Write-Host -ForegroundColor DarkYellow -Object "`n`nSwitched to Full Update Experience" }
|
|
default {
|
|
Invoke-CleanExit
|
|
}
|
|
}
|
|
}
|
|
Write-Host
|
|
|
|
# Confirm the user understands the implications of moving package
|
|
if (($script:Option -eq 'MovePackageIdentifier')) {
|
|
$_menu = @{
|
|
entries = @('[Y] Continue moving package'; '*[Q] Exit Script')
|
|
Prompt = 'Packages should only be moved between identifiers when necessary. Are you sure you want to continue?'
|
|
HelpText = 'This mode should be used with caution. If you are not 100% certain what you are doing, please open an issue at GitHub'
|
|
HelpTextColor = 'Red'
|
|
DefaultString = 'Q'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor']) {
|
|
'Y' {
|
|
# To move a package doesn't require a package version like the other functions of YamlCreate. Therefore, to avoid requesting the information twice
|
|
# the entirety of the Move packages script happens here. This will then exit the script directly.
|
|
Write-Host; Write-Host
|
|
|
|
# Update the ref for upstream master and switch to it to ensure the latest manifest information
|
|
git fetch upstream master --quiet
|
|
git switch -d upstream/master -q
|
|
|
|
# Request the current identifier and validate that it exists
|
|
Write-Host -ForegroundColor 'Green' -Object 'What is the current package identifier?' -NoNewline
|
|
do {
|
|
$OldPackageIdentifier = Read-PackageIdentifier -PackageIdentifier $null
|
|
# Set the folder for the specific package
|
|
$FromAppFolder = Join-Path $ManifestsFolder -ChildPath $OldPackageIdentifier.ToLower().Chars(0) | Join-Path -ChildPath $OldPackageIdentifier.Replace('.', $([IO.Path]::DirectorySeparatorChar))
|
|
if (!(Test-Path -Path "$FromAppFolder")) {
|
|
Write-Host -ForegroundColor 'Red' -Object "No manifests found for $OldPackageIdentifier"
|
|
} else {
|
|
$manifestsExist = $true
|
|
Write-Host
|
|
}
|
|
} while (!$manifestsExist)
|
|
|
|
# Request the new identifier
|
|
Write-Host -ForegroundColor 'Green' -Object 'What is the new package identifier?' -NoNewline
|
|
$NewPackageIdentifier = Read-PackageIdentifier -PackageIdentifier $null
|
|
$ToAppFolder = Join-Path $ManifestsFolder -ChildPath $NewPackageIdentifier.ToLower().Chars(0) | Join-Path -ChildPath $NewPackageIdentifier.Replace('.', [IO.Path]::DirectorySeparatorChar)
|
|
Write-Host
|
|
|
|
# Request the new moniker, in case the moniker needs to be updated
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Moniker (friendly name/alias). For example: vscode'
|
|
if (Test-String -not $NewMoniker -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $NewMoniker" }
|
|
$NewMoniker = Read-Host -Prompt 'Moniker' | ToLower | TrimString | NoWhitespace
|
|
if (Test-String $NewMoniker -MaxLength $Patterns.MonikerMaxLength -AllowNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.MonikerMaxLength)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Get a list of the versions to move
|
|
$VersionsToMove = @(Get-ChildItem -Path $FromAppFolder | Where-Object { @(Get-ChildItem -Directory -Path $_.FullName).Count -eq 0 }).Name
|
|
|
|
# Create an array for logging all the branches that were created
|
|
$BranchesCreated = @()
|
|
|
|
foreach ($Version in $VersionsToMove) {
|
|
Write-Host
|
|
Write-Host -ForegroundColor Yellow -Object "Moving version $Version [$(1+$BranchesCreated.Count/2)/$($VersionsToMove.Count)]"
|
|
# Copy the manifests to the new directory
|
|
$SourceFolder = Join-Path -Path $FromAppFolder -ChildPath $Version
|
|
$DestinationFolder = Join-Path -Path $ToAppFolder -ChildPath $Version
|
|
Copy-Item -Path $SourceFolder -Destination $DestinationFolder -Recurse -Force
|
|
# Rename the files
|
|
Get-ChildItem -Path $DestinationFolder -Filter "*$OldPackageIdentifier*" -Recurse | ForEach-Object { Rename-Item -Path $_.FullName -NewName $($_.Name -replace [regex]::Escape($OldPackageIdentifier), "$NewPackageIdentifier") }
|
|
# Update PackageIdentifier in all files
|
|
Get-ChildItem -Path $DestinationFolder -Filter "*$NewPackageIdentifier*" -Recurse | ForEach-Object { [System.IO.File]::WriteAllLines($_.FullName, $((Get-Content -Path $_.FullName -Raw).TrimEnd() -replace [regex]::Escape($OldPackageIdentifier), "$NewPackageIdentifier"), $Utf8NoBomEncoding) }
|
|
# Update Moniker in all files
|
|
if (Test-String $NewMoniker -Not -IsNull) {
|
|
Get-ChildItem -Path $DestinationFolder -Filter "*$NewPackageIdentifier*" -Recurse | ForEach-Object { [System.IO.File]::WriteAllLines($_.FullName, $((Get-Content -Path $_.FullName -Raw).TrimEnd() -replace 'Moniker:.*', "Moniker: $NewMoniker"), $Utf8NoBomEncoding) }
|
|
}
|
|
|
|
# Create and push to a new branch
|
|
git switch -d upstream/master -q
|
|
git add $DestinationFolder
|
|
git commit -m "Move $OldPackageIdentifier $Version to $NewPackageIdentifier $Version" --quiet
|
|
$BranchName = "Move-$OldPackageIdentifier-v$Version-$RunHash"
|
|
git switch -c "$BranchName" --quiet
|
|
git push --set-upstream origin "$BranchName" --quiet
|
|
$BranchesCreated += $BranchName
|
|
if ($ScriptSettings.AutoSubmitPRs -eq 'Always') {
|
|
gh pr create -f
|
|
}
|
|
|
|
# Switch back to the master branch
|
|
git switch -d upstream/master -q
|
|
# Remove the manifest for the old version
|
|
# Create and push to a new branch
|
|
git add $(Remove-ManifestVersion $SourceFolder)
|
|
git commit -m "Remove $OldPackageIdentifier $Version to $NewPackageIdentifier $Version" --quiet
|
|
$BranchName = "Remove-$OldPackageIdentifier-v$Version-$RunHash"
|
|
git switch -c "$BranchName" --quiet
|
|
git push --set-upstream origin "$BranchName" --quiet
|
|
$BranchesCreated += $BranchName
|
|
if ($ScriptSettings.AutoSubmitPRs -eq 'Always') {
|
|
gh pr create -f
|
|
}
|
|
}
|
|
|
|
}
|
|
default {
|
|
Out-Null # Intentionally do nothing here
|
|
}
|
|
}
|
|
if ($ScriptSettings.AutoSubmitPRs -notin @('Always', 'Never') -and $BranchesCreated.Count -gt 0) {
|
|
$_menu = @{
|
|
entries = @('[Y] Yes'; '*[N] No')
|
|
Prompt = "Do you want to submit all $($BranchesCreated.Count) PRs now?"
|
|
HelpText = "If you choose 'No', the pull requests will need to be manually created"
|
|
DefaultString = 'N'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText']) {
|
|
'Y' {
|
|
foreach ($Branch in $BranchesCreated) {
|
|
git switch $Branch --quiet
|
|
gh pr create -f
|
|
}
|
|
}
|
|
default { Out-Null }
|
|
}
|
|
}
|
|
Invoke-CleanExit
|
|
}
|
|
|
|
# Request Package Identifier and Validate
|
|
$script:PackageIdentifier = Read-PackageIdentifier $script:PackageIdentifier
|
|
|
|
# Request Package Version and Validate
|
|
do {
|
|
if ((Test-String $PackageVersion -IsNull) -or ($script:_returnValue.StatusCode -ne [ReturnValue]::Success().StatusCode)) {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the version. for example: 1.33.7'
|
|
$script:PackageVersion = Read-Host -Prompt 'Version' | TrimString
|
|
}
|
|
if (Test-String $PackageVersion -MaxLength $Patterns.VersionMaxLength -MatchPattern $Patterns.PackageVersion -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
if (Test-String -not $PackageVersion -MaxLength $Patterns.VersionMaxLength -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.VersionMaxLength)
|
|
} elseif (Test-String -not $PackageVersion -MatchPattern $Patterns.PackageVersion) {
|
|
$script:_returnValue = [ReturnValue]::PatternError()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::GenericError()
|
|
}
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
# Check the api for open PR's
|
|
# This is unauthenticated because the call-rate per minute is assumed to be low
|
|
if ($ScriptSettings.ContinueWithExistingPRs -ne 'always' -and $script:Option -ne 'RemoveManifest' -and !$SkipPRCheck) {
|
|
$PRApiResponse = @(Invoke-WebRequest "https://api.github.com/search/issues?q=repo%3Amicrosoft%2Fwinget-pkgs%20is%3Apr%20$($PackageIdentifier -replace '\.', '%2F'))%2F$PackageVersion%20in%3Apath&per_page=1" -UseBasicParsing -ErrorAction SilentlyContinue | ConvertFrom-Json)[0]
|
|
# If there was a PR found, get the URL and title
|
|
if ($PRApiResponse.total_count -gt 0) {
|
|
$_PRUrl = $PRApiResponse.items.html_url
|
|
$_PRTitle = $PRApiResponse.items.title
|
|
if ($ScriptSettings.ContinueWithExistingPRs -eq 'never') {
|
|
Write-Host -ForegroundColor Red "Existing PR Found - $_PRUrl"
|
|
Invoke-CleanExit
|
|
}
|
|
$_menu = @{
|
|
entries = @('[Y] Yes'; '*[N] No')
|
|
Prompt = 'There may already be a PR for this change. Would you like to continue anyways?'
|
|
DefaultString = 'N'
|
|
HelpText = "$_PRTitle - $_PRUrl"
|
|
HelpTextColor = 'Blue'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor'] ) {
|
|
'Y' { Write-Host }
|
|
default {
|
|
Invoke-CleanExit
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Set the folder for the specific package and version
|
|
$script:AppFolder = Join-Path $ManifestsFolder -ChildPath $PackageIdentifier.ToLower().Chars(0) | Join-Path -ChildPath $PackageIdentifierFolder | Join-Path -ChildPath $PackageVersion
|
|
|
|
# If the user selected `NewLocale` or `EditMetadata` the version *MUST* already exist in the folder structure
|
|
if ($script:Option -in @('NewLocale'; 'EditMetadata'; 'RemoveManifest')) {
|
|
# Try getting the old manifests from the specified folder
|
|
if (Test-Path -Path "$AppFolder\..\$PackageVersion") {
|
|
$script:OldManifests = Get-ChildItem -Path "$AppFolder\..\$PackageVersion"
|
|
$LastVersion = $PackageVersion
|
|
}
|
|
# If the old manifests could not be found, request a new version
|
|
while (-not ($OldManifests.Name -like "$PackageIdentifier*.yaml")) {
|
|
Write-Host
|
|
Write-Host -ForegroundColor 'Red' -Object 'Could not find required manifests, input a version containing required manifests or "exit" to cancel'
|
|
$PromptVersion = Read-Host -Prompt 'Version' | TrimString
|
|
if ($PromptVersion -eq 'exit') {
|
|
Invoke-CleanExit
|
|
}
|
|
if (Test-Path -Path "$AppFolder\..\$PromptVersion") {
|
|
$script:OldManifests = Get-ChildItem -Path "$AppFolder\..\$PromptVersion"
|
|
}
|
|
# If a new version is entered, we need to be sure to update the folder for writing manifests
|
|
$LastVersion = $PromptVersion
|
|
$script:AppFolder = Join-Path (Split-Path $AppFolder) -ChildPath $LastVersion
|
|
$script:PackageVersion = $LastVersion
|
|
}
|
|
}
|
|
|
|
# If the user selected `QuickUpdateVersion`, the old manifests must exist
|
|
# If the user selected `New`, the old manifest type is specified as none
|
|
if (-not (Test-Path -Path "$AppFolder\..")) {
|
|
if ($script:Option -in @('QuickUpdateVersion', 'Auto')) {
|
|
Write-Host -ForegroundColor Red 'This option requires manifest of previous version of the package. If you want to create a new package, please select Option 1.'
|
|
Invoke-CleanExit
|
|
}
|
|
$script:OldManifestType = 'None'
|
|
}
|
|
|
|
# Try getting the last version of the package and the old manifests to be updated
|
|
if (!$LastVersion) {
|
|
try {
|
|
$script:LastVersion = Split-Path (Split-Path (Get-ChildItem -Path "$AppFolder\..\" -Recurse -Depth 1 -File -Filter '*.yaml' -ErrorAction SilentlyContinue).FullName ) -Leaf | Sort-Object $ToNatural | Select-Object -Last 1
|
|
$script:ExistingVersions = Split-Path (Split-Path (Get-ChildItem -Path "$AppFolder\..\" -Recurse -Depth 1 -File -Filter '*.yaml' -ErrorAction SilentlyContinue).FullName ) -Leaf | Sort-Object $ToNatural | Select-Object -Unique
|
|
if ($script:Option -eq 'Auto' -and $PackageVersion -in $script:ExistingVersions) { $LastVersion = $PackageVersion }
|
|
Write-Host -ForegroundColor 'DarkYellow' -Object "Found Existing Version: $LastVersion"
|
|
$script:OldManifests = Get-ChildItem -Path "$AppFolder\..\$LastVersion"
|
|
} catch {
|
|
# Take no action here, we just want to catch the exceptions as a precaution
|
|
Out-Null
|
|
}
|
|
}
|
|
|
|
# If the old manifests exist, find the default locale
|
|
if ($OldManifests.Name -match "$([Regex]::Escape($PackageIdentifier))\.locale\..*\.yaml") {
|
|
$_LocaleManifests = $OldManifests | Where-Object { $_.Name -match "$([Regex]::Escape($PackageIdentifier))\.locale\..*\.yaml" }
|
|
foreach ($_Manifest in $_LocaleManifests) {
|
|
$_ManifestContent = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $($_Manifest.FullName) -Encoding UTF8) -join "`n") -Ordered
|
|
if ($_ManifestContent.ManifestType -eq 'defaultLocale') { $PackageLocale = $_ManifestContent.PackageLocale }
|
|
}
|
|
}
|
|
|
|
# If the old manifests exist, read their information into variables
|
|
# Also ensure additional requirements are met for creating or updating files
|
|
if ($OldManifests.Name -eq "$PackageIdentifier.installer.yaml" -and $OldManifests.Name -eq "$PackageIdentifier.locale.$PackageLocale.yaml" -and $OldManifests.Name -eq "$PackageIdentifier.yaml") {
|
|
$script:OldManifestType = 'MultiManifest'
|
|
$script:OldInstallerManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $(Resolve-Path "$AppFolder\..\$LastVersion\$PackageIdentifier.installer.yaml") -Encoding UTF8) -join "`n") -Ordered
|
|
# Move Manifest Level Keys to installer Level
|
|
$_KeysToMove = $InstallerEntryProperties | Where-Object { $_ -in $InstallerProperties }
|
|
foreach ($_Key in $_KeysToMove) {
|
|
if ($_Key -in $script:OldInstallerManifest.Keys) {
|
|
# Handle Installer switches separately
|
|
if ($_Key -eq 'InstallerSwitches') {
|
|
$_SwitchKeysToMove = $script:OldInstallerManifest.$_Key.Keys
|
|
foreach ($_SwitchKey in $_SwitchKeysToMove) {
|
|
# If the InstallerSwitches key doesn't exist, we need to create it, otherwise, preserve switches that were already there
|
|
foreach ($_Installer in $script:OldInstallerManifest['Installers']) {
|
|
if ('InstallerSwitches' -notin $_Installer.Keys) { $_Installer['InstallerSwitches'] = @{} }
|
|
$_Installer.InstallerSwitches["$_SwitchKey"] = $script:OldInstallerManifest.$_Key.$_SwitchKey
|
|
}
|
|
}
|
|
$script:OldInstallerManifest.Remove($_Key)
|
|
continue
|
|
} else {
|
|
foreach ($_Installer in $script:OldInstallerManifest['Installers']) {
|
|
if ($_Key -eq 'InstallModes') { $script:InstallModes = [string]$script:OldInstallerManifest.$_Key }
|
|
if ($_Key -notin $_Installer.Keys) {
|
|
$_Installer[$_Key] = $script:OldInstallerManifest.$_Key
|
|
}
|
|
}
|
|
}
|
|
New-Variable -Name $_Key -Value $($script:OldInstallerManifest.$_Key -join ', ') -Scope Script -Force
|
|
$script:OldInstallerManifest.Remove($_Key)
|
|
}
|
|
}
|
|
$script:OldLocaleManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $(Resolve-Path "$AppFolder\..\$LastVersion\$PackageIdentifier.locale.$PackageLocale.yaml") -Encoding UTF8) -join "`n") -Ordered
|
|
$script:OldVersionManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $(Resolve-Path "$AppFolder\..\$LastVersion\$PackageIdentifier.yaml") -Encoding UTF8) -join "`n") -Ordered
|
|
} elseif ($OldManifests.Name -eq "$PackageIdentifier.yaml") {
|
|
if ($script:Option -eq 'NewLocale') { throw [ManifestException]::new('MultiManifest Required') }
|
|
$script:OldManifestType = 'MultiManifest'
|
|
$script:OldSingletonManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $(Resolve-Path "$AppFolder\..\$LastVersion\$PackageIdentifier.yaml") -Encoding UTF8) -join "`n") -Ordered
|
|
$PackageLocale = $script:OldSingletonManifest.PackageLocale
|
|
# Create new empty manifests
|
|
$script:OldInstallerManifest = [ordered]@{}
|
|
$script:OldLocaleManifest = [ordered]@{}
|
|
$script:OldVersionManifest = [ordered]@{}
|
|
# Parse version keys to version manifest
|
|
foreach ($_Key in $($OldSingletonManifest.Keys | Where-Object { $_ -in $VersionProperties })) {
|
|
$script:OldVersionManifest[$_Key] = $script:OldSingletonManifest.$_Key
|
|
}
|
|
$script:OldVersionManifest['ManifestType'] = 'version'
|
|
#Parse locale keys to locale manifest
|
|
foreach ($_Key in $($OldSingletonManifest.Keys | Where-Object { $_ -in $LocaleProperties })) {
|
|
$script:OldLocaleManifest[$_Key] = $script:OldSingletonManifest.$_Key
|
|
}
|
|
$script:OldLocaleManifest['ManifestType'] = 'defaultLocale'
|
|
#Parse installer keys to installer manifest
|
|
foreach ($_Key in $($OldSingletonManifest.Keys | Where-Object { $_ -in $InstallerProperties })) {
|
|
$script:OldInstallerManifest[$_Key] = $script:OldSingletonManifest.$_Key
|
|
}
|
|
$script:OldInstallerManifest['ManifestType'] = 'installer'
|
|
# Move Manifest Level Keys to installer Level
|
|
$_KeysToMove = $InstallerEntryProperties | Where-Object { $_ -in $InstallerProperties }
|
|
foreach ($_Key in $_KeysToMove) {
|
|
if ($_Key -in $script:OldInstallerManifest.Keys) {
|
|
# Handle Installer switches separately
|
|
if ($_Key -eq 'InstallerSwitches') {
|
|
$_SwitchKeysToMove = $script:OldInstallerManifest.$_Key.Keys
|
|
foreach ($_SwitchKey in $_SwitchKeysToMove) {
|
|
# If the InstallerSwitches key doesn't exist, we need to create it, otherwise, preserve switches that were already there
|
|
foreach ($_Installer in $script:OldInstallerManifest['Installers']) {
|
|
if ('InstallerSwitches' -notin $_Installer.Keys) { $_Installer['InstallerSwitches'] = @{} }
|
|
$_Installer.InstallerSwitches["$_SwitchKey"] = $script:OldInstallerManifest.$_Key.$_SwitchKey
|
|
}
|
|
}
|
|
$script:OldInstallerManifest.Remove($_Key)
|
|
continue
|
|
} else {
|
|
foreach ($_Installer in $script:OldInstallerManifest['Installers']) {
|
|
if ($_Key -eq 'InstallModes') { $script:InstallModes = [string]$script:OldInstallerManifest.$_Key }
|
|
if ($_Key -notin $_Installer.Keys) {
|
|
$_Installer[$_Key] = $script:OldInstallerManifest.$_Key
|
|
}
|
|
}
|
|
}
|
|
New-Variable -Name $_Key -Value $($script:OldInstallerManifest.$_Key -join ', ') -Scope Script -Force
|
|
$script:OldInstallerManifest.Remove($_Key)
|
|
}
|
|
}
|
|
} else {
|
|
if ($script:Option -ne 'New') { throw [ManifestException]::new("Version $LastVersion does not contain the required manifests") }
|
|
$script:OldManifestType = 'None'
|
|
}
|
|
|
|
# If the old manifests exist, read the manifest keys into their specific variables
|
|
if ($OldManifests -and $Option -ne 'NewLocale') {
|
|
$_Parameters = @(
|
|
'Publisher'; 'PublisherUrl'; 'PublisherSupportUrl'; 'PrivacyUrl'
|
|
'Author';
|
|
'PackageName'; 'PackageUrl'; 'Moniker'
|
|
'License'; 'LicenseUrl'
|
|
'Copyright'; 'CopyrightUrl'
|
|
'ShortDescription'; 'Description'
|
|
'Channel'
|
|
'Platform'; 'MinimumOSVersion'
|
|
'InstallerType'; 'NestedInstallerType'
|
|
'Scope'
|
|
'UpgradeBehavior'
|
|
'PackageFamilyName'; 'ProductCode'
|
|
'Tags'; 'FileExtensions'
|
|
'Protocols'; 'Commands'
|
|
'InstallerSuccessCodes'
|
|
'Capabilities'; 'RestrictedCapabilities'
|
|
)
|
|
Foreach ($param in $_Parameters) {
|
|
$_ReadValue = $(if ($script:OldManifestType -eq 'MultiManifest') { (Get-MultiManifestParameter $param) } else { $script:OldVersionManifest[$param] })
|
|
if (Test-String -Not $_ReadValue -IsNull) { New-Variable -Name $param -Value $_ReadValue -Scope Script -Force }
|
|
}
|
|
}
|
|
|
|
# If the old manifests exist, make sure to use the same casing as the existing package identifier
|
|
if ($OldManifests) {
|
|
$script:PackageIdentifier = $OldManifests.Where({ $_.Name -like "$PackageIdentifier.yaml" }).BaseName
|
|
}
|
|
|
|
# Run the data entry and creation of manifests appropriate to the option the user selected
|
|
Switch ($script:Option) {
|
|
'QuickUpdateVersion' {
|
|
Read-QuickInstallerEntry
|
|
Write-LocaleManifest
|
|
Write-InstallerManifest
|
|
Write-VersionManifest
|
|
}
|
|
|
|
'New' {
|
|
Read-InstallerEntry
|
|
Read-InstallerMetadata
|
|
Read-LocaleMetadata
|
|
Write-InstallerManifest
|
|
Write-VersionManifest
|
|
Write-LocaleManifest
|
|
}
|
|
|
|
'EditMetadata' {
|
|
Read-InstallerMetadata
|
|
Read-LocaleMetadata
|
|
Write-InstallerManifest
|
|
Write-VersionManifest
|
|
Write-LocaleManifest
|
|
}
|
|
|
|
'NewLocale' {
|
|
$PackageLocale = $null
|
|
$script:OldLocaleManifest = [ordered]@{}
|
|
$script:OldLocaleManifest['ManifestType'] = 'locale'
|
|
Read-LocaleMetadata
|
|
Write-LocaleManifest
|
|
}
|
|
|
|
'RemoveManifest' {
|
|
# Confirm the user is sure they know what they are doing
|
|
$_menu = @{
|
|
entries = @("[Y] Remove $PackageIdentifier version $PackageVersion"; '*[N] Cancel')
|
|
Prompt = 'Are you sure you want to continue?'
|
|
HelpText = "Manifest Versions should only be removed when necessary`n"
|
|
HelpTextColor = 'Red'
|
|
DefaultString = 'N'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor']) {
|
|
'Y' { Write-Host; continue }
|
|
default {
|
|
Invoke-CleanExit
|
|
}
|
|
}
|
|
|
|
# Require that a reason for the deletion is provided
|
|
do {
|
|
Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
|
|
Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the reason for removing this manifest'
|
|
$script:RemovalReason = Read-Host -Prompt 'Reason' | TrimString
|
|
# Check the reason for validity. The length requirements are arbitrary, but they have been set to encourage concise yet meaningful reasons
|
|
if (Test-String $script:RemovalReason -MinLength 8 -MaxLength 128 -NotNull) {
|
|
$script:_returnValue = [ReturnValue]::Success()
|
|
} else {
|
|
$script:_returnValue = [ReturnValue]::LengthError(8, 128)
|
|
}
|
|
} until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
|
|
|
|
$AppFolder = Remove-ManifestVersion $AppFolder
|
|
}
|
|
|
|
'Auto' {
|
|
# Set new package version
|
|
$script:OldInstallerManifest['PackageVersion'] = $PackageVersion
|
|
$script:OldLocaleManifest['PackageVersion'] = $PackageVersion
|
|
$script:OldVersionManifest['PackageVersion'] = $PackageVersion
|
|
|
|
# Update the manifest with URLs that are already there
|
|
Write-Host $NewLine
|
|
Write-Host 'Updating Manifest Information. This may take a while...' -ForegroundColor Blue
|
|
$_NewInstallers = @();
|
|
foreach ($_Installer in $script:OldInstallerManifest.Installers) {
|
|
$_Installer['InstallerUrl'] = [System.Web.HttpUtility]::UrlDecode($_Installer.InstallerUrl.Replace('+', '%2B'))
|
|
$_Installer['InstallerUrl'] = $_Installer.InstallerUrl.Replace(' ', '%20')
|
|
try {
|
|
$script:dest = Get-InstallerFile -URI $_Installer.InstallerUrl -PackageIdentifier $PackageIdentifier -PackageVersion $PackageVersion
|
|
} catch {
|
|
# Here we also want to pass any exceptions through for potential debugging
|
|
throw [System.Net.WebException]::new('The file could not be downloaded. Try running the script again', $_.Exception)
|
|
}
|
|
# Check that MSI's aren't actually WIX, and EXE's aren't NSIS, INNO or BURN
|
|
if ($_Installer['InstallerType'] -in @('msi'; 'exe')) {
|
|
$DetectedType = Resolve-InstallerType $script:dest
|
|
if ($DetectedType -in @('msi'; 'wix'; 'nullsoft'; 'inno'; 'burn')) { $_Installer['InstallerType'] = $DetectedType }
|
|
}
|
|
# Get the Sha256
|
|
$_Installer['InstallerSha256'] = (Get-FileHash -Path $script:dest -Algorithm SHA256).Hash
|
|
# Update the product code, if a new one exists
|
|
# If a new product code doesn't exist, and the installer isn't an `.exe` file, remove the product code if it exists
|
|
$MSIProductCode = $null
|
|
if ([System.Environment]::OSVersion.Platform -match 'Win' -and ($script:dest).EndsWith('.msi')) {
|
|
$MSIProductCode = ([string](Get-MSIProperty -Path $script:dest -Property 'ProductCode') | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
|
|
} elseif ([System.Environment]::OSVersion.Platform -match 'Unix' -and (Get-Item $script:dest).Name.EndsWith('.msi')) {
|
|
$MSIProductCode = ([string](file $script:dest) | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
|
|
}
|
|
if (Test-String -not $MSIProductCode -IsNull) {
|
|
$_Installer['ProductCode'] = $MSIProductCode
|
|
} elseif ( ($_Installer.Keys -contains 'ProductCode') -and ($_Installer.InstallerType -in @('appx'; 'msi'; 'msix'; 'wix'; 'burn'))) {
|
|
$_Installer.Remove('ProductCode')
|
|
}
|
|
# If the installer is msix or appx, try getting the new SignatureSha256
|
|
# If the new SignatureSha256 can't be found, remove it if it exists
|
|
$NewSignatureSha256 = $null
|
|
if ($_Installer.InstallerType -in @('msix', 'appx')) {
|
|
if (Get-Command 'winget' -ErrorAction SilentlyContinue) { $NewSignatureSha256 = winget hash -m $script:dest | Select-String -Pattern 'SignatureSha256:' | ConvertFrom-String; if ($NewSignatureSha256.P2) { $NewSignatureSha256 = $NewSignatureSha256.P2.ToUpper() } }
|
|
}
|
|
if (Test-String -not $NewSignatureSha256 -IsNull) {
|
|
$_Installer['SignatureSha256'] = $NewSignatureSha256
|
|
} elseif ($_Installer.Keys -contains 'SignatureSha256') {
|
|
$_Installer.Remove('SignatureSha256')
|
|
}
|
|
# If the installer is msix or appx, try getting the new package family name
|
|
# If the new package family name can't be found, remove it if it exists
|
|
if ($script:dest -match '\.(msix|appx)(bundle){0,1}$') {
|
|
$PackageFamilyName = Get-PackageFamilyName $script:dest
|
|
|
|
if (Test-String $PackageFamilyName -MatchPattern $Patterns.FamilyName) {
|
|
$_Installer['PackageFamilyName'] = $PackageFamilyName
|
|
} elseif ($_NewInstaller.Keys -contains 'PackageFamilyName') {
|
|
$_Installer.Remove('PackageFamilyName')
|
|
}
|
|
}
|
|
# Remove the downloaded files
|
|
Remove-Item -Path $script:dest
|
|
$_NewInstallers += Restore-YamlKeyOrder $_Installer $InstallerEntryProperties -NoComments
|
|
}
|
|
# Write the new manifests
|
|
$script:Installers = $_NewInstallers
|
|
Write-LocaleManifest
|
|
Write-InstallerManifest
|
|
Write-VersionManifest
|
|
# Remove the old manifests
|
|
if ($PackageVersion -ne $LastVersion) { Remove-ManifestVersion "$AppFolder\..\$LastVersion" }
|
|
}
|
|
}
|
|
|
|
if ($script:Option -ne 'RemoveManifest') {
|
|
# If the user has winget installed, attempt to validate the manifests
|
|
if (Get-Command 'winget' -ErrorAction SilentlyContinue) { winget validate $AppFolder }
|
|
|
|
# If the user has sandbox enabled, request to test the manifest in the sandbox
|
|
if (Get-Command 'WindowsSandbox' -ErrorAction SilentlyContinue) {
|
|
# Check the settings to see if we need to display this menu
|
|
switch ($ScriptSettings.TestManifestsInSandbox) {
|
|
'always' { $script:SandboxTest = '0' }
|
|
'never' { $script:SandboxTest = '1' }
|
|
default {
|
|
$_menu = @{
|
|
entries = @('*[Y] Yes'; '[N] No')
|
|
Prompt = '[Recommended] Do you want to test your Manifest in Windows Sandbox?'
|
|
DefaultString = 'Y'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'Y' { $script:SandboxTest = '0' }
|
|
'N' { $script:SandboxTest = '1' }
|
|
default { $script:SandboxTest = '0' }
|
|
}
|
|
Write-Host
|
|
}
|
|
}
|
|
if ($script:SandboxTest -eq '0') {
|
|
if (Test-Path -Path "$gitTopLevel\Tools\SandboxTest.ps1") {
|
|
$SandboxScriptPath = (Resolve-Path "$gitTopLevel\Tools\SandboxTest.ps1").Path
|
|
} else {
|
|
while ([string]::IsNullOrWhiteSpace($SandboxScriptPath)) {
|
|
Write-Host
|
|
Write-Host -ForegroundColor 'Green' -Object 'SandboxTest.ps1 not found, input path'
|
|
$SandboxScriptPath = Read-Host -Prompt 'SandboxTest.ps1' | TrimString
|
|
}
|
|
}
|
|
if ($script:UsesPrerelease) {
|
|
& $SandboxScriptPath -Manifest $AppFolder -Prerelease -EnableExperimentalFeatures
|
|
} else {
|
|
& $SandboxScriptPath -Manifest $AppFolder
|
|
}
|
|
}
|
|
}
|
|
}
|
|
# If the user has git installed, request to automatically submit the PR
|
|
if (Get-Command 'git' -ErrorAction SilentlyContinue) {
|
|
switch ($ScriptSettings.AutoSubmitPRs) {
|
|
'always' { $PromptSubmit = '0' }
|
|
'never' { $PromptSubmit = '1' }
|
|
default {
|
|
$_menu = @{
|
|
entries = @('*[Y] Yes'; '[N] No')
|
|
Prompt = 'Do you want to submit your PR now?'
|
|
DefaultString = 'Y'
|
|
}
|
|
switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
|
|
'Y' { $PromptSubmit = '0' }
|
|
'N' { $PromptSubmit = '1' }
|
|
default { $PromptSubmit = '0' }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Write-Host
|
|
|
|
# If the user agreed to automatically submit the PR
|
|
if ($PromptSubmit -eq '0') {
|
|
# Determine what type of update should be used as the prefix for the PR
|
|
switch -regex ($Option) {
|
|
'New|QuickUpdateVersion|Auto' {
|
|
$AllVersions = (@($script:ExistingVersions) + @($PackageVersion)) | Sort-Object $ToNatural
|
|
if ($AllVersions.Count -eq '1') { $CommitType = 'New package' }
|
|
elseif ($script:PackageVersion -in $script:ExistingVersions) { $CommitType = 'Update' }
|
|
elseif (($AllVersions.IndexOf($PackageVersion) + 1) -eq $AllVersions.Count) { $CommitType = 'New version' }
|
|
elseif (($AllVersions.IndexOf($PackageVersion) + 1) -ne $AllVersions.Count) { $CommitType = 'Add version' }
|
|
}
|
|
'EditMetadata' { $CommitType = 'Metadata' }
|
|
'NewLocale' { $CommitType = 'Locale' }
|
|
'RemoveManifest' { $CommitType = 'Remove' }
|
|
}
|
|
|
|
# Change the users git configuration to suppress some git messages
|
|
$_previousConfig = git config --get core.safecrlf
|
|
if ($_previousConfig) {
|
|
git config --replace core.safecrlf false
|
|
} else {
|
|
git config --add core.safecrlf false
|
|
}
|
|
|
|
# Fetch the upstream branch, create a commit onto the detached head, and push it to a new branch
|
|
git fetch upstream master --quiet
|
|
git switch -d upstream/master
|
|
if ($LASTEXITCODE -eq '0') {
|
|
$BranchName = "$PackageIdentifier-$PackageVersion-$RunHash"
|
|
# Git branch names cannot start with `.` cannot contain any of {`..`, `\`, `~`, `^`, `:`, ` `, `?`, `@{`, `[`}, and cannot end with {`/`, `.lock`, `.`}
|
|
$BranchName = $BranchName -replace '[\~,\^,\:,\\,\?,\@\{,\*,\[,\s]{1,}|[.lock|/|\.]*$|^\.{1,}|\.\.', ''
|
|
git add "$(Join-Path (Get-Item $AppFolder).Parent.FullName -ChildPath '*')"
|
|
git commit -m "$CommitType`: $PackageIdentifier version $PackageVersion" --quiet
|
|
git switch -c "$BranchName" --quiet
|
|
git push --set-upstream origin "$BranchName" --quiet
|
|
|
|
# If the user has the cli too
|
|
if (Get-Command 'gh' -ErrorAction SilentlyContinue) {
|
|
# Request the user to fill out the PR template
|
|
if (Test-Path -Path "$gitTopLevel\.github\PULL_REQUEST_TEMPLATE.md") {
|
|
Read-PRBody (Resolve-Path "$gitTopLevel\.github\PULL_REQUEST_TEMPLATE.md").Path
|
|
} else {
|
|
while ([string]::IsNullOrWhiteSpace($PRTemplate)) {
|
|
Write-Host
|
|
Write-Host -ForegroundColor 'Green' -Object 'PULL_REQUEST_TEMPLATE.md not found, input path'
|
|
$PRTemplate = Read-Host -Prompt 'PR Template' | TrimString
|
|
}
|
|
Read-PRBody "$PRTemplate"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Restore the user's previous git settings to ensure we don't disrupt their normal flow
|
|
if ($_previousConfig) {
|
|
git config --replace core.safecrlf $_previousConfig
|
|
} else {
|
|
git config --unset core.safecrlf
|
|
}
|
|
|
|
} else {
|
|
Write-Host
|
|
Invoke-CleanExit
|
|
}
|
|
[Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
|
|
[Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
|
|
|
|
# Error levels for the ReturnValue class
|
|
Enum ErrorLevel {
|
|
Undefined = -1
|
|
Info = 0
|
|
Warning = 1
|
|
Error = 2
|
|
Critical = 3
|
|
}
|
|
|
|
# Custom class for validation and error checking
|
|
# `200` should be indicative of a success
|
|
# `400` should be indicative of a bad request
|
|
# `500` should be indicative of an internal error / other error
|
|
Class ReturnValue {
|
|
[int] $StatusCode
|
|
[string] $Title
|
|
[string] $Message
|
|
[ErrorLevel] $Severity
|
|
|
|
# Default Constructor
|
|
ReturnValue() {
|
|
}
|
|
|
|
# Overload 1; Creates a return value with only a status code and no descriptors
|
|
ReturnValue(
|
|
[int]$statusCode
|
|
) {
|
|
$this.StatusCode = $statusCode
|
|
$this.Title = '-'
|
|
$this.Message = '-'
|
|
$this.Severity = -1
|
|
}
|
|
|
|
# Overload 2; Create a return value with all parameters defined
|
|
ReturnValue(
|
|
[int] $statusCode,
|
|
[string] $title,
|
|
[string] $message,
|
|
[ErrorLevel] $severity
|
|
) {
|
|
$this.StatusCode = $statusCode
|
|
$this.Title = $title
|
|
$this.Message = $message
|
|
$this.Severity = $severity
|
|
}
|
|
|
|
# Static reference to a default success value
|
|
[ReturnValue] static Success() {
|
|
return [ReturnValue]::new(200, 'OK', 'The command completed successfully', 'Info')
|
|
}
|
|
|
|
# Static reference to a default internal error value
|
|
[ReturnValue] static GenericError() {
|
|
return [ReturnValue]::new(500, 'Internal Error', 'Value was not able to be saved successfully', 2)
|
|
|
|
}
|
|
|
|
# Static reference to a specific error relating to the pattern of user input
|
|
[ReturnValue] static PatternError() {
|
|
return [ReturnValue]::new(400, 'Invalid Pattern', 'The value entered does not match the pattern requirements defined in the manifest schema', 2)
|
|
}
|
|
|
|
# Static reference to a specific error relating to the length of user input
|
|
[ReturnValue] static LengthError([int]$MinLength, [int]$MaxLength) {
|
|
return [ReturnValue]::new(400, 'Invalid Length', "Length must be between $MinLength and $MaxLength characters", 2)
|
|
}
|
|
|
|
# Static reference to a specific error relating to the number of entries a user input
|
|
[ReturnValue] static MaxItemsError([int]$MaxEntries) {
|
|
return [ReturnValue]::new(400, 'Too many entries', "Number of entries must be less than or equal to $MaxEntries", 2)
|
|
}
|
|
|
|
# Returns the ReturnValue as a nicely formatted string
|
|
[string] ToString() {
|
|
return "[$($this.Severity)] ($($this.StatusCode)) $($this.Title) - $($this.Message)"
|
|
}
|
|
|
|
# Returns the ReturnValue as a nicely formatted string if the status code is not equal to 200
|
|
[string] ErrorString() {
|
|
if ($this.StatusCode -eq 200) {
|
|
return $null
|
|
} else {
|
|
return "[$($this.Severity)] $($this.Title) - $($this.Message)`n"
|
|
}
|
|
}
|
|
}
|
|
|
|
class UnmetDependencyException : Exception {
|
|
UnmetDependencyException([string] $message) : base($message) {}
|
|
UnmetDependencyException([string] $message, [Exception] $exception) : base($message, $exception) {}
|
|
}
|
|
class ManifestException : Exception {
|
|
ManifestException([string] $message) : base($message) {}
|
|
}
|