YamlCreate.ps1 145 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763
  1. #Requires -Version 5
  2. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'This script is not intended to have any outputs piped')]
  3. Param
  4. (
  5. [switch] $Settings,
  6. [switch] $AutoUpgrade,
  7. [switch] $help,
  8. [switch] $SkipPRCheck,
  9. [Parameter(Mandatory = $false)]
  10. [string] $PackageIdentifier,
  11. [Parameter(Mandatory = $false)]
  12. [string] $PackageVersion,
  13. [Parameter(Mandatory = $false)]
  14. [string] $Mode
  15. )
  16. if ($help) {
  17. Write-Host -ForegroundColor 'Green' 'For full documentation of the script, see https://github.com/microsoft/winget-pkgs/tree/master/doc/tools/YamlCreate.md'
  18. Write-Host -ForegroundColor 'Yellow' 'Usage: ' -NoNewline
  19. Write-Host -ForegroundColor 'White' '.\YamlCreate.ps1 [-PackageIdentifier <identifier>] [-PackageVersion <version>] [-Mode <1-5>] [-Settings] [-SkipPRCheck]'
  20. Write-Host
  21. exit
  22. }
  23. # Custom menu prompt that listens for keypresses. Requires a prompt and array of entries at minimum. Entries preceeded with `*` are shown in green
  24. # Returns a console key value
  25. Function Invoke-KeypressMenu {
  26. Param
  27. (
  28. [Parameter(Mandatory = $true, Position = 0)]
  29. [string] $Prompt,
  30. [Parameter(Mandatory = $true, Position = 1)]
  31. [string[]] $Entries,
  32. [Parameter(Mandatory = $false)]
  33. [string] $HelpText,
  34. [Parameter(Mandatory = $false)]
  35. [string] $HelpTextColor,
  36. [Parameter(Mandatory = $false)]
  37. [string] $DefaultString
  38. )
  39. Write-Host "`n"
  40. Write-Host -ForegroundColor 'Yellow' "$Prompt"
  41. if ($PSBoundParameters.ContainsKey('HelpText') -and (![string]::IsNullOrWhiteSpace($HelpText))) {
  42. if ($PSBoundParameters.ContainsKey('HelpTextColor') -and (![string]::IsNullOrWhiteSpace($HelpTextColor))) {
  43. Write-Host -ForegroundColor $HelpTextColor $HelpText
  44. } else {
  45. Write-Host -ForegroundColor 'Blue' $HelpText
  46. }
  47. }
  48. foreach ($entry in $Entries) {
  49. $_isDefault = $entry.StartsWith('*')
  50. if ($_isDefault) {
  51. $_entry = ' ' + $entry.Substring(1)
  52. $_color = 'Green'
  53. } else {
  54. $_entry = ' ' + $entry
  55. $_color = 'White'
  56. }
  57. Write-Host -ForegroundColor $_color $_entry
  58. }
  59. Write-Host
  60. if ($PSBoundParameters.ContainsKey('DefaultString') -and (![string]::IsNullOrWhiteSpace($DefaultString))) {
  61. Write-Host -NoNewline "Enter Choice (default is '$DefaultString'): "
  62. } else {
  63. Write-Host -NoNewline 'Enter Choice ('
  64. Write-Host -NoNewline -ForegroundColor 'Green' 'Green'
  65. Write-Host -NoNewline ' is default): '
  66. }
  67. do {
  68. $keyInfo = [Console]::ReadKey($false)
  69. } until ($keyInfo.Key)
  70. return $keyInfo.Key
  71. }
  72. #If the user has git installed, make sure it is a patched version
  73. if (Get-Command 'git.exe' -ErrorAction SilentlyContinue) {
  74. $GitMinimumVersion = [System.Version]::Parse('2.35.2')
  75. $gitVersionString = ((git version) | Select-String '([0-9]{1,}\.){3,4}').Matches.Value.Trim(' ', '.')
  76. $gitVersion = [System.Version]::Parse($gitVersionString)
  77. if ($gitVersion -lt $GitMinimumVersion) {
  78. # Prompt user to install git
  79. if (Get-Command 'winget.exe' -ErrorAction SilentlyContinue) {
  80. $_menu = @{
  81. entries = @('[Y] Upgrade Git'; '[N] Do not upgrade')
  82. Prompt = 'The version of git installed on your machine does not satisfy the requirement of version >= 2.35.2; Would you like to upgrade?'
  83. HelpText = "Upgrading will attempt to upgrade git using winget`n"
  84. DefaultString = ''
  85. }
  86. switch (Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText']) {
  87. 'Y' {
  88. Write-Host
  89. try {
  90. winget upgrade --id Git.Git --exact
  91. } catch {
  92. throw [UnmetDependencyException]::new('Git could not be upgraded sucessfully', $_)
  93. } finally {
  94. $gitVersionString = ((git version) | Select-String '([0-9]{1,}\.){3,4}').Matches.Value.Trim(' ', '.')
  95. $gitVersion = [System.Version]::Parse($gitVersionString)
  96. if ($gitVersion -lt $GitMinimumVersion) {
  97. throw [UnmetDependencyException]::new('Git could not be upgraded sucessfully')
  98. }
  99. }
  100. }
  101. default { Write-Host; throw [UnmetDependencyException]::new('The version of git installed on your machine does not satisfy the requirement of version >= 2.35.2') }
  102. }
  103. } else {
  104. throw [UnmetDependencyException]::new('The version of git installed on your machine does not satisfy the requirement of version >= 2.35.2')
  105. }
  106. }
  107. # Check whether the script is present inside a fork/clone of microsoft/winget-pkgs repository
  108. try {
  109. $script:gitTopLevel = (Resolve-Path $(git rev-parse --show-toplevel)).Path
  110. } catch {
  111. # 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
  112. throw [UnmetDependencyException]::new('This script must be run from inside a clone of the winget-pkgs repository', $_.Exception)
  113. }
  114. }
  115. # Installs `powershell-yaml` as a dependency for parsing yaml content
  116. if (-not(Get-Module -ListAvailable -Name powershell-yaml)) {
  117. try {
  118. Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
  119. Install-Module -Name powershell-yaml -Force -Repository PSGallery -Scope CurrentUser
  120. } catch {
  121. # If there was an exception while installing powershell-yaml, pass it as an InternalException for further debugging
  122. throw [UnmetDependencyException]::new("'powershell-yaml' unable to be installed successfully", $_.Exception)
  123. } finally {
  124. # Double check that it was installed properly
  125. if (-not(Get-Module -ListAvailable -Name powershell-yaml)) {
  126. throw [UnmetDependencyException]::new("'powershell-yaml' is not found")
  127. }
  128. }
  129. }
  130. # Set settings directory on basis of Operating System
  131. $script:SettingsPath = Join-Path $(if ([System.Environment]::OSVersion.Platform -match 'Win') { $env:LOCALAPPDATA } else { $env:HOME + '/.config' } ) -ChildPath 'YamlCreate'
  132. # Check for settings directory and create it if none exists
  133. if (!(Test-Path $script:SettingsPath)) { New-Item -ItemType 'Directory' -Force -Path $script:SettingsPath | Out-Null }
  134. # Check for settings file and create it if none exists
  135. $script:SettingsPath = $(Join-Path $script:SettingsPath -ChildPath 'Settings.yaml')
  136. 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 }
  137. # Load settings from file
  138. $ScriptSettings = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $script:SettingsPath -Encoding UTF8) -join "`n")
  139. if ($Settings) {
  140. Invoke-Item -Path $script:SettingsPath
  141. exit
  142. }
  143. $ScriptHeader = '# Created with YamlCreate.ps1 v2.1.2'
  144. $ManifestVersion = '1.1.0'
  145. $PSDefaultParameterValues = @{ '*:Encoding' = 'UTF8' }
  146. $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
  147. $ofs = ', '
  148. $callingUICulture = [Threading.Thread]::CurrentThread.CurrentUICulture
  149. $callingCulture = [Threading.Thread]::CurrentThread.CurrentCulture
  150. [Threading.Thread]::CurrentThread.CurrentUICulture = 'en-US'
  151. [Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'
  152. <#
  153. .SYNOPSIS
  154. Winget Manifest creation helper script
  155. .DESCRIPTION
  156. The intent of this file is to help you generate a manifest for publishing
  157. to the Windows Package Manager repository.
  158. It'll attempt to download an installer from the user-provided URL to calculate
  159. a checksum. That checksum and the rest of the input data will be compiled in a
  160. .YAML file.
  161. .EXAMPLE
  162. PS C:\Projects\winget-pkgs> Get-Help .\Tools\YamlCreate.ps1 -Full
  163. Show this script's help
  164. .EXAMPLE
  165. PS C:\Projects\winget-pkgs> .\Tools\YamlCreate.ps1
  166. Run the script to create a manifest file
  167. .NOTES
  168. Please file an issue if you run into errors with this script:
  169. https://github.com/microsoft/winget-pkgs/issues/
  170. .LINK
  171. https://github.com/microsoft/winget-pkgs/blob/master/Tools/YamlCreate.ps1
  172. #>
  173. # Fetch Schema data from github for entry validation, key ordering, and automatic commenting
  174. try {
  175. $ProgressPreference = 'SilentlyContinue'
  176. $LocaleSchema = @(Invoke-WebRequest "https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v$ManifestVersion/manifest.defaultLocale.$ManifestVersion.json" -UseBasicParsing | ConvertFrom-Json)
  177. $LocaleProperties = (ConvertTo-Yaml $LocaleSchema.properties | ConvertFrom-Yaml -Ordered).Keys
  178. $VersionSchema = @(Invoke-WebRequest "https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v$ManifestVersion/manifest.version.$ManifestVersion.json" -UseBasicParsing | ConvertFrom-Json)
  179. $VersionProperties = (ConvertTo-Yaml $VersionSchema.properties | ConvertFrom-Yaml -Ordered).Keys
  180. $InstallerSchema = @(Invoke-WebRequest "https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v$ManifestVersion/manifest.installer.$ManifestVersion.json" -UseBasicParsing | ConvertFrom-Json)
  181. $InstallerProperties = (ConvertTo-Yaml $InstallerSchema.properties | ConvertFrom-Yaml -Ordered).Keys
  182. $InstallerSwitchProperties = (ConvertTo-Yaml $InstallerSchema.definitions.InstallerSwitches.properties | ConvertFrom-Yaml -Ordered).Keys
  183. $InstallerEntryProperties = (ConvertTo-Yaml $InstallerSchema.definitions.Installer.properties | ConvertFrom-Yaml -Ordered).Keys
  184. $InstallerDependencyProperties = (ConvertTo-Yaml $InstallerSchema.definitions.Dependencies.properties | ConvertFrom-Yaml -Ordered).Keys
  185. } catch {
  186. # Here we want to pass the exception as an inner exception for debugging if necessary
  187. throw [System.Net.WebException]::new('Manifest schemas could not be downloaded. Try running the script again', $_.Exception)
  188. }
  189. filter TrimString {
  190. $_.Trim()
  191. }
  192. filter UniqueItems {
  193. [string]$($_.Split(',').Trim() | Select-Object -Unique)
  194. }
  195. filter ToLower {
  196. [string]$_.ToLower()
  197. }
  198. filter NoWhitespace {
  199. [string]$_ -replace '\s{1,}', '-'
  200. }
  201. $ToNatural = { [regex]::Replace($_, '\d+', { $args[0].Value.PadLeft(20) }) }
  202. # Various patterns used in validation to simplify the validation logic
  203. $Patterns = @{
  204. PackageIdentifier = $VersionSchema.properties.PackageIdentifier.pattern
  205. IdentifierMaxLength = $VersionSchema.properties.PackageIdentifier.maxLength
  206. PackageVersion = $InstallerSchema.definitions.PackageVersion.pattern
  207. VersionMaxLength = $VersionSchema.properties.PackageVersion.maxLength
  208. InstallerSha256 = $InstallerSchema.definitions.Installer.properties.InstallerSha256.pattern
  209. InstallerUrl = $InstallerSchema.definitions.Installer.properties.InstallerUrl.pattern
  210. InstallerUrlMaxLength = $InstallerSchema.definitions.Installer.properties.InstallerUrl.maxLength
  211. ValidArchitectures = $InstallerSchema.definitions.Installer.properties.Architecture.enum
  212. ValidInstallerTypes = $InstallerSchema.definitions.InstallerType.enum
  213. SilentSwitchMaxLength = $InstallerSchema.definitions.InstallerSwitches.properties.Silent.maxLength
  214. ProgressSwitchMaxLength = $InstallerSchema.definitions.InstallerSwitches.properties.SilentWithProgress.maxLength
  215. CustomSwitchMaxLength = $InstallerSchema.definitions.InstallerSwitches.properties.Custom.maxLength
  216. SignatureSha256 = $InstallerSchema.definitions.Installer.properties.SignatureSha256.pattern
  217. FamilyName = $InstallerSchema.definitions.PackageFamilyName.pattern
  218. FamilyNameMaxLength = $InstallerSchema.definitions.PackageFamilyName.maxLength
  219. PackageLocale = $LocaleSchema.properties.PackageLocale.pattern
  220. InstallerLocaleMaxLength = $InstallerSchema.definitions.Locale.maxLength
  221. ProductCodeMinLength = $InstallerSchema.definitions.ProductCode.minLength
  222. ProductCodeMaxLength = $InstallerSchema.definitions.ProductCode.maxLength
  223. MaxItemsFileExtensions = $InstallerSchema.definitions.FileExtensions.maxItems
  224. MaxItemsProtocols = $InstallerSchema.definitions.Protocols.maxItems
  225. MaxItemsCommands = $InstallerSchema.definitions.Commands.maxItems
  226. MaxItemsSuccessCodes = $InstallerSchema.definitions.InstallerSuccessCodes.maxItems
  227. MaxItemsInstallModes = $InstallerSchema.definitions.InstallModes.maxItems
  228. PackageLocaleMaxLength = $LocaleSchema.properties.PackageLocale.maxLength
  229. PublisherMaxLength = $LocaleSchema.properties.Publisher.maxLength
  230. PackageNameMaxLength = $LocaleSchema.properties.PackageName.maxLength
  231. MonikerMaxLength = $LocaleSchema.definitions.Tag.maxLength
  232. GenericUrl = $LocaleSchema.definitions.Url.pattern
  233. GenericUrlMaxLength = $LocaleSchema.definitions.Url.maxLength
  234. AuthorMinLength = $LocaleSchema.properties.Author.minLength
  235. AuthorMaxLength = $LocaleSchema.properties.Author.maxLength
  236. LicenseMaxLength = $LocaleSchema.properties.License.maxLength
  237. CopyrightMinLength = $LocaleSchema.properties.Copyright.minLength
  238. CopyrightMaxLength = $LocaleSchema.properties.Copyright.maxLength
  239. TagsMaxItems = $LocaleSchema.properties.Tags.maxItems
  240. ShortDescriptionMaxLength = $LocaleSchema.properties.ShortDescription.maxLength
  241. DescriptionMinLength = $LocaleSchema.properties.Description.minLength
  242. DescriptionMaxLength = $LocaleSchema.properties.Description.maxLength
  243. ValidInstallModes = $InstallerSchema.definitions.InstallModes.items.enum
  244. FileExtension = $InstallerSchema.definitions.FileExtensions.items.pattern
  245. FileExtensionMaxLength = $InstallerSchema.definitions.FileExtensions.items.maxLength
  246. ReleaseNotesMinLength = $LocaleSchema.properties.ReleaseNotes.MinLength
  247. ReleaseNotesMaxLength = $LocaleSchema.properties.ReleaseNotes.MaxLength
  248. }
  249. # This function validates whether a string matches Minimum Length, Maximum Length, and Regex pattern
  250. # The switches can be used to specify if null values are allowed regardless of validation
  251. Function Test-String {
  252. Param
  253. (
  254. [Parameter(Mandatory = $true, Position = 0)]
  255. [AllowEmptyString()]
  256. [string] $InputString,
  257. [Parameter(Mandatory = $false)]
  258. [regex] $MatchPattern,
  259. [Parameter(Mandatory = $false)]
  260. [int] $MinLength,
  261. [Parameter(Mandatory = $false)]
  262. [int] $MaxLength,
  263. [switch] $AllowNull,
  264. [switch] $NotNull,
  265. [switch] $IsNull,
  266. [switch] $Not
  267. )
  268. $_isValid = $true
  269. if ($PSBoundParameters.ContainsKey('MinLength')) {
  270. $_isValid = $_isValid -and ($InputString.Length -ge $MinLength)
  271. }
  272. if ($PSBoundParameters.ContainsKey('MaxLength')) {
  273. $_isValid = $_isValid -and ($InputString.Length -le $MaxLength)
  274. }
  275. if ($PSBoundParameters.ContainsKey('MatchPattern')) {
  276. $_isValid = $_isValid -and ($InputString -match $MatchPattern)
  277. }
  278. if ($AllowNull -and [string]::IsNullOrWhiteSpace($InputString)) {
  279. $_isValid = $true
  280. } elseif ($NotNull -and [string]::IsNullOrWhiteSpace($InputString)) {
  281. $_isValid = $false
  282. }
  283. if ($IsNull) {
  284. $_isValid = [string]::IsNullOrWhiteSpace($InputString)
  285. }
  286. if ($Not) {
  287. return !$_isValid
  288. } else {
  289. return $_isValid
  290. }
  291. }
  292. # Takes an array of strings and an array of colors then writes one line of text composed of each string being its respective color
  293. Function Write-MulticolorLine {
  294. Param
  295. (
  296. [Parameter(Mandatory = $true, Position = 0)]
  297. [string[]] $TextStrings,
  298. [Parameter(Mandatory = $true, Position = 1)]
  299. [string[]] $Colors
  300. )
  301. If ($TextStrings.Count -ne $Colors.Count) {
  302. throw [System.ArgumentException]::new('Invalid Function Parameters. Arguments must be of equal length')
  303. }
  304. $_index = 0
  305. Foreach ($String in $TextStrings) {
  306. Write-Host -ForegroundColor $Colors[$_index] -NoNewline $String
  307. $_index++
  308. }
  309. }
  310. # Checks a URL and returns the status code received from the URL
  311. Function Test-Url {
  312. Param
  313. (
  314. [Parameter(Mandatory = $true, Position = 0)]
  315. [string] $URL
  316. )
  317. try {
  318. $HTTP_Request = [System.Net.WebRequest]::Create($URL)
  319. $HTTP_Request.UserAgent = 'Microsoft-Delivery-Optimization/10.1'
  320. $HTTP_Response = $HTTP_Request.GetResponse()
  321. $script:ResponseUri = $HTTP_Response.ResponseUri.AbsoluteUri
  322. $HTTP_Status = [int]$HTTP_Response.StatusCode
  323. } catch {
  324. # Take no action here; If there is an exception, we will treat it like a 404
  325. $HTTP_Status = 404
  326. }
  327. If ($null -eq $HTTP_Response) { $HTTP_Status = 404 }
  328. Else { $HTTP_Response.Close() }
  329. return $HTTP_Status
  330. }
  331. # Checks a file name for validity and returns a boolean value
  332. Function Test-ValidFileName {
  333. param([string]$FileName)
  334. $IndexOfInvalidChar = $FileName.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars())
  335. # IndexOfAny() returns the value -1 to indicate no such character was found
  336. return $IndexOfInvalidChar -eq -1
  337. }
  338. # 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
  339. # Returns the validated URL which was entered
  340. Function Request-InstallerUrl {
  341. do {
  342. Write-Host -ForegroundColor $(if ($script:_returnValue.Severity -gt 1) { 'red' } else { 'yellow' }) $script:_returnValue.ErrorString()
  343. if ($script:_returnValue.StatusCode -ne 409) {
  344. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the download url to the installer.'
  345. $NewInstallerUrl = Read-Host -Prompt 'Url' | TrimString
  346. }
  347. $script:_returnValue = [ReturnValue]::GenericError()
  348. if ((Test-Url $NewInstallerUrl) -ne 200) {
  349. $script:_returnValue = [ReturnValue]::new(502, 'Invalid URL Response', 'The URL did not return a successful response from the server', 2)
  350. } else {
  351. if (($script:ResponseUri -ne $NewInstallerUrl) -and ($ScriptSettings.UseRedirectedURL -ne 'never') -and ($NewInstallerUrl -notmatch 'github')) {
  352. #If urls don't match, ask to update; If they do update, set custom error and check for validity;
  353. $_menu = @{
  354. entries = @('*[Y] Use detected URL'; '[N] Use original URL')
  355. Prompt = 'The URL provided appears to be redirected. Would you like to use the destination URL instead?'
  356. HelpText = "Discovered URL: $($script:ResponseUri)"
  357. DefaultString = 'Y'
  358. }
  359. switch ($(if ($ScriptSettings.UseRedirectedURL -eq 'always') { 'Y' } else { Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] })) {
  360. 'N' { Write-Host -ForegroundColor 'Green' "`nOriginal URL Retained - Proceeding with $NewInstallerUrl`n" } #Continue without replacing URL
  361. default {
  362. $NewInstallerUrl = $script:ResponseUri
  363. $script:_returnValue = [ReturnValue]::new(409, 'URL Changed', 'The URL was changed during processing and will be re-validated', 1)
  364. Write-Host
  365. }
  366. }
  367. }
  368. if ($script:_returnValue.StatusCode -ne 409) {
  369. if (Test-String $NewInstallerUrl -MaxLength $Patterns.InstallerUrlMaxLength -MatchPattern $Patterns.InstallerUrl -NotNull) {
  370. $script:_returnValue = [ReturnValue]::Success()
  371. } else {
  372. if (Test-String -not $NewInstallerUrl -MaxLength $Patterns.InstallerUrlMaxLength -NotNull) {
  373. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.InstallerUrlMaxLength)
  374. } elseif (Test-String -not $NewInstallerUrl -MatchPattern $Patterns.InstallerUrl) {
  375. $script:_returnValue = [ReturnValue]::PatternError()
  376. } else {
  377. $script:_returnValue = [ReturnValue]::GenericError()
  378. }
  379. }
  380. }
  381. }
  382. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  383. return $NewInstallerUrl
  384. }
  385. Function Get-InstallerFile {
  386. Param
  387. (
  388. [Parameter(Mandatory = $true, Position = 0)]
  389. [string] $URI,
  390. [Parameter(Mandatory = $true, Position = 1)]
  391. [string] $PackageIdentifier,
  392. [Parameter(Mandatory = $true, Position = 2)]
  393. [string] $PackageVersion
  394. )
  395. # Create a filename based on the Package Identifier and Version; Try to get the extension from the URL
  396. # If the extension isn't found, use a custom one
  397. $_URIPath = $URI.Split('?')[0]
  398. $_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' })
  399. if (Test-ValidFileName $_Filename) { $_OutFile = Join-Path -Path $env:TEMP -ChildPath $_Filename }
  400. else { $_OutFile = (New-TemporaryFile).FullName }
  401. # Create a new web client for downloading the file
  402. $_WebClient = [System.Net.WebClient]::new()
  403. $_WebClient.Headers.Add('User-Agent', 'Microsoft-Delivery-Optimization/10.1')
  404. # If the system has a default proxy set, use it
  405. # Powershell Core will automatically use this, so it's only necessary for PS5
  406. if ($PSVersionTable.PSVersion.Major -lt 6) { $_WebClient.Proxy = [System.Net.WebProxy]::GetDefaultProxy() }
  407. # Download the file
  408. $_WebClient.DownloadFile($URI, $_OutFile)
  409. # Dispose of the web client to release the resources it uses
  410. $_WebClient.Dispose()
  411. return $_OutFile
  412. }
  413. Function Get-MSIProperty {
  414. Param
  415. (
  416. [Parameter(Mandatory = $true)]
  417. [string] $MSIPath,
  418. [Parameter(Mandatory = $true)]
  419. [string] $Parameter
  420. )
  421. try {
  422. $windowsInstaller = New-Object -com WindowsInstaller.Installer
  423. $database = $windowsInstaller.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $windowsInstaller, @($MSIPath, 0))
  424. $view = $database.GetType().InvokeMember('OpenView', 'InvokeMethod', $null, $database, ("SELECT Value FROM Property WHERE Property = '$Parameter'"))
  425. $view.GetType().InvokeMember('Execute', 'InvokeMethod', $null, $view, $null)
  426. $record = $view.GetType().InvokeMember('Fetch', 'InvokeMethod', $null, $view, $null)
  427. $outputObject = $($record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, 1))
  428. $view.GetType().InvokeMember('Close', 'InvokeMethod', $null, $view, $null)
  429. [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($view)
  430. [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($database)
  431. [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($windowsInstaller)
  432. [System.GC]::Collect()
  433. [System.GC]::WaitForPendingFinalizers()
  434. return $outputObject
  435. } catch {
  436. Write-Error -Message $_.ToString()
  437. break
  438. }
  439. }
  440. Function Get-ItemMetadata {
  441. Param
  442. (
  443. [Parameter(Mandatory = $true)]
  444. [string] $FilePath
  445. )
  446. try {
  447. $MetaDataObject = [ordered] @{}
  448. $FileInformation = (Get-Item $FilePath)
  449. $ShellApplication = New-Object -ComObject Shell.Application
  450. $ShellFolder = $ShellApplication.Namespace($FileInformation.Directory.FullName)
  451. $ShellFile = $ShellFolder.ParseName($FileInformation.Name)
  452. $MetaDataProperties = [ordered] @{}
  453. 0..400 | ForEach-Object -Process {
  454. $DataValue = $ShellFolder.GetDetailsOf($null, $_)
  455. $PropertyValue = (Get-Culture).TextInfo.ToTitleCase($DataValue.Trim()).Replace(' ', '')
  456. if ($PropertyValue -ne '') {
  457. $MetaDataProperties["$_"] = $PropertyValue
  458. }
  459. }
  460. foreach ($Key in $MetaDataProperties.Keys) {
  461. $Property = $MetaDataProperties[$Key]
  462. $Value = $ShellFolder.GetDetailsOf($ShellFile, [int] $Key)
  463. if ($Property -in 'Attributes', 'Folder', 'Type', 'SpaceFree', 'TotalSize', 'SpaceUsed') {
  464. continue
  465. }
  466. If (($null -ne $Value) -and ($Value -ne '')) {
  467. $MetaDataObject["$Property"] = $Value
  468. }
  469. }
  470. [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($ShellFile)
  471. [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($ShellFolder)
  472. [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($ShellApplication)
  473. [System.GC]::Collect()
  474. [System.GC]::WaitForPendingFinalizers()
  475. return $MetaDataObject
  476. } catch {
  477. Write-Error -Message $_.ToString()
  478. break
  479. }
  480. }
  481. function Get-Property ($Object, $PropertyName, [object[]]$ArgumentList) {
  482. return $Object.GetType().InvokeMember($PropertyName, 'Public, Instance, GetProperty', $null, $Object, $ArgumentList)
  483. }
  484. Function Get-MsiDatabase {
  485. Param
  486. (
  487. [Parameter(Mandatory = $true)]
  488. [string] $FilePath
  489. )
  490. Write-Host -ForegroundColor 'Yellow' 'Reading Installer Database. This may take some time. . .'
  491. $windowsInstaller = New-Object -com WindowsInstaller.Installer
  492. $MSI = $windowsInstaller.OpenDatabase($FilePath, 0)
  493. $_TablesView = $MSI.OpenView('select * from _Tables')
  494. $_TablesView.Execute()
  495. $_Database = @{}
  496. do {
  497. $_Table = $_TablesView.Fetch()
  498. if ($_Table) {
  499. $_TableName = Get-Property $_Table StringData 1
  500. $_Database["$_TableName"] = @{}
  501. }
  502. } while ($_Table)
  503. [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($_TablesView)
  504. foreach ($_Table in $_Database.Keys) {
  505. # Write-Host $_Table
  506. $_ItemView = $MSI.OpenView("select * from $_Table")
  507. $_ItemView.Execute()
  508. do {
  509. $_Item = $_ItemView.Fetch()
  510. if ($_Item) {
  511. $_ItemValue = $null
  512. $_ItemName = Get-Property $_Item StringData 1
  513. if ($_Table -eq 'Property') { $_ItemValue = Get-Property $_Item StringData 2 -ErrorAction SilentlyContinue }
  514. $_Database.$_Table["$_ItemName"] = $_ItemValue
  515. }
  516. } while ($_Item)
  517. [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($_ItemView)
  518. }
  519. [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($MSI)
  520. [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($windowsInstaller)
  521. Write-Host -ForegroundColor 'Yellow' 'Closing Installer Database. . .'
  522. return $_Database
  523. }
  524. Function Test-IsWix {
  525. Param
  526. (
  527. [Parameter(Mandatory = $true)]
  528. [object] $Database,
  529. [Parameter(Mandatory = $true)]
  530. [object] $MetaDataObject
  531. )
  532. # If any of the table names match wix
  533. if ($Database.Keys -match 'wix') { return $true }
  534. # If any of the keys in the property table match wix
  535. if ($Database.Property.Keys.Where({ $_ -match 'wix' })) { return $true }
  536. # If the CreatedBy value matches wix
  537. if ($MetaDataObject.ProgramName -match 'wix') { return $true }
  538. # If the CreatedBy value matches xml
  539. if ($MetaDataObject.ProgramName -match 'xml') { return $true }
  540. return $false
  541. }
  542. Function Get-UserSavePreference {
  543. switch ($ScriptSettings.SaveToTemporaryFolder) {
  544. 'always' { $_Preference = '0' }
  545. 'never' { $_Preference = '1' }
  546. 'manual' { $_Preference = '2' }
  547. default {
  548. $_menu = @{
  549. entries = @('[Y] Yes'; '*[N] No'; '[M] Manually Enter SHA256')
  550. Prompt = 'Do you want to save the files to the Temp folder?'
  551. DefaultString = 'N'
  552. }
  553. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
  554. 'Y' { $_Preference = '0' }
  555. 'N' { $_Preference = '1' }
  556. 'M' { $_Preference = '2' }
  557. default { $_Preference = '1' }
  558. }
  559. }
  560. }
  561. return $_Preference
  562. }
  563. Function Get-PathInstallerType {
  564. Param
  565. (
  566. [Parameter(Mandatory = $true, Position = 0)]
  567. [string] $Path
  568. )
  569. if ($Path -match '\.msix(bundle){0,1}$') { return 'msix' }
  570. if ($Path -match '\.msi$') {
  571. $ObjectMetadata = Get-ItemMetadata $Path
  572. $ObjectDatabase = Get-MsiDatabase $Path
  573. if (Test-IsWix -Database $ObjectDatabase -MetaDataObject $ObjectMetadata ) {
  574. return 'wix'
  575. }
  576. return 'msi'
  577. }
  578. if ($Path -match '\.appx(bundle){0,1}$') { return 'appx' }
  579. if ($Path -match '\.zip$') { return 'zip' }
  580. return $null
  581. }
  582. Function Get-UriArchitecture {
  583. Param
  584. (
  585. [Parameter(Mandatory = $true, Position = 0)]
  586. [string] $URI
  587. )
  588. if ($URI -match '\b(x|win){0,1}64\b') { return 'x64' }
  589. if ($URI -match '\b((win|ia)32)|(x{0,1}86)\b') { return 'x86' }
  590. if ($URI -match '\b(arm|aarch)64\b') { return 'arm64' }
  591. if ($URI -match '\barm\b') { return 'arm' }
  592. return $null
  593. }
  594. # Prompts the user to enter installer values
  595. # Sets the $script:Installers value as an output
  596. # Returns void
  597. Function Read-InstallerEntry {
  598. $_Installer = [ordered] @{}
  599. # Request user enter Installer URL
  600. $_Installer['InstallerUrl'] = Request-InstallerUrl
  601. if ($_Installer.InstallerUrl -in ($script:Installers).InstallerUrl) {
  602. $_MatchingInstaller = $script:Installers | Where-Object { $_.InstallerUrl -eq $_Installer.InstallerUrl } | Select-Object -First 1
  603. if ($_MatchingInstaller.InstallerSha256) { $_Installer['InstallerSha256'] = $_MatchingInstaller.InstallerSha256 }
  604. if ($_MatchingInstaller.InstallerType) { $_Installer['InstallerType'] = $_MatchingInstaller.InstallerType }
  605. if ($_MatchingInstaller.ProductCode) { $_Installer['ProductCode'] = $_MatchingInstaller.ProductCode }
  606. if ($_MatchingInstaller.PackageFamilyName) { $_Installer['PackageFamilyName'] = $_MatchingInstaller.PackageFamilyName }
  607. if ($_MatchingInstaller.SignatureSha256) { $_Installer['SignatureSha256'] = $_MatchingInstaller.SignatureSha256 }
  608. }
  609. # Get or request Installer Sha256
  610. # Check the settings to see if we need to display this menu
  611. if ($_Installer.Keys -notcontains 'InstallerSha256') {
  612. $script:SaveOption = Get-UserSavePreference
  613. # If user did not select manual entry for Sha256, download file and calculate hash
  614. # Also attempt to detect installer type and architecture
  615. if ($script:SaveOption -ne '2') {
  616. Write-Host
  617. $start_time = Get-Date
  618. Write-Host $NewLine
  619. Write-Host 'Downloading URL. This will take a while...' -ForegroundColor Blue
  620. try {
  621. $script:dest = Get-InstallerFile -URI $_Installer['InstallerUrl'] -PackageIdentifier $PackageIdentifier -PackageVersion $PackageVersion
  622. } catch {
  623. # Here we also want to pass any exceptions through for potential debugging
  624. throw [System.Net.WebException]::new('The file could not be downloaded. Try running the script again', $_.Exception)
  625. } finally {
  626. Write-Host "Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)" -ForegroundColor Green
  627. $_Installer['InstallerSha256'] = (Get-FileHash -Path $script:dest -Algorithm SHA256).Hash
  628. Get-PathInstallerType -Path $script:dest -OutVariable _ | Out-Null
  629. if ($_) { $_Installer['InstallerType'] = $_ | Select-Object -First 1 }
  630. Get-UriArchitecture -URI $_Installer['InstallerUrl'] -OutVariable _ | Out-Null
  631. if ($_) { $_Installer['Architecture'] = $_ | Select-Object -First 1 }
  632. if ([System.Environment]::OSVersion.Platform -match 'Win' -and ($script:dest).EndsWith('.msi')) {
  633. $ProductCode = ([string](Get-MSIProperty -MSIPath $script:dest -Parameter 'ProductCode') | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
  634. }
  635. if (Test-String -Not "$ProductCode" -IsNull) { $_Installer['ProductCode'] = "$ProductCode" }
  636. }
  637. }
  638. # Manual Entry of Sha256 with validation
  639. else {
  640. Write-Host
  641. do {
  642. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  643. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the installer SHA256 Hash'
  644. $_Installer['InstallerSha256'] = Read-Host -Prompt 'InstallerSha256' | TrimString
  645. $_Installer['InstallerSha256'] = $_Installer['InstallerSha256'].toUpper()
  646. if ($_Installer['InstallerSha256'] -match $Patterns.InstallerSha256) {
  647. $script:_returnValue = [ReturnValue]::Success()
  648. } else {
  649. $script:_returnValue = [ReturnValue]::PatternError()
  650. }
  651. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  652. }
  653. }
  654. # Manual Entry of Architecture with validation
  655. do {
  656. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  657. if (Test-String $_Installer['Architecture'] -IsNull) { Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the architecture. Options:' , @($Patterns.ValidArchitectures -join ', ') }
  658. else {
  659. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the architecture. Options:' , @($Patterns.ValidArchitectures -join ', ')
  660. Write-Host -ForegroundColor 'DarkGray' -Object "Old Variable: $($_Installer['Architecture'])"
  661. }
  662. Read-Host -Prompt 'Architecture' -OutVariable _ | Out-Null
  663. if (Test-String $_ -Not -IsNull) { $_Installer['Architecture'] = $_ | TrimString }
  664. if ($_Installer['Architecture'] -Cin @($Patterns.ValidArchitectures)) {
  665. $script:_returnValue = [ReturnValue]::Success()
  666. } else {
  667. $script:_returnValue = [ReturnValue]::new(400, 'Invalid Architecture', "Value must exist in the enum - $(@($Patterns.ValidArchitectures -join ', '))", 2)
  668. }
  669. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  670. # Manual Entry of Installer Type with validation
  671. if ($_Installer['InstallerType'] -CNotIn @($Patterns.ValidInstallerTypes)) {
  672. do {
  673. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  674. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the InstallerType. Options:' , @($Patterns.ValidInstallerTypes -join ', ' )
  675. $_Installer['InstallerType'] = Read-Host -Prompt 'InstallerType' | TrimString
  676. if ($_Installer['InstallerType'] -Cin @($Patterns.ValidInstallerTypes)) {
  677. $script:_returnValue = [ReturnValue]::Success()
  678. } else {
  679. $script:_returnValue = [ReturnValue]::new(400, 'Invalid Installer Type', "Value must exist in the enum - $(@($Patterns.ValidInstallerTypes -join ', '))", 2)
  680. }
  681. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  682. }
  683. $_Switches = [ordered] @{}
  684. # If Installer Type is `exe`, require the silent switches to be entered
  685. do {
  686. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  687. if ($_Installer['InstallerType'] -ieq 'exe') { Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the silent install switch. For example: /S, -verysilent, /qn, --silent, /exenoui' }
  688. else { Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the silent install switch. For example: /S, -verysilent, /qn, --silent, /exenoui' }
  689. Read-Host -Prompt 'Silent switch' -OutVariable _ | Out-Null
  690. if ($_) { $_Switches['Silent'] = $_ | TrimString }
  691. if (Test-String $_Switches['Silent'] -MaxLength $Patterns.SilentSwitchMaxLength -NotNull) {
  692. $script:_returnValue = [ReturnValue]::Success()
  693. } elseif ($_Installer['InstallerType'] -ne 'exe' -and (Test-String $_Switches['Silent'] -MaxLength $Patterns.SilentSwitchMaxLength -AllowNull)) {
  694. $script:_returnValue = [ReturnValue]::Success()
  695. } else {
  696. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.SilentSwitchMaxLength)
  697. }
  698. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  699. do {
  700. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  701. if ($_Installer['InstallerType'] -ieq 'exe') { Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the silent with progress install switch. For example: /S, -silent, /qb, /exebasicui' }
  702. else { Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the silent with progress install switch. For example: /S, -silent, /qb, /exebasicui' }
  703. Read-Host -Prompt 'Silent with progress switch' -OutVariable _ | Out-Null
  704. if ($_) { $_Switches['SilentWithProgress'] = $_ | TrimString }
  705. if (Test-String $_Switches['SilentWithProgress'] -MaxLength $Patterns.ProgressSwitchMaxLength -NotNull) {
  706. $script:_returnValue = [ReturnValue]::Success()
  707. } elseif ($_Installer['InstallerType'] -ne 'exe' -and (Test-String $_Switches['SilentWithProgress'] -MaxLength $Patterns.ProgressSwitchMaxLength -AllowNull)) {
  708. $script:_returnValue = [ReturnValue]::Success()
  709. } else {
  710. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.ProgressSwitchMaxLength)
  711. }
  712. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  713. # Optional entry of `Custom` switches with validation for all installer types
  714. do {
  715. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  716. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter any custom switches for the installer. For example: /norestart, -norestart'
  717. Read-Host -Prompt 'Custom Switch' -OutVariable _ | Out-Null
  718. if ($_) { $_Switches['Custom'] = $_ | TrimString }
  719. if (Test-String $_Switches['Custom'] -MaxLength $Patterns.CustomSwitchMaxLength -AllowNull) {
  720. $script:_returnValue = [ReturnValue]::Success()
  721. } else {
  722. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.CustomSwitchMaxLength)
  723. }
  724. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  725. if ($_Switches.Keys.Count -gt 0) { $_Installer['InstallerSwitches'] = $_Switches }
  726. # If the installer is `msix` or `appx`, prompt for or detect additional fields
  727. if ($_Installer['InstallerType'] -in @('msix'; 'appx')) {
  728. # Detect or prompt for Signature Sha256
  729. if (Get-Command 'winget.exe' -ErrorAction SilentlyContinue) { $SignatureSha256 = winget hash -m $script:dest | Select-String -Pattern 'SignatureSha256:' | ConvertFrom-String; if ($SignatureSha256.P2) { $SignatureSha256 = $SignatureSha256.P2.ToUpper() } }
  730. if ($SignatureSha256) { $_Installer['SignatureSha256'] = $SignatureSha256 }
  731. if (Test-String $_Installer['SignatureSha256'] -IsNull) {
  732. # Manual entry of Signature Sha256 with validation
  733. do {
  734. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  735. Write-Host -ForegroundColor 'Yellow' -Object '[Recommended] Enter the installer SignatureSha256'
  736. Read-Host -Prompt 'SignatureSha256' -OutVariable _ | Out-Null
  737. if ($_) { $_Installer['SignatureSha256'] = $_ | TrimString }
  738. if (Test-String $_Installer['SignatureSha256'] -MatchPattern $Patterns.SignatureSha256 -AllowNull) {
  739. $script:_returnValue = [ReturnValue]::Success()
  740. } else {
  741. $script:_returnValue = [ReturnValue]::PatternError()
  742. }
  743. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  744. }
  745. # Prompt user to find package name automatically, unless package was not downloaded
  746. if ($script:SaveOption -eq '2' -or (!$(Test-Path $script:dest))) {
  747. $ChoicePfn = '1'
  748. } else {
  749. $_menu = @{
  750. entries = @('*[F] Find Automatically [Note: This will install the package to find Family Name and then removes it.]'; '[M] Manually Enter PackageFamilyName')
  751. Prompt = 'Discover the package family name?'
  752. DefaultString = 'F'
  753. }
  754. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
  755. 'M' { $ChoicePfn = '1' }
  756. default { $ChoicePfn = '0' }
  757. }
  758. }
  759. # If user selected to find automatically -
  760. # Install package, get family name, uninstall package
  761. if ($ChoicePfn -eq '0') {
  762. try {
  763. Add-AppxPackage -Path $script:dest
  764. $InstalledPkg = Get-AppxPackage | Select-Object -Last 1 | Select-Object PackageFamilyName, PackageFullName
  765. if ($InstalledPkg.PackageFamilyName) { $_Installer['PackageFamilyName'] = $InstalledPkg.PackageFamilyName }
  766. Remove-AppxPackage $InstalledPkg.PackageFullName
  767. } catch {
  768. # Take no action here, we just want to catch the exceptions as a precaution
  769. Out-Null
  770. } finally {
  771. if (Test-String $_Installer['PackageFamilyName'] -IsNull) {
  772. $script:_returnValue = [ReturnValue]::new(500, 'Could not find PackageFamilyName', 'Value should be entered manually', 1)
  773. }
  774. }
  775. }
  776. # Validate Package Family Name if found automatically
  777. # Allow for manual entry if selected or if validation failed
  778. do {
  779. if (($ChoicePfn -ne '0') -or ($script:_returnValue.StatusCode -ne [ReturnValue]::Success().StatusCode)) {
  780. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  781. Write-Host -ForegroundColor 'Yellow' -Object '[Recommended] Enter the PackageFamilyName'
  782. Read-Host -Prompt 'PackageFamilyName' -OutVariable _ | Out-Null
  783. if ($_) { $_Installer['PackageFamilyName'] = $_ | TrimString }
  784. }
  785. if (Test-String $_Installer['PackageFamilyName'] -MaxLength $Patterns.FamilyNameMaxLength -MatchPattern $Patterns.FamilyName -AllowNull) {
  786. if (Test-String $_Installer['PackageFamilyName'] -IsNull) { $_Installer['PackageFamilyName'] = "$([char]0x2370)" }
  787. $script:_returnValue = [ReturnValue]::Success()
  788. } else {
  789. if (Test-String -not $_Installer['PackageFamilyName'] -MaxLength $Patterns.FamilyNameMaxLength) {
  790. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.FamilyNameMaxLength)
  791. } elseif (Test-String -not $_Installer['PackageFamilyName'] -MatchPattern $Patterns.FamilyName) {
  792. $script:_returnValue = [ReturnValue]::PatternError()
  793. } else {
  794. $script:_returnValue = [ReturnValue]::GenericError()
  795. }
  796. }
  797. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  798. }
  799. # Request installer locale with validation as optional
  800. do {
  801. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  802. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the installer locale. For example: en-US, en-CA'
  803. Write-Host -ForegroundColor 'Blue' -Object 'https://docs.microsoft.com/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a'
  804. Read-Host -Prompt 'InstallerLocale' -OutVariable _
  805. if ($_) { $_Installer['InstallerLocale'] = $_ | TrimString }
  806. # If user defined a default locale, add it
  807. if ((Test-String $_Installer['InstallerLocale'] -IsNull) -and (Test-String -not $ScriptSettings.DefaultInstallerLocale -IsNull)) { $_Installer['InstallerLocale'] = $ScriptSettings.DefaultInstallerLocale }
  808. if (Test-String $_Installer['InstallerLocale'] -MaxLength $Patterns.InstallerLocaleMaxLength -MatchPattern $Patterns.PackageLocale -AllowNull) {
  809. $script:_returnValue = [ReturnValue]::Success()
  810. } else {
  811. if (Test-String -not $_Installer['InstallerLocale'] -MaxLength $Patterns.InstallerLocaleMaxLength -AllowNull) {
  812. $script:_returnValue = [ReturnValue]::LengthError(0, $Patterns.InstallerLocaleMaxLength)
  813. } elseif (Test-String -not $_Installer['InstallerLocale'] -MatchPattern $Patterns.PackageLocale) {
  814. $script:_returnValue = [ReturnValue]::PatternError()
  815. } else {
  816. $script:_returnValue = [ReturnValue]::GenericError()
  817. }
  818. }
  819. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  820. # Request product code with validation
  821. do {
  822. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  823. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application product code. Looks like {CF8E6E00-9C03-4440-81C0-21FACB921A6B}'
  824. Write-Host -ForegroundColor 'White' -Object "ProductCode found from installer: $($_Installer['ProductCode'])"
  825. 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'
  826. $NewProductCode = Read-Host -Prompt 'ProductCode' | TrimString
  827. if (Test-String $NewProductCode -Not -IsNull) { $_Installer['ProductCode'] = $NewProductCode }
  828. elseif (Test-String $_Installer['ProductCode'] -Not -IsNull) { $_Installer['ProductCode'] = "$($_Installer['ProductCode'])" }
  829. if (Test-String $_Installer['ProductCode'] -MinLength $Patterns.ProductCodeMinLength -MaxLength $Patterns.ProductCodeMaxLength -AllowNull) {
  830. $script:_returnValue = [ReturnValue]::Success()
  831. } else {
  832. $script:_returnValue = [ReturnValue]::LengthError($Patterns.ProductCodeMinLength, $Patterns.ProductCodeMaxLength)
  833. }
  834. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  835. # Request installer scope
  836. $_menu = @{
  837. entries = @('[M] Machine'; '[U] User'; '*[N] No idea')
  838. Prompt = '[Optional] Enter the Installer Scope'
  839. DefaultString = 'N'
  840. }
  841. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
  842. 'M' { $_Installer['Scope'] = 'machine' }
  843. 'U' { $_Installer['Scope'] = 'user' }
  844. default { }
  845. }
  846. # Request upgrade behavior
  847. $_menu = @{
  848. entries = @('*[I] Install'; '[U] Uninstall Previous')
  849. Prompt = '[Optional] Enter the Upgrade Behavior'
  850. DefaultString = 'I'
  851. }
  852. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
  853. 'U' { $_Installer['UpgradeBehavior'] = 'uninstallPrevious' }
  854. default { $_Installer['UpgradeBehavior'] = 'install' }
  855. }
  856. Write-Host
  857. # Request release date
  858. $script:ReleaseDatePrompted = $true
  859. do {
  860. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  861. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application release date. Example: 2021-11-17'
  862. Read-Host -Prompt 'ReleaseDate' -OutVariable ReleaseDate | Out-Null
  863. try {
  864. Get-Date([datetime]$($ReleaseDate | TrimString)) -f 'yyyy-MM-dd' -OutVariable _ValidDate | Out-Null
  865. if ($_ValidDate) { $_Installer['ReleaseDate'] = $_ValidDate | TrimString }
  866. $script:_returnValue = [ReturnValue]::Success()
  867. } catch {
  868. if (Test-String $ReleaseDate -IsNull) {
  869. $script:_returnValue = [ReturnValue]::Success()
  870. } else {
  871. $script:_returnValue = [ReturnValue]::new(400, 'Invalid Date', 'Input could not be resolved to a date', 2)
  872. }
  873. }
  874. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  875. if ($script:SaveOption -eq '1' -and (Test-Path -Path $script:dest)) { Remove-Item -Path $script:dest }
  876. # If the installers array is empty, create it
  877. if (!$script:Installers) {
  878. $script:Installers = @()
  879. }
  880. # Add the completed installer to the installers array
  881. $_Installer = Restore-YamlKeyOrder $_Installer $InstallerEntryProperties -NoComments
  882. $script:Installers += $_Installer
  883. # Prompt the user for additional intaller entries
  884. $_menu = @{
  885. entries = @(
  886. '[Y] Yes'
  887. '*[N] No'
  888. )
  889. Prompt = 'Do you want to create another installer?'
  890. DefaultString = 'N'
  891. }
  892. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
  893. 'Y' { $AnotherInstaller = '0' }
  894. 'N' { $AnotherInstaller = '1' }
  895. default { $AnotherInstaller = '1' }
  896. }
  897. # If there are additional entries, run this function again to fetch the values and add them to the installers array
  898. if ($AnotherInstaller -eq '0') {
  899. Write-Host; Read-InstallerEntry
  900. }
  901. }
  902. # Prompts user for Installer Values using the `Quick Update` Method
  903. # Sets the $script:Installers value as an output
  904. # Returns void
  905. Function Read-QuickInstallerEntry {
  906. # We know old manifests exist if we got here without error
  907. # Fetch the old installers based on the manifest type
  908. if ($script:OldInstallerManifest) { $_OldInstallers = $script:OldInstallerManifest['Installers'] } else {
  909. $_OldInstallers = $script:OldVersionManifest['Installers']
  910. }
  911. $_iteration = 0
  912. $_NewInstallers = @()
  913. foreach ($_OldInstaller in $_OldInstallers) {
  914. # Create the new installer as an exact copy of the old installer entry
  915. # This is to ensure all previously entered and un-modified parameters are retained
  916. $_iteration += 1
  917. $_NewInstaller = $_OldInstaller
  918. $_NewInstaller.Remove('InstallerSha256');
  919. # Show the user which installer entry they should be entering information for
  920. Write-Host -ForegroundColor 'Green' "Installer Entry #$_iteration`:`n"
  921. if ($_OldInstaller.InstallerLocale) { Write-Host -ForegroundColor 'Yellow' "`tInstallerLocale: $($_OldInstaller.InstallerLocale)" }
  922. if ($_OldInstaller.Architecture) { Write-Host -ForegroundColor 'Yellow' "`tArchitecture: $($_OldInstaller.Architecture)" }
  923. if ($_OldInstaller.InstallerType) { Write-Host -ForegroundColor 'Yellow' "`tInstallerType: $($_OldInstaller.InstallerType)" }
  924. if ($_OldInstaller.Scope) { Write-Host -ForegroundColor 'Yellow' "`tScope: $($_OldInstaller.Scope)" }
  925. Write-Host
  926. # Request user enter the new Installer URL
  927. $_NewInstaller['InstallerUrl'] = Request-InstallerUrl
  928. if ($_NewInstaller.InstallerUrl -in ($_NewInstallers).InstallerUrl) {
  929. $_MatchingInstaller = $_NewInstallers | Where-Object { $_.InstallerUrl -eq $_NewInstaller.InstallerUrl } | Select-Object -First 1
  930. if ($_MatchingInstaller.InstallerSha256) { $_NewInstaller['InstallerSha256'] = $_MatchingInstaller.InstallerSha256 }
  931. if ($_MatchingInstaller.InstallerType) { $_NewInstaller['InstallerType'] = $_MatchingInstaller.InstallerType }
  932. if ($_MatchingInstaller.ProductCode) { $_NewInstaller['ProductCode'] = $_MatchingInstaller.ProductCode }
  933. elseif ( ($_NewInstaller.Keys -contains 'ProductCode') -and ($script:dest -notmatch '.exe$')) { $_NewInstaller.Remove('ProductCode') }
  934. if ($_MatchingInstaller.PackageFamilyName) { $_NewInstaller['PackageFamilyName'] = $_MatchingInstaller.PackageFamilyName }
  935. elseif ($_NewInstaller.Keys -contains 'PackageFamilyName') { $_NewInstaller.Remove('PackageFamilyName') }
  936. if ($_MatchingInstaller.SignatureSha256) { $_NewInstaller['SignatureSha256'] = $_MatchingInstaller.SignatureSha256 }
  937. elseif ($_NewInstaller.Keys -contains 'SignatureSha256') { $_NewInstaller.Remove('SignatureSha256') }
  938. }
  939. if ($_NewInstaller.Keys -notcontains 'InstallerSha256') {
  940. try {
  941. Write-Host -ForegroundColor 'Green' 'Downloading Installer. . .'
  942. $script:dest = Get-InstallerFile -URI $_NewInstaller['InstallerUrl'] -PackageIdentifier $PackageIdentifier -PackageVersion $PackageVersion
  943. } catch {
  944. # Here we also want to pass any exceptions through for potential debugging
  945. throw [System.Net.WebException]::new('The file could not be downloaded. Try running the script again', $_.Exception)
  946. } finally {
  947. # Check that MSI's aren't actually WIX
  948. Write-Host -ForegroundColor 'Green' "Installer Downloaded!`nProcessing installer data. . . "
  949. if ($_NewInstaller['InstallerType'] -eq 'msi') {
  950. $DetectedType = Get-PathInstallerType $script:dest
  951. if ($DetectedType -in @('msi'; 'wix')) { $_NewInstaller['InstallerType'] = $DetectedType }
  952. }
  953. # Get the Sha256
  954. $_NewInstaller['InstallerSha256'] = (Get-FileHash -Path $script:dest -Algorithm SHA256).Hash
  955. # Update the product code, if a new one exists
  956. # If a new product code doesn't exist, and the installer isn't an `.exe` file, remove the product code if it exists
  957. $MSIProductCode = $null
  958. if ([System.Environment]::OSVersion.Platform -match 'Win' -and ($script:dest).EndsWith('.msi')) {
  959. $MSIProductCode = ([string](Get-MSIProperty -MSIPath $script:dest -Parameter 'ProductCode') | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
  960. }
  961. if (Test-String -not $MSIProductCode -IsNull) {
  962. $_NewInstaller['ProductCode'] = $MSIProductCode
  963. } elseif ( ($_NewInstaller.Keys -contains 'ProductCode') -and ($_NewInstaller.InstallerType -in @('appx'; 'msi'; 'msix'; 'wix'; 'burn'))) {
  964. $_NewInstaller.Remove('ProductCode')
  965. }
  966. # If the installer is msix or appx, try getting the new SignatureSha256
  967. # If the new SignatureSha256 can't be found, remove it if it exists
  968. $NewSignatureSha256 = $null
  969. if ($_NewInstaller.InstallerType -in @('msix', 'appx')) {
  970. if (Get-Command 'winget.exe' -ErrorAction SilentlyContinue) { $NewSignatureSha256 = winget hash -m $script:dest | Select-String -Pattern 'SignatureSha256:' | ConvertFrom-String; if ($NewSignatureSha256.P2) { $NewSignatureSha256 = $NewSignatureSha256.P2.ToUpper() } }
  971. }
  972. if (Test-String -not $NewSignatureSha256 -IsNull) {
  973. $_NewInstaller['SignatureSha256'] = $NewSignatureSha256
  974. } elseif ($_NewInstaller.Keys -contains 'SignatureSha256') {
  975. $_NewInstaller.Remove('SignatureSha256')
  976. }
  977. # If the installer is msix or appx, try getting the new package family name
  978. # If the new package family name can't be found, remove it if it exists
  979. if ($script:dest -match '\.(msix|appx)(bundle){0,1}$') {
  980. try {
  981. Add-AppxPackage -Path $script:dest
  982. $InstalledPkg = Get-AppxPackage | Select-Object -Last 1 | Select-Object PackageFamilyName, PackageFullName
  983. $PackageFamilyName = $InstalledPkg.PackageFamilyName
  984. Remove-AppxPackage $InstalledPkg.PackageFullName
  985. } catch {
  986. # Take no action here, we just want to catch the exceptions as a precaution
  987. Out-Null
  988. } finally {
  989. if (Test-String -not $PackageFamilyName -IsNull) {
  990. $_NewInstaller['PackageFamilyName'] = $PackageFamilyName
  991. } elseif ($_NewInstaller.Keys -contains 'PackageFamilyName') {
  992. $_NewInstaller.Remove('PackageFamilyName')
  993. }
  994. }
  995. }
  996. # Remove the downloaded files
  997. Remove-Item -Path $script:dest
  998. Write-Host -ForegroundColor 'Green' "Installer updated!`n"
  999. }
  1000. }
  1001. #Add the updated installer to the new installers array
  1002. $_NewInstaller = Restore-YamlKeyOrder $_NewInstaller $InstallerEntryProperties -NoComments
  1003. $_NewInstallers += $_NewInstaller
  1004. }
  1005. $script:Installers = $_NewInstallers
  1006. }
  1007. # Requests the user enter an optional value with a prompt
  1008. # If the value already exists, also print the existing value
  1009. # Returns the new value if entered, Returns the existing value if no new value was entered
  1010. Function Read-InstallerMetadataValue {
  1011. Param
  1012. (
  1013. [Parameter(Mandatory = $true, Position = 0)]
  1014. [AllowNull()]
  1015. [PSCustomObject] $Variable,
  1016. [Parameter(Mandatory = $true, Position = 1)]
  1017. [string] $Key,
  1018. [Parameter(Mandatory = $true, Position = 2)]
  1019. [string] $Prompt
  1020. )
  1021. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1022. Write-Host -ForegroundColor 'Yellow' -Object $Prompt
  1023. if (Test-String -not $Variable -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $Variable" }
  1024. $NewValue = Read-Host -Prompt $Key | TrimString
  1025. if (Test-String -not $NewValue -IsNull) {
  1026. return $NewValue
  1027. } else {
  1028. return $Variable
  1029. }
  1030. }
  1031. # Sorts keys within an object based on a reference ordered dictionary
  1032. # If a key does not exist, it sets the value to a special character to be removed / commented later
  1033. # Returns the result as a new object
  1034. Function Restore-YamlKeyOrder {
  1035. Param
  1036. (
  1037. [Parameter(Mandatory = $true, Position = 0)]
  1038. [PSCustomObject] $InputObject,
  1039. [Parameter(Mandatory = $true, Position = 1)]
  1040. [PSCustomObject] $SortOrder,
  1041. [switch] $NoComments
  1042. )
  1043. $_ExcludedKeys = @(
  1044. 'InstallerSwitches'
  1045. 'Capabilities'
  1046. 'RestrictedCapabilities'
  1047. 'InstallerSuccessCodes'
  1048. 'ProductCode'
  1049. 'PackageFamilyName'
  1050. 'InstallerLocale'
  1051. 'InstallerType'
  1052. 'Scope'
  1053. 'UpgradeBehavior'
  1054. 'Dependencies'
  1055. )
  1056. $_Temp = [ordered] @{}
  1057. $SortOrder.GetEnumerator() | ForEach-Object {
  1058. if ($InputObject.Contains($_)) {
  1059. $_Temp.Add($_, $InputObject[$_])
  1060. } else {
  1061. if (!$NoComments -and $_ -notin $_ExcludedKeys) {
  1062. $_Temp.Add($_, "$([char]0x2370)")
  1063. }
  1064. }
  1065. }
  1066. return $_Temp
  1067. }
  1068. # Requests the user to input optional values for the Installer Manifest file
  1069. Function Read-InstallerMetadata {
  1070. Write-Host
  1071. # Request File Extensions and validate
  1072. do {
  1073. if (!$FileExtensions) { $FileExtensions = '' }
  1074. else { $FileExtensions = $FileExtensions | ToLower | UniqueItems }
  1075. $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
  1076. 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) {
  1077. $script:_returnValue = [ReturnValue]::Success()
  1078. } else {
  1079. if (($script:FileExtensions -split ',').Count -gt $Patterns.MaxItemsFileExtensions ) {
  1080. $script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsFileExtensions)
  1081. } else {
  1082. $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)
  1083. }
  1084. }
  1085. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1086. # Request Protocols and validate
  1087. do {
  1088. if (!$Protocols) { $Protocols = '' }
  1089. else { $Protocols = $Protocols | ToLower | UniqueItems }
  1090. $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
  1091. if (($script:Protocols -split ',').Count -le $Patterns.MaxItemsProtocols) {
  1092. $script:_returnValue = [ReturnValue]::Success()
  1093. } else {
  1094. $script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsProtocols)
  1095. }
  1096. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1097. # Request Commands and validate
  1098. do {
  1099. if (!$Commands) { $Commands = '' }
  1100. else { $Commands = $Commands | UniqueItems }
  1101. $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
  1102. if (($script:Commands -split ',').Count -le $Patterns.MaxItemsCommands) {
  1103. $script:_returnValue = [ReturnValue]::Success()
  1104. } else {
  1105. $script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsCommands)
  1106. }
  1107. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1108. # Request Installer Success Codes and validate
  1109. do {
  1110. if (!$InstallerSuccessCodes) { $InstallerSuccessCodes = '' }
  1111. $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
  1112. if (($script:InstallerSuccessCodes -split ',').Count -le $Patterns.MaxItemsSuccessCodes) {
  1113. $script:_returnValue = [ReturnValue]::Success()
  1114. try {
  1115. #Ensure all values are integers
  1116. $script:InstallerSuccessCodes.Split(',').Trim() | ForEach-Object { [long]$_ }
  1117. $script:_returnValue = [ReturnValue]::Success()
  1118. } catch {
  1119. $script:_returnValue = [ReturnValue]::new(400, 'Invalid Data Type', 'The value entered does not match the type requirements defined in the manifest schema', 2)
  1120. }
  1121. } else {
  1122. $script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsSuccessCodes)
  1123. }
  1124. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1125. # Request Install Modes and validate
  1126. do {
  1127. if ($script:InstallModes) { $script:InstallModes = $script:InstallModes | UniqueItems }
  1128. $script:InstallModes = Read-InstallerMetadataValue -Variable $script:InstallModes -Key 'InstallModes' -Prompt "[Optional] List of supported installer modes. Options: $($Patterns.ValidInstallModes -join ', ')"
  1129. if ($script:InstallModes) { $script:InstallModes = $script:InstallModes | UniqueItems }
  1130. 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)) {
  1131. $script:_returnValue = [ReturnValue]::Success()
  1132. } else {
  1133. if (($script:InstallModes -split ',').Count -gt $Patterns.MaxItemsInstallModes ) {
  1134. $script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.MaxItemsInstallModes)
  1135. } else {
  1136. $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)
  1137. }
  1138. }
  1139. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1140. }
  1141. # Requests the user to input values for the Locale Manifest file
  1142. Function Read-LocaleMetadata {
  1143. # Request Package Locale and Validate
  1144. if (Test-String -not $script:PackageLocale -MaxLength $Patterns.PackageLocaleMaxLength -MatchPattern $Patterns.PackageLocale -NotNull) {
  1145. do {
  1146. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1147. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the Package Locale. For example: en-US, en-CA'
  1148. Write-Host -ForegroundColor 'Blue' 'Reference Link: https://docs.microsoft.com/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a'
  1149. $script:PackageLocale = Read-Host -Prompt 'PackageLocale' | TrimString
  1150. if (Test-String $script:PackageLocale -MaxLength $Patterns.PackageLocaleMaxLength -MatchPattern $Patterns.PackageLocale -NotNull) {
  1151. $script:_returnValue = [ReturnValue]::Success()
  1152. } else {
  1153. if (Test-String $script:PackageLocale -not -MaxLength $Patterns.PackageLocaleMaxLength -NotNull) {
  1154. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.PackageLocaleMaxLength)
  1155. } elseif (Test-String $script:PackageLocale -not -MatchPattern $Patterns.PackageLocale ) {
  1156. $script:_returnValue = [ReturnValue]::PatternError()
  1157. } else {
  1158. $script:_returnValue = [ReturnValue]::GenericError()
  1159. }
  1160. }
  1161. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1162. }
  1163. # Request Publisher Name and Validate
  1164. do {
  1165. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1166. if (Test-String $script:Publisher -IsNull) {
  1167. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the full publisher name. For example: Microsoft Corporation'
  1168. } else {
  1169. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the full publisher name. For example: Microsoft Corporation'
  1170. Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Publisher"
  1171. }
  1172. $NewPublisher = Read-Host -Prompt 'Publisher' | TrimString
  1173. if (Test-String $NewPublisher -NotNull) {
  1174. $script:Publisher = $NewPublisher
  1175. }
  1176. if (Test-String $script:Publisher -MaxLength $Patterns.PublisherMaxLength -NotNull) {
  1177. $script:_returnValue = [ReturnValue]::Success()
  1178. } else {
  1179. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.PublisherMaxLength)
  1180. }
  1181. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1182. # Request Application Name and Validate
  1183. do {
  1184. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1185. if (Test-String $script:PackageName -IsNull) {
  1186. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the full application name. For example: Microsoft Teams'
  1187. } else {
  1188. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the full application name. For example: Microsoft Teams'
  1189. Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PackageName"
  1190. }
  1191. $NewPackageName = Read-Host -Prompt 'PackageName' | TrimString
  1192. if (Test-String -not $NewPackageName -IsNull) { $script:PackageName = $NewPackageName }
  1193. if (Test-String $script:PackageName -MaxLength $Patterns.PackageNameMaxLength -NotNull) {
  1194. $script:_returnValue = [ReturnValue]::Success()
  1195. } else {
  1196. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.PackageNameMaxLength)
  1197. }
  1198. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1199. # If the option is `NewLocale` then these moniker should already exist
  1200. # If the option is not `NewLocale`, Request Moniker and Validate
  1201. if ($Option -ne 'NewLocale') {
  1202. do {
  1203. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1204. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Moniker (friendly name/alias). For example: vscode'
  1205. if (Test-String -not $script:Moniker -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Moniker" }
  1206. $NewMoniker = Read-Host -Prompt 'Moniker' | ToLower | TrimString | NoWhitespace
  1207. if (Test-String -not $NewMoniker -IsNull) { $script:Moniker = $NewMoniker }
  1208. if (Test-String $script:Moniker -MaxLength $Patterns.MonikerMaxLength -AllowNull) {
  1209. $script:_returnValue = [ReturnValue]::Success()
  1210. } else {
  1211. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.MonikerMaxLength)
  1212. }
  1213. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1214. }
  1215. #Request Publisher URL and Validate
  1216. do {
  1217. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1218. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Publisher Url.'
  1219. if (Test-String -not $script:PublisherUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PublisherUrl" }
  1220. $NewPublisherUrl = Read-Host -Prompt 'Publisher Url' | TrimString
  1221. if (Test-String -not $NewPublisherUrl -IsNull) { $script:PublisherUrl = $NewPublisherUrl }
  1222. if (Test-String $script:PublisherUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
  1223. $script:_returnValue = [ReturnValue]::Success()
  1224. } else {
  1225. if (Test-String -not $script:PublisherUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
  1226. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
  1227. } elseif (Test-String -not $script:PublisherUrl -MatchPattern $Patterns.GenericUrl) {
  1228. $script:_returnValue = [ReturnValue]::PatternError()
  1229. } else {
  1230. $script:_returnValue = [ReturnValue]::GenericError()
  1231. }
  1232. }
  1233. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1234. # Request Publisher Support URL and Validate
  1235. do {
  1236. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1237. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Publisher Support Url.'
  1238. if (Test-String -not $script:PublisherSupportUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PublisherSupportUrl" }
  1239. $NewPublisherSupportUrl = Read-Host -Prompt 'Publisher Support Url' | TrimString
  1240. if (Test-String -not $NewPublisherSupportUrl -IsNull) { $script:PublisherSupportUrl = $NewPublisherSupportUrl }
  1241. if (Test-String $script:PublisherSupportUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
  1242. $script:_returnValue = [ReturnValue]::Success()
  1243. } else {
  1244. if (Test-String -not $script:PublisherSupportUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
  1245. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
  1246. } elseif (Test-String -not $script:PublisherSupportUrl -MatchPattern $Patterns.GenericUrl) {
  1247. $script:_returnValue = [ReturnValue]::PatternError()
  1248. } else {
  1249. $script:_returnValue = [ReturnValue]::GenericError()
  1250. }
  1251. }
  1252. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1253. # Request Publisher Privacy URL and Validate
  1254. do {
  1255. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1256. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Publisher Privacy Url.'
  1257. if (Test-String -not $script:PrivacyUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PrivacyUrl" }
  1258. $NewPrivacyUrl = Read-Host -Prompt 'Publisher Privacy Url' | TrimString
  1259. if (Test-String -not $NewPrivacyUrl -IsNull) { $script:PrivacyUrl = $NewPrivacyUrl }
  1260. if (Test-String $script:PrivacyUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
  1261. $script:_returnValue = [ReturnValue]::Success()
  1262. } else {
  1263. if (Test-String -not $script:PrivacyUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
  1264. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
  1265. } elseif (Test-String -not $script:PrivacyUrl -MatchPattern $Patterns.GenericUrl) {
  1266. $script:_returnValue = [ReturnValue]::PatternError()
  1267. } else {
  1268. $script:_returnValue = [ReturnValue]::GenericError()
  1269. }
  1270. }
  1271. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1272. # Request Author and Validate
  1273. do {
  1274. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1275. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application Author.'
  1276. if (Test-String -not $script:Author -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Author" }
  1277. $NewAuthor = Read-Host -Prompt 'Author' | TrimString
  1278. if (Test-String -not $NewAuthor -IsNull) { $script:Author = $NewAuthor }
  1279. if (Test-String $script:Author -MinLength $Patterns.AuthorMinLength -MaxLength $Patterns.AuthorMaxLength -AllowNull) {
  1280. $script:_returnValue = [ReturnValue]::Success()
  1281. } else {
  1282. $script:_returnValue = [ReturnValue]::LengthError($Patterns.AuthorMinLength, $Patterns.AuthorMaxLength)
  1283. }
  1284. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1285. # Request Package URL and Validate
  1286. do {
  1287. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1288. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the Url to the homepage of the application.'
  1289. if (Test-String -not $script:PackageUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:PackageUrl" }
  1290. $NewPackageUrl = Read-Host -Prompt 'Homepage' | TrimString
  1291. if (Test-String -not $NewPackageUrl -IsNull) { $script:PackageUrl = $NewPackageUrl }
  1292. if (Test-String $script:PackageUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
  1293. $script:_returnValue = [ReturnValue]::Success()
  1294. } else {
  1295. if (Test-String -not $script:PackageUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
  1296. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
  1297. } elseif (Test-String -not $script:PackageUrl -MatchPattern $Patterns.GenericUrl) {
  1298. $script:_returnValue = [ReturnValue]::PatternError()
  1299. } else {
  1300. $script:_returnValue = [ReturnValue]::GenericError()
  1301. }
  1302. }
  1303. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1304. # Request License and Validate
  1305. do {
  1306. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1307. if (Test-String $script:License -IsNull) {
  1308. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the application License. For example: MIT, GPL, Freeware, Proprietary'
  1309. } else {
  1310. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application License. For example: MIT, GPL, Freeware, Proprietary'
  1311. Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:License"
  1312. }
  1313. $NewLicense = Read-Host -Prompt 'License' | TrimString
  1314. if (Test-String -not $NewLicense -IsNull) { $script:License = $NewLicense }
  1315. if (Test-String $script:License -MinLength $Patterns.LicenseMinLength -MaxLength $Patterns.LicenseMaxLength -NotNull) {
  1316. $script:_returnValue = [ReturnValue]::Success()
  1317. } elseif (Test-String $script:License -IsNull) {
  1318. $script:_returnValue = [ReturnValue]::new(400, 'Required Field', 'The value entered cannot be null or empty', 2)
  1319. } else {
  1320. $script:_returnValue = [ReturnValue]::LengthError($Patterns.LicenseMinLength, $Patterns.LicenseMaxLength)
  1321. }
  1322. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1323. # Request License URL and Validate
  1324. do {
  1325. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1326. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application License URL.'
  1327. if (Test-String -not $script:LicenseUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:LicenseUrl" }
  1328. $NewLicenseUrl = Read-Host -Prompt 'License URL' | TrimString
  1329. if (Test-String -not $NewLicenseUrl -IsNull) { $script:LicenseUrl = $NewLicenseUrl }
  1330. if (Test-String $script:LicenseUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
  1331. $script:_returnValue = [ReturnValue]::Success()
  1332. } else {
  1333. if (Test-String -not $script:LicenseUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
  1334. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
  1335. } elseif (Test-String -not $script:LicenseUrl -MatchPattern $Patterns.GenericUrl) {
  1336. $script:_returnValue = [ReturnValue]::PatternError()
  1337. } else {
  1338. $script:_returnValue = [ReturnValue]::GenericError()
  1339. }
  1340. }
  1341. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1342. # Request Copyright and Validate
  1343. do {
  1344. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1345. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application Copyright.'
  1346. Write-Host -ForegroundColor 'Blue' 'Example: Copyright (c) Microsoft Corporation'
  1347. if (Test-String -not $script:Copyright -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Copyright" }
  1348. $NewCopyright = Read-Host -Prompt 'Copyright' | TrimString
  1349. if (Test-String -not $NewCopyright -IsNull) { $script:Copyright = $NewCopyright }
  1350. if (Test-String $script:Copyright -MinLength $Patterns.CopyrightMinLength -MaxLength $Patterns.CopyrightMaxLength -AllowNull) {
  1351. $script:_returnValue = [ReturnValue]::Success()
  1352. } else {
  1353. $script:_returnValue = [ReturnValue]::LengthError($Patterns.CopyrightMinLength, $Patterns.CopyrightMaxLength)
  1354. }
  1355. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1356. # Request Copyright URL and Validate
  1357. do {
  1358. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1359. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the application Copyright Url.'
  1360. if (Test-String -not $script:CopyrightUrl -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:CopyrightUrl" }
  1361. $NewCopyrightUrl = Read-Host -Prompt 'CopyrightUrl' | TrimString
  1362. if (Test-String -not $NewCopyrightUrl -IsNull) { $script:CopyrightUrl = $NewCopyrightUrl }
  1363. if (Test-String $script:CopyrightUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
  1364. $script:_returnValue = [ReturnValue]::Success()
  1365. } else {
  1366. if (Test-String -not $script:CopyrightUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
  1367. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
  1368. } elseif (Test-String -not $script:CopyrightUrl -MatchPattern $Patterns.GenericUrl) {
  1369. $script:_returnValue = [ReturnValue]::PatternError()
  1370. } else {
  1371. $script:_returnValue = [ReturnValue]::GenericError()
  1372. }
  1373. }
  1374. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1375. # Request Tags and Validate
  1376. do {
  1377. $script:Tags = [string]$script:Tags
  1378. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1379. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter any tags that would be useful to discover this tool.'
  1380. Write-Host -ForegroundColor 'Blue' -Object 'Example: zip, c++, photos, OBS (Max', ($Patterns.TagsMaxItems), 'items)'
  1381. if (Test-String -not $script:Tags -IsNull) {
  1382. $script:Tags = $script:Tags | ToLower | UniqueItems
  1383. Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Tags"
  1384. }
  1385. $NewTags = Read-Host -Prompt 'Tags' | TrimString | ToLower | UniqueItems
  1386. if (Test-String -not $NewTags -IsNull) { $script:Tags = $NewTags }
  1387. if (($script:Tags -split ',').Count -le $Patterns.TagsMaxItems) {
  1388. $script:_returnValue = [ReturnValue]::Success()
  1389. } else {
  1390. $script:_returnValue = [ReturnValue]::MaxItemsError($Patterns.TagsMaxItems)
  1391. }
  1392. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1393. # Request Short Description and Validate
  1394. do {
  1395. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1396. if (Test-String $script:ShortDescription -IsNull) {
  1397. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter a short description of the application.'
  1398. } else {
  1399. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter a short description of the application.'
  1400. Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:ShortDescription"
  1401. }
  1402. $NewShortDescription = Read-Host -Prompt 'Short Description' | TrimString
  1403. if (Test-String -not $NewShortDescription -IsNull) { $script:ShortDescription = $NewShortDescription }
  1404. if (Test-String $script:ShortDescription -MaxLength $Patterns.ShortDescriptionMaxLength -NotNull) {
  1405. $script:_returnValue = [ReturnValue]::Success()
  1406. } else {
  1407. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.ShortDescriptionMaxLength)
  1408. }
  1409. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1410. # Request Long Description and Validate
  1411. do {
  1412. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1413. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter a long description of the application.'
  1414. if (Test-String -not $script:Description -IsNull) { Write-Host -ForegroundColor 'DarkGray' "Old Variable: $script:Description" }
  1415. $NewDescription = Read-Host -Prompt 'Description' | TrimString
  1416. if (Test-String -not $NewDescription -IsNull) { $script:Description = $NewDescription }
  1417. if (Test-String $script:Description -MinLength $Patterns.DescriptionMinLength -MaxLength $Patterns.DescriptionMaxLength -AllowNull) {
  1418. $script:_returnValue = [ReturnValue]::Success()
  1419. } else {
  1420. $script:_returnValue = [ReturnValue]::LengthError($Patterns.DescriptionMinLength, $Patterns.DescriptionMaxLength)
  1421. }
  1422. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1423. # Request ReleaseNotes and Validate
  1424. do {
  1425. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1426. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter release notes for this version of the package.'
  1427. $script:ReleaseNotes = Read-Host -Prompt 'ReleaseNotes' | TrimString
  1428. if (Test-String $script:ReleaseNotes -MinLength $Patterns.ReleaseNotesMinLength -MaxLength $Patterns.ReleaseNotesMaxLength -AllowNull) {
  1429. $script:_returnValue = [ReturnValue]::Success()
  1430. } else {
  1431. $script:_returnValue = [ReturnValue]::LengthError($Patterns.ReleaseNotesMinLength, $Patterns.ReleaseNotesMaxLength)
  1432. }
  1433. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1434. # Request ReleaseNotes URL and Validate
  1435. do {
  1436. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1437. Write-Host -ForegroundColor 'Yellow' -Object '[Optional] Enter the release notes URL for this version of the package.'
  1438. $script:ReleaseNotesUrl = Read-Host -Prompt 'ReleaseNotesUrl' | TrimString
  1439. if (Test-String $script:ReleaseNotesUrl -MaxLength $Patterns.GenericUrlMaxLength -MatchPattern $Patterns.GenericUrl -AllowNull) {
  1440. $script:_returnValue = [ReturnValue]::Success()
  1441. } else {
  1442. if (Test-String -not $script:ReleaseNotesUrl -MaxLength $Patterns.GenericUrlMaxLength -AllowNull) {
  1443. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.GenericUrlMaxLength)
  1444. } elseif (Test-String -not $script:ReleaseNotesUrl -MatchPattern $Patterns.GenericUrl) {
  1445. $script:_returnValue = [ReturnValue]::PatternError()
  1446. } else {
  1447. $script:_returnValue = [ReturnValue]::GenericError()
  1448. }
  1449. }
  1450. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1451. }
  1452. # Requests the user to answer the prompts found in the winget-pkgs pull request template
  1453. # Uses this template and responses to create a PR
  1454. Function Read-PRBody {
  1455. $PrBodyContent = Get-Content $args[0]
  1456. ForEach ($_line in ($PrBodyContent | Where-Object { $_ -like '-*[ ]*' })) {
  1457. $_showMenu = $true
  1458. switch -Wildcard ( $_line ) {
  1459. '*CLA*' {
  1460. if ($ScriptSettings.SignedCLA -eq 'true') {
  1461. $PrBodyContentReply += @($_line.Replace('[ ]', '[X]'))
  1462. $_showMenu = $false
  1463. } else {
  1464. $_menu = @{
  1465. Prompt = 'Have you signed the Contributor License Agreement (CLA)?'
  1466. Entries = @('[Y] Yes'; '*[N] No')
  1467. HelpText = 'Reference Link: https://cla.opensource.microsoft.com/microsoft/winget-pkgs'
  1468. HelpTextColor = ''
  1469. DefaultString = 'N'
  1470. }
  1471. }
  1472. }
  1473. '*open `[pull requests`]*' {
  1474. $_menu = @{
  1475. Prompt = "Have you checked that there aren't other open pull requests for the same manifest update/change?"
  1476. Entries = @('[Y] Yes'; '*[N] No')
  1477. HelpText = 'Reference Link: https://github.com/microsoft/winget-pkgs/pulls'
  1478. HelpTextColor = ''
  1479. DefaultString = 'N'
  1480. }
  1481. }
  1482. '*winget validate*' {
  1483. if ($?) {
  1484. $PrBodyContentReply += @($_line.Replace('[ ]', '[X]'))
  1485. $_showMenu = $false
  1486. } else {
  1487. $_menu = @{
  1488. Prompt = "Have you validated your manifest locally with 'winget validate --manifest <path>'?"
  1489. Entries = @('[Y] Yes'; '*[N] No')
  1490. HelpText = 'Automatic manifest validation failed. Check your manifest and try again'
  1491. HelpTextColor = 'Red'
  1492. DefaultString = 'N'
  1493. }
  1494. }
  1495. }
  1496. '*tested your manifest*' {
  1497. if ($script:SandboxTest -eq '0') {
  1498. $PrBodyContentReply += @($_line.Replace('[ ]', '[X]'))
  1499. $_showMenu = $false
  1500. } else {
  1501. $_menu = @{
  1502. Prompt = "Have you tested your manifest locally with 'winget install --manifest <path>'?"
  1503. Entries = @('[Y] Yes'; '*[N] No')
  1504. HelpText = 'You did not test your Manifest in Windows Sandbox previously.'
  1505. HelpTextColor = 'Red'
  1506. DefaultString = 'N'
  1507. }
  1508. }
  1509. }
  1510. '*schema*' {
  1511. $_Match = ($_line | Select-String -Pattern 'https://+.+(?=\))').Matches.Value
  1512. $_menu = @{
  1513. Prompt = $_line.TrimStart('- [ ]') -replace '\[|\]|\(.+\)', ''
  1514. Entries = @('[Y] Yes'; '*[N] No')
  1515. HelpText = "Reference Link: $_Match"
  1516. HelpTextColor = ''
  1517. DefaultString = 'N'
  1518. }
  1519. }
  1520. Default {
  1521. $_menu = @{
  1522. Prompt = $_line.TrimStart('- [ ]')
  1523. Entries = @('[Y] Yes'; '*[N] No')
  1524. HelpText = ''
  1525. HelpTextColor = ''
  1526. DefaultString = 'N'
  1527. }
  1528. }
  1529. }
  1530. if ($_showMenu) {
  1531. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor']) {
  1532. 'Y' { $PrBodyContentReply += @($_line.Replace('[ ]', '[X]')) }
  1533. default { $PrBodyContentReply += @($_line) }
  1534. }
  1535. }
  1536. }
  1537. # Request user to enter if there were any issues resolved by the PR
  1538. $_menu = @{
  1539. entries = @('[Y] Yes'; '*[N] No')
  1540. Prompt = 'Does this pull request resolve any issues?'
  1541. DefaultString = 'N'
  1542. }
  1543. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
  1544. 'Y' {
  1545. # If there were issues resolved by the PR, request user to enter them
  1546. Write-Host
  1547. Write-Host "Enter issue number. For example`: 21983, 43509"
  1548. $ResolvedIssues = Read-Host -Prompt 'Resolved Issues' | UniqueItems
  1549. $PrBodyContentReply += @('')
  1550. # Validate each of the issues entered by checking the URL to ensure it returns a 200 status code
  1551. Foreach ($i in ($ResolvedIssues.Split(',').Trim())) {
  1552. if ($i.Contains('#')) {
  1553. $_UrlParameters = $i.Split('#')
  1554. switch ($_UrlParameters.Count) {
  1555. 2 {
  1556. if ([string]::IsNullOrWhiteSpace($_urlParameters[0])) {
  1557. $_checkedURL = "https://github.com/microsoft/winget-pkgs/issues/$($_urlParameters[1])"
  1558. } else {
  1559. $_checkedURL = "https://github.com/$($_urlParameters[0])/issues/$($_urlParameters[1])"
  1560. }
  1561. }
  1562. default {
  1563. Write-Host -ForegroundColor 'Red' "Invalid Issue: $i"
  1564. continue
  1565. }
  1566. }
  1567. $_responseCode = Test-Url $_checkedURL
  1568. if ($_responseCode -ne 200) {
  1569. Write-Host -ForegroundColor 'Red' "Invalid Issue: $i"
  1570. continue
  1571. }
  1572. $PrBodyContentReply += @("Resolves $i")
  1573. } else {
  1574. $_checkedURL = "https://github.com/microsoft/winget-pkgs/issues/$i"
  1575. $_responseCode = Test-Url $_checkedURL
  1576. if ($_responseCode -ne 200) {
  1577. Write-Host -ForegroundColor 'Red' "Invalid Issue: $i"
  1578. continue
  1579. }
  1580. $PrBodyContentReply += @("* Resolves #$i")
  1581. }
  1582. }
  1583. }
  1584. default { Write-Host }
  1585. }
  1586. # If we are removing a manifest, we need to include the reason
  1587. if ($CommitType -eq 'Remove') {
  1588. $PrBodyContentReply = @("## $($script:RemovalReason)"; '') + $PrBodyContentReply
  1589. }
  1590. # Write the PR using a temporary file
  1591. Set-Content -Path PrBodyFile -Value $PrBodyContentReply | Out-Null
  1592. gh pr create --body-file PrBodyFile -f
  1593. Remove-Item PrBodyFile
  1594. }
  1595. # Takes a comma separated list of values, converts it to an array object, and adds the result to a specified object-key
  1596. Function Add-YamlListParameter {
  1597. Param
  1598. (
  1599. [Parameter(Mandatory = $true, Position = 0)]
  1600. [PSCustomObject] $Object,
  1601. [Parameter(Mandatory = $true, Position = 1)]
  1602. [string] $Parameter,
  1603. [Parameter(Mandatory = $true, Position = 2)]
  1604. $Values
  1605. )
  1606. $_Values = @()
  1607. Foreach ($Value in $Values.Split(',').Trim()) {
  1608. $_Values += $Value
  1609. }
  1610. $Object[$Parameter] = $_Values
  1611. }
  1612. # Takes a single value and adds it to a specified object-key
  1613. Function Add-YamlParameter {
  1614. Param
  1615. (
  1616. [Parameter(Mandatory = $true, Position = 0)]
  1617. [PSCustomObject] $Object,
  1618. [Parameter(Mandatory = $true, Position = 1)]
  1619. [string] $Parameter,
  1620. [Parameter(Mandatory = $true, Position = 2)]
  1621. [string] $Value
  1622. )
  1623. $Object[$Parameter] = $Value
  1624. }
  1625. # Fetch the value of a manifest value regardless of which manifest file it exists in
  1626. Function Get-MultiManifestParameter {
  1627. Param(
  1628. [Parameter(Mandatory = $true, Position = 1)]
  1629. [string] $Parameter
  1630. )
  1631. $_vals = $($script:OldInstallerManifest[$Parameter] + $script:OldLocaleManifest[$Parameter] + $script:OldVersionManifest[$Parameter] | Where-Object { $_ })
  1632. return ($_vals -join ', ')
  1633. }
  1634. Function Get-DebugString {
  1635. $debug = ' $debug='
  1636. $debug += $(switch ($script:Option) {
  1637. 'New' { 'NV' }
  1638. 'QuickUpdateVersion' { 'QU' }
  1639. 'EditMetadata' { 'MD' }
  1640. 'NewLocale' { 'NL' }
  1641. 'Auto' { 'AU' }
  1642. Default { 'XX' }
  1643. })
  1644. $debug += $(
  1645. switch ($script:SaveOption) {
  1646. '0' { 'S0.' }
  1647. '1' { 'S1.' }
  1648. '2' { 'S2.' }
  1649. Default { 'SU.' }
  1650. }
  1651. )
  1652. $debug += $PSVersionTable.PSVersion -Replace '\.', '-'
  1653. return $debug
  1654. }
  1655. # Take all the entered values and write the version manifest file
  1656. Function Write-VersionManifest {
  1657. # Create new empty manifest
  1658. [PSCustomObject]$VersionManifest = [ordered]@{}
  1659. # Write these values into the manifest
  1660. $_Singletons = [ordered]@{
  1661. 'PackageIdentifier' = $PackageIdentifier
  1662. 'PackageVersion' = $PackageVersion
  1663. 'DefaultLocale' = if ($PackageLocale) { $PackageLocale } else { 'en-US' }
  1664. 'ManifestType' = 'version'
  1665. 'ManifestVersion' = $ManifestVersion
  1666. }
  1667. foreach ($_Item in $_Singletons.GetEnumerator()) {
  1668. If ($_Item.Value) { Add-YamlParameter -Object $VersionManifest -Parameter $_Item.Name -Value $_Item.Value }
  1669. }
  1670. $VersionManifest = Restore-YamlKeyOrder $VersionManifest $VersionProperties
  1671. # Create the folder for the file if it doesn't exist
  1672. New-Item -ItemType 'Directory' -Force -Path $AppFolder | Out-Null
  1673. $VersionManifestPath = $AppFolder + "\$PackageIdentifier" + '.yaml'
  1674. # Write the manifest to the file
  1675. $ScriptHeader + "$(Get-DebugString)`n# yaml-language-server: `$schema=https://aka.ms/winget-manifest.version.$ManifestVersion.schema.json`n" > $VersionManifestPath
  1676. ConvertTo-Yaml $VersionManifest >> $VersionManifestPath
  1677. $(Get-Content $VersionManifestPath -Encoding UTF8) -replace "(.*)$([char]0x2370)", "# `$1" | Out-File -FilePath $VersionManifestPath -Force
  1678. $MyRawString = Get-Content -Raw $VersionManifestPath | TrimString
  1679. [System.IO.File]::WriteAllLines($VersionManifestPath, $MyRawString, $Utf8NoBomEncoding)
  1680. # Tell user the file was created and the path to the file
  1681. Write-Host
  1682. Write-Host "Yaml file created: $VersionManifestPath"
  1683. }
  1684. # Take all the entered values and write the installer manifest file
  1685. Function Write-InstallerManifest {
  1686. # If the old manifests exist, copy it so it can be updated in place, otherwise, create a new empty manifest
  1687. if ($script:OldManifestType -eq 'MultiManifest') {
  1688. $InstallerManifest = $script:OldInstallerManifest
  1689. }
  1690. if (!$InstallerManifest) { [PSCustomObject]$InstallerManifest = [ordered]@{} }
  1691. #Add the properties to the manifest
  1692. Add-YamlParameter -Object $InstallerManifest -Parameter 'PackageIdentifier' -Value $PackageIdentifier
  1693. Add-YamlParameter -Object $InstallerManifest -Parameter 'PackageVersion' -Value $PackageVersion
  1694. $InstallerManifest['MinimumOSVersion'] = If ($MinimumOSVersion) { $MinimumOSVersion } Else { '10.0.0.0' }
  1695. $_ListSections = [ordered]@{
  1696. 'FileExtensions' = $FileExtensions
  1697. 'Protocols' = $Protocols
  1698. 'Commands' = $Commands
  1699. 'InstallerSuccessCodes' = $InstallerSuccessCodes
  1700. 'InstallModes' = $InstallModes
  1701. }
  1702. foreach ($Section in $_ListSections.GetEnumerator()) {
  1703. If ($Section.Value) { Add-YamlListParameter -Object $InstallerManifest -Parameter $Section.Name -Values $Section.Value }
  1704. }
  1705. if ($Option -ne 'EditMetadata') {
  1706. $InstallerManifest['Installers'] = $script:Installers
  1707. } elseif ($script:OldInstallerManifest) {
  1708. $InstallerManifest['Installers'] = $script:OldInstallerManifest['Installers']
  1709. } else {
  1710. $InstallerManifest['Installers'] = $script:OldVersionManifest['Installers']
  1711. }
  1712. foreach ($_Installer in $InstallerManifest.Installers) {
  1713. if ($_Installer['ReleaseDate'] -and !$script:ReleaseDatePrompted) { $_Installer.Remove('ReleaseDate') }
  1714. }
  1715. Add-YamlParameter -Object $InstallerManifest -Parameter 'ManifestType' -Value 'installer'
  1716. Add-YamlParameter -Object $InstallerManifest -Parameter 'ManifestVersion' -Value $ManifestVersion
  1717. If ($InstallerManifest['Dependencies']) {
  1718. $InstallerManifest['Dependencies'] = Restore-YamlKeyOrder $InstallerManifest['Dependencies'] $InstallerDependencyProperties -NoComments
  1719. }
  1720. # Move Installer Level Keys to Manifest Level
  1721. $_KeysToMove = $InstallerEntryProperties | Where-Object { $_ -in $InstallerProperties -and $_ -ne 'ProductCode' }
  1722. foreach ($_Key in $_KeysToMove) {
  1723. if ($_Key -in $InstallerManifest.Installers[0].Keys) {
  1724. # Handle the switches specially
  1725. if ($_Key -eq 'InstallerSwitches') {
  1726. # Go into each of the subkeys to see if they are the same
  1727. foreach ($_InstallerSwitchKey in $InstallerManifest.Installers[0].$_Key.Keys) {
  1728. $_AllAreSame = $true
  1729. $_FirstInstallerSwitchKeyValue = ConvertTo-Json($InstallerManifest.Installers[0].$_Key.$_InstallerSwitchKey)
  1730. foreach ($_Installer in $InstallerManifest.Installers) {
  1731. $_CurrentInstallerSwitchKeyValue = ConvertTo-Json($_Installer.$_Key.$_InstallerSwitchKey)
  1732. if (Test-String $_CurrentInstallerSwitchKeyValue -IsNull) { $_AllAreSame = $false }
  1733. else { $_AllAreSame = $_AllAreSame -and (@(Compare-Object $_CurrentInstallerSwitchKeyValue $_FirstInstallerSwitchKeyValue).Length -eq 0) }
  1734. }
  1735. if ($_AllAreSame) {
  1736. if ($_Key -notin $InstallerManifest.Keys) { $InstallerManifest[$_Key] = @{} }
  1737. $InstallerManifest.$_Key[$_InstallerSwitchKey] = $InstallerManifest.Installers[0].$_Key.$_InstallerSwitchKey
  1738. }
  1739. }
  1740. # Remove them from the individual installer switches if we moved them to the manifest level
  1741. if ($_Key -in $InstallerManifest.Keys) {
  1742. foreach ($_InstallerSwitchKey in $InstallerManifest.$_Key.Keys) {
  1743. foreach ($_Installer in $InstallerManifest.Installers) {
  1744. if ($_Installer.Keys -contains $_Key) {
  1745. if ($_Installer.$_Key.Keys -contains $_InstallerSwitchKey) { $_Installer.$_Key.Remove($_InstallerSwitchKey) }
  1746. if (@($_Installer.$_Key.Keys).Count -eq 0) { $_Installer.Remove($_Key) }
  1747. }
  1748. }
  1749. }
  1750. }
  1751. } else {
  1752. # Check if all installers are the same
  1753. $_AllAreSame = $true
  1754. $_FirstInstallerKeyValue = ConvertTo-Json($InstallerManifest.Installers[0].$_Key)
  1755. foreach ($_Installer in $InstallerManifest.Installers) {
  1756. $_CurrentInstallerKeyValue = ConvertTo-Json($_Installer.$_Key)
  1757. if (Test-String $_CurrentInstallerKeyValue -IsNull) { $_AllAreSame = $false }
  1758. else { $_AllAreSame = $_AllAreSame -and (@(Compare-Object $_CurrentInstallerKeyValue $_FirstInstallerKeyValue).Length -eq 0) }
  1759. }
  1760. # If all installers are the same move the key to the manifest level
  1761. if ($_AllAreSame) {
  1762. $InstallerManifest[$_Key] = $InstallerManifest.Installers[0].$_Key
  1763. foreach ($_Installer in $InstallerManifest.Installers) {
  1764. $_Installer.Remove($_Key)
  1765. }
  1766. }
  1767. }
  1768. }
  1769. }
  1770. if ($InstallerManifest.Keys -contains 'InstallerSwitches') { $InstallerManifest['InstallerSwitches'] = Restore-YamlKeyOrder $InstallerManifest.InstallerSwitches $InstallerSwitchProperties -NoComments }
  1771. foreach ($_Installer in $InstallerManifest.Installers) {
  1772. if ($_Installer.Keys -contains 'InstallerSwitches') { $_Installer['InstallerSwitches'] = Restore-YamlKeyOrder $_Installer.InstallerSwitches $InstallerSwitchProperties -NoComments }
  1773. }
  1774. # Clean up the existing files just in case
  1775. if ($InstallerManifest['Commands']) { $InstallerManifest['Commands'] = @($InstallerManifest['Commands'] | UniqueItems | NoWhitespace | Sort-Object) }
  1776. if ($InstallerManifest['Protocols']) { $InstallerManifest['Protocols'] = @($InstallerManifest['Protocols'] | ToLower | UniqueItems | NoWhitespace | Sort-Object) }
  1777. if ($InstallerManifest['FileExtensions']) { $InstallerManifest['FileExtensions'] = @($InstallerManifest['FileExtensions'] | ToLower | UniqueItems | NoWhitespace | Sort-Object) }
  1778. $InstallerManifest = Restore-YamlKeyOrder $InstallerManifest $InstallerProperties -NoComments
  1779. # Create the folder for the file if it doesn't exist
  1780. New-Item -ItemType 'Directory' -Force -Path $AppFolder | Out-Null
  1781. $script:InstallerManifestPath = $AppFolder + "\$PackageIdentifier" + '.installer' + '.yaml'
  1782. # Write the manifest to the file
  1783. $ScriptHeader + "$(Get-DebugString)`n# yaml-language-server: `$schema=https://aka.ms/winget-manifest.installer.$ManifestVersion.schema.json`n" > $InstallerManifestPath
  1784. ConvertTo-Yaml $InstallerManifest >> $InstallerManifestPath
  1785. $(Get-Content $InstallerManifestPath -Encoding UTF8) -replace "(.*)$([char]0x2370)", "# `$1" | Out-File -FilePath $InstallerManifestPath -Force
  1786. $MyRawString = Get-Content -Raw $InstallerManifestPath | TrimString
  1787. [System.IO.File]::WriteAllLines($InstallerManifestPath, $MyRawString, $Utf8NoBomEncoding)
  1788. # Tell user the file was created and the path to the file
  1789. Write-Host
  1790. Write-Host "Yaml file created: $InstallerManifestPath"
  1791. }
  1792. # Take all the entered values and write the locale manifest file
  1793. Function Write-LocaleManifest {
  1794. # If the old manifests exist, copy it so it can be updated in place, otherwise, create a new empty manifest
  1795. if ($script:OldManifestType -eq 'MultiManifest') {
  1796. $LocaleManifest = $script:OldLocaleManifest
  1797. }
  1798. if (!$LocaleManifest) { [PSCustomObject]$LocaleManifest = [ordered]@{} }
  1799. # Add the properties to the manifest
  1800. $_Singletons = [ordered]@{
  1801. 'PackageIdentifier' = $PackageIdentifier
  1802. 'PackageVersion' = $PackageVersion
  1803. 'PackageLocale' = $PackageLocale
  1804. 'Publisher' = $Publisher
  1805. 'PublisherUrl' = $PublisherUrl
  1806. 'PublisherSupportUrl' = $PublisherSupportUrl
  1807. 'PrivacyUrl' = $PrivacyUrl
  1808. 'Author' = $Author
  1809. 'PackageName' = $PackageName
  1810. 'PackageUrl' = $PackageUrl
  1811. 'License' = $License
  1812. 'LicenseUrl' = $LicenseUrl
  1813. 'Copyright' = $Copyright
  1814. 'CopyrightUrl' = $CopyrightUrl
  1815. 'ShortDescription' = $ShortDescription
  1816. 'Description' = $Description
  1817. 'ReleaseNotes' = $ReleaseNotes
  1818. 'ReleaseNotesUrl' = $ReleaseNotesUrl
  1819. }
  1820. foreach ($_Item in $_Singletons.GetEnumerator()) {
  1821. If ($_Item.Value) { Add-YamlParameter -Object $LocaleManifest -Parameter $_Item.Name -Value $_Item.Value }
  1822. }
  1823. If ($Tags) { Add-YamlListParameter -Object $LocaleManifest -Parameter 'Tags' -Values $Tags }
  1824. If (!$LocaleManifest.ManifestType) { $LocaleManifest['ManifestType'] = 'defaultLocale' }
  1825. If ($Moniker -and $($LocaleManifest.ManifestType -eq 'defaultLocale')) { Add-YamlParameter -Object $LocaleManifest -Parameter 'Moniker' -Value $Moniker }
  1826. Add-YamlParameter -Object $LocaleManifest -Parameter 'ManifestVersion' -Value $ManifestVersion
  1827. # Clean up the existing files just in case
  1828. if ($LocaleManifest['Tags']) { $LocaleManifest['Tags'] = @($LocaleManifest['Tags'] | ToLower | UniqueItems | NoWhitespace | Sort-Object) }
  1829. if ($LocaleManifest['Moniker']) { $LocaleManifest['Moniker'] = $LocaleManifest['Moniker'] | ToLower | NoWhitespace }
  1830. # Clean up the volatile fields
  1831. if ($LocaleManifest['ReleaseNotes'] -and (Test-String $script:ReleaseNotes -IsNull)) { $LocaleManifest.Remove('ReleaseNotes') }
  1832. if ($LocaleManifest['ReleaseNotesUrl'] -and (Test-String $script:ReleaseNotes -IsNull)) { $LocaleManifest.Remove('ReleaseNotesUrl') }
  1833. $LocaleManifest = Restore-YamlKeyOrder $LocaleManifest $LocaleProperties
  1834. # Set the appropriate langage server depending on if it is a default locale file or generic locale file
  1835. if ($LocaleManifest.ManifestType -eq 'defaultLocale') { $yamlServer = "# yaml-language-server: `$schema=https://aka.ms/winget-manifest.defaultLocale.$ManifestVersion.schema.json" } else { $yamlServer = "# yaml-language-server: `$schema=https://aka.ms/winget-manifest.locale.$ManifestVersion.schema.json" }
  1836. # Create the folder for the file if it doesn't exist
  1837. New-Item -ItemType 'Directory' -Force -Path $AppFolder | Out-Null
  1838. $script:LocaleManifestPath = $AppFolder + "\$PackageIdentifier" + '.locale.' + "$PackageLocale" + '.yaml'
  1839. # Write the manifest to the file
  1840. $ScriptHeader + "$(Get-DebugString)`n$yamlServer`n" > $LocaleManifestPath
  1841. ConvertTo-Yaml $LocaleManifest >> $LocaleManifestPath
  1842. $(Get-Content $LocaleManifestPath -Encoding UTF8) -replace "(.*)$([char]0x2370)", "# `$1" | Out-File -FilePath $LocaleManifestPath -Force
  1843. $MyRawString = Get-Content -Raw $LocaleManifestPath | TrimString
  1844. [System.IO.File]::WriteAllLines($LocaleManifestPath, $MyRawString, $Utf8NoBomEncoding)
  1845. # Copy over all locale files from previous version that aren't the same
  1846. if ($OldManifests) {
  1847. ForEach ($DifLocale in $OldManifests) {
  1848. if ($DifLocale.Name -notin @("$PackageIdentifier.yaml", "$PackageIdentifier.installer.yaml", "$PackageIdentifier.locale.$PackageLocale.yaml")) {
  1849. if (!(Test-Path $AppFolder)) { New-Item -ItemType 'Directory' -Force -Path $AppFolder | Out-Null }
  1850. $script:OldLocaleManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $DifLocale.FullName -Encoding UTF8) -join "`n") -Ordered
  1851. $script:OldLocaleManifest['PackageVersion'] = $PackageVersion
  1852. if ($script:OldLocaleManifest.Keys -contains 'Moniker') { $script:OldLocaleManifest.Remove('Moniker') }
  1853. $script:OldLocaleManifest['ManifestVersion'] = $ManifestVersion
  1854. # Clean up the existing files just in case
  1855. if ($script:OldLocaleManifest['Tags']) { $script:OldLocaleManifest['Tags'] = @($script:OldLocaleManifest['Tags'] | ToLower | UniqueItems | NoWhitespace | Sort-Object) }
  1856. # Clean up the volatile fields
  1857. if ($OldLocaleManifest['ReleaseNotes'] -and (Test-String $script:ReleaseNotes -IsNull)) { $OldLocaleManifest.Remove('ReleaseNotes') }
  1858. if ($OldLocaleManifest['ReleaseNotesUrl'] -and (Test-String $script:ReleaseNotes -IsNull)) { $OldLocaleManifest.Remove('ReleaseNotesUrl') }
  1859. $script:OldLocaleManifest = Restore-YamlKeyOrder $script:OldLocaleManifest $LocaleProperties
  1860. $yamlServer = "# yaml-language-server: `$schema=https://aka.ms/winget-manifest.locale.$ManifestVersion.schema.json"
  1861. $ScriptHeader + "$(Get-DebugString)`n$yamlServer`n" > ($AppFolder + '\' + $DifLocale.Name)
  1862. ConvertTo-Yaml $OldLocaleManifest >> ($AppFolder + '\' + $DifLocale.Name)
  1863. $(Get-Content $($AppFolder + '\' + $DifLocale.Name) -Encoding UTF8) -replace "(.*)$([char]0x2370)", "# `$1" | Out-File -FilePath $($AppFolder + '\' + $DifLocale.Name) -Force
  1864. $MyRawString = Get-Content -Raw $($AppFolder + '\' + $DifLocale.Name) | TrimString
  1865. [System.IO.File]::WriteAllLines($($AppFolder + '\' + $DifLocale.Name), $MyRawString, $Utf8NoBomEncoding)
  1866. }
  1867. }
  1868. }
  1869. # Tell user the file was created and the path to the file
  1870. Write-Host
  1871. Write-Host "Yaml file created: $LocaleManifestPath"
  1872. }
  1873. function Remove-ManifestVersion {
  1874. [CmdletBinding(SupportsShouldProcess)]
  1875. Param(
  1876. [Parameter(Mandatory = $true, Position = 1)]
  1877. [string] $PathToVersion
  1878. )
  1879. # Remove the manifest, and then any parent folders so long as the parent folders are empty
  1880. do {
  1881. Remove-Item -Path $PathToVersion -Recurse -Force
  1882. $PathToVersion = Split-Path $PathToVersion
  1883. } while (@(Get-ChildItem $PathToVersion).Count -eq 0)
  1884. }
  1885. ## START OF MAIN SCRIPT ##
  1886. # Initialize the return value to be a success
  1887. $script:_returnValue = [ReturnValue]::new(200)
  1888. $script:UsingAdvancedOption = ($ScriptSettings.EnableDeveloperOptions -eq 'true') -and ($AutoUpgrade)
  1889. if (!$script:UsingAdvancedOption) {
  1890. # Request the user to choose an operation mode
  1891. Clear-Host
  1892. if ($Mode -in 1..5) {
  1893. $UserChoice = $Mode
  1894. } else {
  1895. Write-Host -ForegroundColor 'Yellow' "Select Mode:`n"
  1896. Write-MulticolorLine ' [', '1', "] New Manifest or Package Version`n" 'DarkCyan', 'White', 'DarkCyan'
  1897. Write-MulticolorLine ' [', '2', '] Quick Update Package Version ', "(Note: Must be used only when previous version`'s metadata is complete.)`n" 'DarkCyan', 'White', 'DarkCyan', 'Green'
  1898. Write-MulticolorLine ' [', '3', "] Update Package Metadata`n" 'DarkCyan', 'White', 'DarkCyan'
  1899. Write-MulticolorLine ' [', '4', "] New Locale`n" 'DarkCyan', 'White', 'DarkCyan'
  1900. Write-MulticolorLine ' [', '5', "] Remove a manifest`n" 'DarkCyan', 'White', 'DarkCyan'
  1901. Write-MulticolorLine ' [', 'Q', ']', " Any key to quit`n" 'DarkCyan', 'White', 'DarkCyan', 'Red'
  1902. Write-MulticolorLine "`nSelection: " 'White'
  1903. # Listen for keypress and set operation mode based on keypress
  1904. $Keys = @{
  1905. [ConsoleKey]::D1 = '1';
  1906. [ConsoleKey]::D2 = '2';
  1907. [ConsoleKey]::D3 = '3';
  1908. [ConsoleKey]::D4 = '4';
  1909. [ConsoleKey]::D5 = '5';
  1910. [ConsoleKey]::NumPad1 = '1';
  1911. [ConsoleKey]::NumPad2 = '2';
  1912. [ConsoleKey]::NumPad3 = '3';
  1913. [ConsoleKey]::NumPad4 = '4';
  1914. [ConsoleKey]::NumPad5 = '5';
  1915. }
  1916. do {
  1917. $keyInfo = [Console]::ReadKey($false)
  1918. } until ($keyInfo.Key)
  1919. $UserChoice = $Keys[$keyInfo.Key]
  1920. }
  1921. switch ($UserChoice) {
  1922. '1' { $script:Option = 'New' }
  1923. '2' { $script:Option = 'QuickUpdateVersion' }
  1924. '3' { $script:Option = 'EditMetadata' }
  1925. '4' { $script:Option = 'NewLocale' }
  1926. '5' { $script:Option = 'RemoveManifest' }
  1927. default {
  1928. Write-Host
  1929. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  1930. exit
  1931. }
  1932. }
  1933. } else {
  1934. if ($AutoUpgrade) { $script:Option = 'Auto' }
  1935. }
  1936. # Confirm the user undertands the implications of using the quick update mode
  1937. if (($script:Option -eq 'QuickUpdateVersion') -and ($ScriptSettings.SuppressQuickUpdateWarning -ne 'true')) {
  1938. $_menu = @{
  1939. entries = @('[Y] Continue with Quick Update'; '[N] Use Full Update Experience'; '*[Q] Exit Script')
  1940. Prompt = 'Quick Updates only allow for changes to the existing Installer URLs, Sha256 Values, and Product Codes. Are you sure you want to continue?'
  1941. 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'
  1942. HelpTextColor = 'Red'
  1943. DefaultString = 'Q'
  1944. }
  1945. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor']) {
  1946. 'Y' { Write-Host -ForegroundColor DarkYellow -Object "`n`nContinuing with Quick Update" }
  1947. 'N' { $script:Option = 'New'; Write-Host -ForegroundColor DarkYellow -Object "`n`nSwitched to Full Update Experience" }
  1948. default {
  1949. Write-Host
  1950. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  1951. [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
  1952. exit
  1953. }
  1954. }
  1955. }
  1956. Write-Host
  1957. # Request Package Identifier and Validate
  1958. do {
  1959. if ((Test-String $PackageIdentifier -IsNull) -or ($script:_returnValue.StatusCode -ne [ReturnValue]::Success().StatusCode)) {
  1960. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1961. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the Package Identifier, in the following format <Publisher shortname.Application shortname>. For example: Microsoft.Excel'
  1962. $script:PackageIdentifier = Read-Host -Prompt 'PackageIdentifier' | TrimString
  1963. }
  1964. $PackageIdentifierFolder = $PackageIdentifier.Replace('.', '\')
  1965. if (Test-String $PackageIdentifier -MinLength 4 -MaxLength $Patterns.IdentifierMaxLength -MatchPattern $Patterns.PackageIdentifier) {
  1966. $script:_returnValue = [ReturnValue]::Success()
  1967. } else {
  1968. if (Test-String -not $PackageIdentifier -MinLength 4 -MaxLength $Patterns.IdentifierMaxLength) {
  1969. $script:_returnValue = [ReturnValue]::LengthError(4, $Patterns.IdentifierMaxLength)
  1970. } elseif (Test-String -not $PackageIdentifier -MatchPattern $Patterns.PackageIdentifier) {
  1971. $script:_returnValue = [ReturnValue]::PatternError()
  1972. } else {
  1973. $script:_returnValue = [ReturnValue]::GenericError()
  1974. }
  1975. }
  1976. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1977. # Request Package Version and Validate
  1978. do {
  1979. if ((Test-String $PackageVersion -IsNull) -or ($script:_returnValue.StatusCode -ne [ReturnValue]::Success().StatusCode)) {
  1980. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  1981. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the version. for example: 1.33.7'
  1982. $script:PackageVersion = Read-Host -Prompt 'Version' | TrimString
  1983. }
  1984. if (Test-String $PackageVersion -MaxLength $Patterns.VersionMaxLength -MatchPattern $Patterns.PackageVersion -NotNull) {
  1985. $script:_returnValue = [ReturnValue]::Success()
  1986. } else {
  1987. if (Test-String -not $PackageVersion -MaxLength $Patterns.VersionMaxLength -NotNull) {
  1988. $script:_returnValue = [ReturnValue]::LengthError(1, $Patterns.VersionMaxLength)
  1989. } elseif (Test-String -not $PackageVersion -MatchPattern $Patterns.PackageVersion) {
  1990. $script:_returnValue = [ReturnValue]::PatternError()
  1991. } else {
  1992. $script:_returnValue = [ReturnValue]::GenericError()
  1993. }
  1994. }
  1995. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  1996. # Check the api for open PR's
  1997. # This is unauthenticated because the call-rate per minute is assumed to be low
  1998. if ($ScriptSettings.ContinueWithExistingPRs -ne 'always' -and $script:Option -ne 'RemoveManifest' -and !$SkipPRCheck) {
  1999. $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]
  2000. # If there was a PR found, get the URL and title
  2001. if ($PRApiResponse.total_count -gt 0) {
  2002. $_PRUrl = $PRApiResponse.items.html_url
  2003. $_PRTitle = $PRApiResponse.items.title
  2004. if ($ScriptSettings.ContinueWithExistingPRs -eq 'never') {
  2005. Write-Host -ForegroundColor Red "Existing PR Found - $_PRUrl"
  2006. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  2007. [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
  2008. exit
  2009. }
  2010. $_menu = @{
  2011. entries = @('[Y] Yes'; '*[N] No')
  2012. Prompt = 'There may already be a PR for this change. Would you like to continue anyways?'
  2013. DefaultString = 'N'
  2014. HelpText = "$_PRTitle - $_PRUrl"
  2015. HelpTextColor = 'Blue'
  2016. }
  2017. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor'] ) {
  2018. 'Y' { Write-Host }
  2019. default {
  2020. Write-Host
  2021. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  2022. [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
  2023. exit
  2024. }
  2025. }
  2026. }
  2027. }
  2028. # Set the root folder where new manifests should be created
  2029. if (Test-Path -Path "$PSScriptRoot\..\manifests") {
  2030. $ManifestsFolder = (Resolve-Path "$PSScriptRoot\..\manifests").Path
  2031. } else {
  2032. $ManifestsFolder = (Resolve-Path '.\').Path
  2033. }
  2034. # Set the folder for the specific package and version
  2035. $script:AppFolder = Join-Path $ManifestsFolder -ChildPath $PackageIdentifier.ToLower().Chars(0) | Join-Path -ChildPath $PackageIdentifierFolder | Join-Path -ChildPath $PackageVersion
  2036. # If the user selected `NewLocale` or `EditMetadata` the version *MUST* already exist in the folder structure
  2037. if ($script:Option -in @('NewLocale'; 'EditMetadata'; 'RemoveManifest')) {
  2038. # Try getting the old manifests from the specified folder
  2039. if (Test-Path -Path "$AppFolder\..\$PackageVersion") {
  2040. $script:OldManifests = Get-ChildItem -Path "$AppFolder\..\$PackageVersion"
  2041. $LastVersion = $PackageVersion
  2042. }
  2043. # If the old manifests could not be found, request a new version
  2044. while (-not ($OldManifests.Name -like "$PackageIdentifier*.yaml")) {
  2045. Write-Host
  2046. Write-Host -ForegroundColor 'Red' -Object 'Could not find required manifests, input a version containing required manifests or "exit" to cancel'
  2047. $PromptVersion = Read-Host -Prompt 'Version' | TrimString
  2048. if ($PromptVersion -eq 'exit') {
  2049. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  2050. [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
  2051. exit
  2052. }
  2053. if (Test-Path -Path "$AppFolder\..\$PromptVersion") {
  2054. $script:OldManifests = Get-ChildItem -Path "$AppFolder\..\$PromptVersion"
  2055. }
  2056. # If a new version is entered, we need to be sure to update the folder for writing manifests
  2057. $LastVersion = $PromptVersion
  2058. $script:AppFolder = (Split-Path $AppFolder) + "\$LastVersion"
  2059. $script:PackageVersion = $LastVersion
  2060. }
  2061. }
  2062. # If the user selected `QuickUpdateVersion`, the old manifests must exist
  2063. # If the user selected `New`, the old manifest type is specified as none
  2064. if (-not (Test-Path -Path "$AppFolder\..")) {
  2065. if ($script:Option -in @('QuickUpdateVersion', 'Auto')) {
  2066. 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.'
  2067. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  2068. [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
  2069. exit
  2070. }
  2071. $script:OldManifestType = 'None'
  2072. }
  2073. # Try getting the last version of the package and the old manifests to be updated
  2074. if (!$LastVersion) {
  2075. try {
  2076. $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
  2077. $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
  2078. if ($script:Option -eq 'Auto' -and $PackageVersion -in $script:ExistingVersions) { $LastVersion = $PackageVersion }
  2079. Write-Host -ForegroundColor 'DarkYellow' -Object "Found Existing Version: $LastVersion"
  2080. $script:OldManifests = Get-ChildItem -Path "$AppFolder\..\$LastVersion"
  2081. } catch {
  2082. # Take no action here, we just want to catch the exceptions as a precaution
  2083. Out-Null
  2084. }
  2085. }
  2086. # If the old manifests exist, find the default locale
  2087. if ($OldManifests.Name -match "$([Regex]::Escape($PackageIdentifier))\.locale\..*\.yaml") {
  2088. $_LocaleManifests = $OldManifests | Where-Object { $_.Name -match "$([Regex]::Escape($PackageIdentifier))\.locale\..*\.yaml" }
  2089. foreach ($_Manifest in $_LocaleManifests) {
  2090. $_ManifestContent = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $($_Manifest.FullName) -Encoding UTF8) -join "`n") -Ordered
  2091. if ($_ManifestContent.ManifestType -eq 'defaultLocale') { $PackageLocale = $_ManifestContent.PackageLocale }
  2092. }
  2093. }
  2094. # If the old manifests exist, read their information into variables
  2095. # Also ensure additional requirements are met for creating or updating files
  2096. if ($OldManifests.Name -eq "$PackageIdentifier.installer.yaml" -and $OldManifests.Name -eq "$PackageIdentifier.locale.$PackageLocale.yaml" -and $OldManifests.Name -eq "$PackageIdentifier.yaml") {
  2097. $script:OldManifestType = 'MultiManifest'
  2098. $script:OldInstallerManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $(Resolve-Path "$AppFolder\..\$LastVersion\$PackageIdentifier.installer.yaml") -Encoding UTF8) -join "`n") -Ordered
  2099. # Move Manifest Level Keys to installer Level
  2100. $_KeysToMove = $InstallerEntryProperties | Where-Object { $_ -in $InstallerProperties }
  2101. foreach ($_Key in $_KeysToMove) {
  2102. if ($_Key -in $script:OldInstallerManifest.Keys) {
  2103. # Handle Installer switches separately
  2104. if ($_Key -eq 'InstallerSwitches') {
  2105. $_SwitchKeysToMove = $script:OldInstallerManifest.$_Key.Keys
  2106. foreach ($_SwitchKey in $_SwitchKeysToMove) {
  2107. # If the InstallerSwitches key doesn't exist, we need to create it, otherwise, preserve switches that were already there
  2108. foreach ($_Installer in $script:OldInstallerManifest['Installers']) {
  2109. if ('InstallerSwitches' -notin $_Installer.Keys) { $_Installer['InstallerSwitches'] = @{} }
  2110. $_Installer.InstallerSwitches["$_SwitchKey"] = $script:OldInstallerManifest.$_Key.$_SwitchKey
  2111. }
  2112. }
  2113. $script:OldInstallerManifest.Remove($_Key)
  2114. continue
  2115. } else {
  2116. foreach ($_Installer in $script:OldInstallerManifest['Installers']) {
  2117. if ($_Key -eq 'InstallModes') { $script:InstallModes = [string]$script:OldInstallerManifest.$_Key }
  2118. if ($_Key -notin $_Installer.Keys) {
  2119. $_Installer[$_Key] = $script:OldInstallerManifest.$_Key
  2120. }
  2121. }
  2122. }
  2123. New-Variable -Name $_Key -Value $($script:OldInstallerManifest.$_Key -join ', ') -Scope Script -Force
  2124. $script:OldInstallerManifest.Remove($_Key)
  2125. }
  2126. }
  2127. $script:OldLocaleManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $(Resolve-Path "$AppFolder\..\$LastVersion\$PackageIdentifier.locale.$PackageLocale.yaml") -Encoding UTF8) -join "`n") -Ordered
  2128. $script:OldVersionManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $(Resolve-Path "$AppFolder\..\$LastVersion\$PackageIdentifier.yaml") -Encoding UTF8) -join "`n") -Ordered
  2129. } elseif ($OldManifests.Name -eq "$PackageIdentifier.yaml") {
  2130. if ($script:Option -eq 'NewLocale') { throw [ManifestException]::new('MultiManifest Required') }
  2131. $script:OldManifestType = 'MultiManifest'
  2132. $script:OldSingletonManifest = ConvertFrom-Yaml -Yaml ($(Get-Content -Path $(Resolve-Path "$AppFolder\..\$LastVersion\$PackageIdentifier.yaml") -Encoding UTF8) -join "`n") -Ordered
  2133. $PackageLocale = $script:OldSingletonManifest.PackageLocale
  2134. # Create new empty manifests
  2135. $script:OldInstallerManifest = [ordered]@{}
  2136. $script:OldLocaleManifest = [ordered]@{}
  2137. $script:OldVersionManifest = [ordered]@{}
  2138. # Parse version keys to version manifest
  2139. foreach ($_Key in $($OldSingletonManifest.Keys | Where-Object { $_ -in $VersionProperties })) {
  2140. $script:OldVersionManifest[$_Key] = $script:OldSingletonManifest.$_Key
  2141. }
  2142. $script:OldVersionManifest['ManifestType'] = 'version'
  2143. #Parse locale keys to locale manifest
  2144. foreach ($_Key in $($OldSingletonManifest.Keys | Where-Object { $_ -in $LocaleProperties })) {
  2145. $script:OldLocaleManifest[$_Key] = $script:OldSingletonManifest.$_Key
  2146. }
  2147. $script:OldLocaleManifest['ManifestType'] = 'defaultLocale'
  2148. #Parse installer keys to installer manifest
  2149. foreach ($_Key in $($OldSingletonManifest.Keys | Where-Object { $_ -in $InstallerProperties })) {
  2150. $script:OldInstallerManifest[$_Key] = $script:OldSingletonManifest.$_Key
  2151. }
  2152. $script:OldInstallerManifest['ManifestType'] = 'installer'
  2153. # Move Manifest Level Keys to installer Level
  2154. $_KeysToMove = $InstallerEntryProperties | Where-Object { $_ -in $InstallerProperties }
  2155. foreach ($_Key in $_KeysToMove) {
  2156. if ($_Key -in $script:OldInstallerManifest.Keys) {
  2157. # Handle Installer switches separately
  2158. if ($_Key -eq 'InstallerSwitches') {
  2159. $_SwitchKeysToMove = $script:OldInstallerManifest.$_Key.Keys
  2160. foreach ($_SwitchKey in $_SwitchKeysToMove) {
  2161. # If the InstallerSwitches key doesn't exist, we need to create it, otherwise, preserve switches that were already there
  2162. foreach ($_Installer in $script:OldInstallerManifest['Installers']) {
  2163. if ('InstallerSwitches' -notin $_Installer.Keys) { $_Installer['InstallerSwitches'] = @{} }
  2164. $_Installer.InstallerSwitches["$_SwitchKey"] = $script:OldInstallerManifest.$_Key.$_SwitchKey
  2165. }
  2166. }
  2167. $script:OldInstallerManifest.Remove($_Key)
  2168. continue
  2169. } else {
  2170. foreach ($_Installer in $script:OldInstallerManifest['Installers']) {
  2171. if ($_Key -eq 'InstallModes') { $script:InstallModes = [string]$script:OldInstallerManifest.$_Key }
  2172. if ($_Key -notin $_Installer.Keys) {
  2173. $_Installer[$_Key] = $script:OldInstallerManifest.$_Key
  2174. }
  2175. }
  2176. }
  2177. New-Variable -Name $_Key -Value $($script:OldInstallerManifest.$_Key -join ', ') -Scope Script -Force
  2178. $script:OldInstallerManifest.Remove($_Key)
  2179. }
  2180. }
  2181. } else {
  2182. if ($script:Option -ne 'New') { throw [ManifestException]::new("Version $LastVersion does not contain the required manifests") }
  2183. $script:OldManifestType = 'None'
  2184. }
  2185. # If the old manifests exist, read the manifest keys into their specific variables
  2186. if ($OldManifests -and $Option -ne 'NewLocale') {
  2187. $_Parameters = @(
  2188. 'Publisher'; 'PublisherUrl'; 'PublisherSupportUrl'; 'PrivacyUrl'
  2189. 'Author';
  2190. 'PackageName'; 'PackageUrl'; 'Moniker'
  2191. 'License'; 'LicenseUrl'
  2192. 'Copyright'; 'CopyrightUrl'
  2193. 'ShortDescription'; 'Description'
  2194. 'Channel'
  2195. 'Platform'; 'MinimumOSVersion'
  2196. 'InstallerType'
  2197. 'Scope'
  2198. 'UpgradeBehavior'
  2199. 'PackageFamilyName'; 'ProductCode'
  2200. 'Tags'; 'FileExtensions'
  2201. 'Protocols'; 'Commands'
  2202. 'InstallerSuccessCodes'
  2203. 'Capabilities'; 'RestrictedCapabilities'
  2204. )
  2205. Foreach ($param in $_Parameters) {
  2206. $_ReadValue = $(if ($script:OldManifestType -eq 'MultiManifest') { (Get-MultiManifestParameter $param) } else { $script:OldVersionManifest[$param] })
  2207. if (Test-String -Not $_ReadValue -IsNull) { New-Variable -Name $param -Value $_ReadValue -Scope Script -Force }
  2208. }
  2209. }
  2210. # Run the data entry and creation of manifests appropriate to the option the user selected
  2211. Switch ($script:Option) {
  2212. 'QuickUpdateVersion' {
  2213. Read-QuickInstallerEntry
  2214. Write-LocaleManifest
  2215. Write-InstallerManifest
  2216. Write-VersionManifest
  2217. }
  2218. 'New' {
  2219. Read-InstallerEntry
  2220. Read-InstallerMetadata
  2221. Read-LocaleMetadata
  2222. Write-InstallerManifest
  2223. Write-VersionManifest
  2224. Write-LocaleManifest
  2225. }
  2226. 'EditMetadata' {
  2227. Read-InstallerMetadata
  2228. Read-LocaleMetadata
  2229. Write-InstallerManifest
  2230. Write-VersionManifest
  2231. Write-LocaleManifest
  2232. }
  2233. 'NewLocale' {
  2234. $PackageLocale = $null
  2235. $script:OldLocaleManifest = [ordered]@{}
  2236. $script:OldLocaleManifest['ManifestType'] = 'locale'
  2237. Read-LocaleMetadata
  2238. Write-LocaleManifest
  2239. }
  2240. 'RemoveManifest' {
  2241. # Confirm the user is sure they know what they are doing
  2242. $_menu = @{
  2243. entries = @("[Y] Remove $PackageIdentifier version $PackageVersion"; '*[N] Cancel')
  2244. Prompt = 'Are you sure you want to continue?'
  2245. HelpText = "Manifest Versions should only be removed when necessary`n"
  2246. HelpTextColor = 'Red'
  2247. DefaultString = 'N'
  2248. }
  2249. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString'] -HelpText $_menu['HelpText'] -HelpTextColor $_menu['HelpTextColor']) {
  2250. 'Y' { Write-Host; continue }
  2251. default {
  2252. Write-Host;
  2253. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  2254. [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
  2255. exit 1
  2256. }
  2257. }
  2258. # Require that a reason for the deletion is provided
  2259. do {
  2260. Write-Host -ForegroundColor 'Red' $script:_returnValue.ErrorString()
  2261. Write-Host -ForegroundColor 'Green' -Object '[Required] Enter the reason for removing this manifest'
  2262. $script:RemovalReason = Read-Host -Prompt 'Reason' | TrimString
  2263. # Check the reason for validity. The length requirements are arbitrary, but they have been set to encourage concise yet meaningful reasons
  2264. if (Test-String $script:RemovalReason -MinLength 8 -MaxLength 128 -NotNull) {
  2265. $script:_returnValue = [ReturnValue]::Success()
  2266. } else {
  2267. $script:_returnValue = [ReturnValue]::LengthError(8, 128)
  2268. }
  2269. } until ($script:_returnValue.StatusCode -eq [ReturnValue]::Success().StatusCode)
  2270. Remove-ManifestVersion $AppFolder
  2271. }
  2272. 'Auto' {
  2273. # Set new package version
  2274. $script:OldInstallerManifest['PackageVersion'] = $PackageVersion
  2275. $script:OldLocaleManifest['PackageVersion'] = $PackageVersion
  2276. $script:OldVersionManifest['PackageVersion'] = $PackageVersion
  2277. # Update the manifest with URLs that are already there
  2278. Write-Host $NewLine
  2279. Write-Host 'Updating Manifest Information. This may take a while...' -ForegroundColor Blue
  2280. foreach ($_Installer in $script:OldInstallerManifest.Installers) {
  2281. try {
  2282. $script:dest = Get-InstallerFile -URI $_Installer.InstallerUrl -PackageIdentifier $PackageIdentifier -PackageVersion $PackageVersion
  2283. } catch {
  2284. # Here we also want to pass any exceptions through for potential debugging
  2285. throw [System.Net.WebException]::new('The file could not be downloaded. Try running the script again', $_.Exception)
  2286. } finally {
  2287. # Check that MSI's aren't actually WIX
  2288. if ($_Installer['InstallerType'] -eq 'msi') {
  2289. $DetectedType = Get-PathInstallerType $script:dest
  2290. if ($DetectedType -in @('msi'; 'wix')) { $_Installer['InstallerType'] = $DetectedType }
  2291. }
  2292. # Get the Sha256
  2293. $_Installer['InstallerSha256'] = (Get-FileHash -Path $script:dest -Algorithm SHA256).Hash
  2294. # Update the product code, if a new one exists
  2295. # If a new product code doesn't exist, and the installer isn't an `.exe` file, remove the product code if it exists
  2296. $MSIProductCode = $null
  2297. if ([System.Environment]::OSVersion.Platform -match 'Win' -and ($script:dest).EndsWith('.msi')) {
  2298. $MSIProductCode = ([string](Get-MSIProperty -MSIPath $script:dest -Parameter 'ProductCode') | Select-String -Pattern '{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}').Matches.Value
  2299. }
  2300. if (Test-String -not $MSIProductCode -IsNull) {
  2301. $_Installer['ProductCode'] = $MSIProductCode
  2302. } elseif ( ($_Installer.Keys -contains 'ProductCode') -and ($_Installer.InstallerType -in @('appx'; 'msi'; 'msix'; 'wix'; 'burn'))) {
  2303. $_Installer.Remove('ProductCode')
  2304. }
  2305. # If the installer is msix or appx, try getting the new SignatureSha256
  2306. # If the new SignatureSha256 can't be found, remove it if it exists
  2307. $NewSignatureSha256 = $null
  2308. if ($_Installer.InstallerType -in @('msix', 'appx')) {
  2309. if (Get-Command 'winget.exe' -ErrorAction SilentlyContinue) { $NewSignatureSha256 = winget hash -m $script:dest | Select-String -Pattern 'SignatureSha256:' | ConvertFrom-String; if ($NewSignatureSha256.P2) { $NewSignatureSha256 = $NewSignatureSha256.P2.ToUpper() } }
  2310. }
  2311. if (Test-String -not $NewSignatureSha256 -IsNull) {
  2312. $_Installer['SignatureSha256'] = $NewSignatureSha256
  2313. } elseif ($_Installer.Keys -contains 'SignatureSha256') {
  2314. $_Installer.Remove('SignatureSha256')
  2315. }
  2316. # If the installer is msix or appx, try getting the new package family name
  2317. # If the new package family name can't be found, remove it if it exists
  2318. if ($script:dest -match '\.(msix|appx)(bundle){0,1}$') {
  2319. try {
  2320. Add-AppxPackage -Path $script:dest
  2321. $InstalledPkg = Get-AppxPackage | Select-Object -Last 1 | Select-Object PackageFamilyName, PackageFullName
  2322. $PackageFamilyName = $InstalledPkg.PackageFamilyName
  2323. Remove-AppxPackage $InstalledPkg.PackageFullName
  2324. } catch {
  2325. # Take no action here, we just want to catch the exceptions as a precaution
  2326. Out-Null
  2327. } finally {
  2328. if (Test-String -not $PackageFamilyName -IsNull) {
  2329. $_Installer['PackageFamilyName'] = $PackageFamilyName
  2330. } elseif ($_Installer.Keys -contains 'PackageFamilyName') {
  2331. $_Installer.Remove('PackageFamilyName')
  2332. }
  2333. }
  2334. }
  2335. # Remove the downloaded files
  2336. Remove-Item -Path $script:dest
  2337. }
  2338. }
  2339. # Write the new manifests
  2340. $script:Installers = $script:OldInstallerManifest.Installers
  2341. Write-LocaleManifest
  2342. Write-InstallerManifest
  2343. Write-VersionManifest
  2344. # Remove the old manifests
  2345. if ($PackageVersion -ne $LastVersion) { Remove-ManifestVersion "$AppFolder\..\$LastVersion" }
  2346. }
  2347. }
  2348. if ($script:Option -ne 'RemoveManifest') {
  2349. # If the user has winget installed, attempt to validate the manifests
  2350. if (Get-Command 'winget.exe' -ErrorAction SilentlyContinue) { winget validate $AppFolder }
  2351. # If the user has sandbox enabled, request to test the manifest in the sandbox
  2352. if (Get-Command 'WindowsSandbox.exe' -ErrorAction SilentlyContinue) {
  2353. # Check the settings to see if we need to display this menu
  2354. switch ($ScriptSettings.TestManifestsInSandbox) {
  2355. 'always' { $script:SandboxTest = '0' }
  2356. 'never' { $script:SandboxTest = '1' }
  2357. default {
  2358. $_menu = @{
  2359. entries = @('*[Y] Yes'; '[N] No')
  2360. Prompt = '[Recommended] Do you want to test your Manifest in Windows Sandbox?'
  2361. DefaultString = 'Y'
  2362. }
  2363. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
  2364. 'Y' { $script:SandboxTest = '0' }
  2365. 'N' { $script:SandboxTest = '1' }
  2366. default { $script:SandboxTest = '0' }
  2367. }
  2368. Write-Host
  2369. }
  2370. }
  2371. if ($script:SandboxTest -eq '0') {
  2372. if (Test-Path -Path "$gitTopLevel\Tools\SandboxTest.ps1") {
  2373. $SandboxScriptPath = (Resolve-Path "$gitTopLevel\Tools\SandboxTest.ps1").Path
  2374. } else {
  2375. while ([string]::IsNullOrWhiteSpace($SandboxScriptPath)) {
  2376. Write-Host
  2377. Write-Host -ForegroundColor 'Green' -Object 'SandboxTest.ps1 not found, input path'
  2378. $SandboxScriptPath = Read-Host -Prompt 'SandboxTest.ps1' | TrimString
  2379. }
  2380. }
  2381. & $SandboxScriptPath -Manifest $AppFolder
  2382. }
  2383. }
  2384. }
  2385. # If the user has git installed, request to automatically submit the PR
  2386. if (Get-Command 'git.exe' -ErrorAction SilentlyContinue) {
  2387. switch ($ScriptSettings.AutoSubmitPRs) {
  2388. 'always' { $PromptSubmit = '0' }
  2389. 'never' { $PromptSubmit = '1' }
  2390. default {
  2391. $_menu = @{
  2392. entries = @('*[Y] Yes'; '[N] No')
  2393. Prompt = 'Do you want to submit your PR now?'
  2394. DefaultString = 'Y'
  2395. }
  2396. switch ( Invoke-KeypressMenu -Prompt $_menu['Prompt'] -Entries $_menu['Entries'] -DefaultString $_menu['DefaultString']) {
  2397. 'Y' { $PromptSubmit = '0' }
  2398. 'N' { $PromptSubmit = '1' }
  2399. default { $PromptSubmit = '0' }
  2400. }
  2401. }
  2402. }
  2403. }
  2404. Write-Host
  2405. # If the user agreed to automatically submit the PR
  2406. if ($PromptSubmit -eq '0') {
  2407. # Determine what type of update should be used as the prefix for the PR
  2408. switch -regex ($Option) {
  2409. 'New|QuickUpdateVersion|Auto' {
  2410. $AllVersions = (@($script:ExistingVersions) + @($PackageVersion)) | Sort-Object $ToNatural
  2411. if ($AllVersions.Count -eq '1') { $CommitType = 'New package' }
  2412. elseif ($script:PackageVersion -in $script:ExistingVersions) { $CommitType = 'Update' }
  2413. elseif (($AllVersions.IndexOf($PackageVersion) + 1) -eq $AllVersions.Count) { $CommitType = 'New version' }
  2414. elseif (($AllVersions.IndexOf($PackageVersion) + 1) -ne $AllVersions.Count) { $CommitType = 'Add version' }
  2415. }
  2416. 'EditMetadata' { $CommitType = 'Metadata' }
  2417. 'NewLocale' { $CommitType = 'Locale' }
  2418. 'RemoveManifest' { $CommitType = 'Remove' }
  2419. }
  2420. # Change the users git configuration to suppress some git messages
  2421. $_previousConfig = git config --get core.safecrlf
  2422. if ($_previousConfig) {
  2423. git config --replace core.safecrlf false
  2424. } else {
  2425. git config --add core.safecrlf false
  2426. }
  2427. # Fetch the upstream branch, create a commit onto the detached head, and push it to a new branch
  2428. git fetch upstream master --quiet
  2429. git switch -d upstream/master
  2430. if ($LASTEXITCODE -eq '0') {
  2431. # Make sure path exists and is valid before hashing
  2432. $UniqueBranchID = ''
  2433. if ($script:LocaleManifestPath -and (Test-Path -Path $script:LocaleManifestPath)) { $UniqueBranchID = $UniqueBranchID + $($(Get-FileHash $script:LocaleManifestPath).Hash[0..6] -Join '') }
  2434. if ($script:InstallerManifestPath -and (Test-Path -Path $script:InstallerManifestPath)) { $UniqueBranchID = $UniqueBranchID + $($(Get-FileHash $script:InstallerManifestPath).Hash[0..6] -Join '') }
  2435. if (Test-String -IsNull $UniqueBranchID) { $UniqueBranchID = 'DEL' }
  2436. $BranchName = "$PackageIdentifier-$PackageVersion-$UniqueBranchID"
  2437. # Git branch names cannot start with `.` cannot contain any of {`..`, `\`, `~`, `^`, `:`, ` `, `?`, `@{`, `[`}, and cannot end with {`/`, `.lock`, `.`}
  2438. $BranchName = $BranchName -replace '[\~,\^,\:,\\,\?,\@\{,\*,\[,\s]{1,}|[.lock|/|\.]*$|^\.{1,}|\.\.', ''
  2439. git add "$((Resolve-Path "$gitTopLevel\manifests").Path)\*"
  2440. git commit -m "$CommitType`: $PackageIdentifier version $PackageVersion" --quiet
  2441. git switch -c "$BranchName" --quiet
  2442. git push --set-upstream origin "$BranchName" --quiet
  2443. # If the user has the cli too
  2444. if (Get-Command 'gh.exe' -ErrorAction SilentlyContinue) {
  2445. # Request the user to fill out the PR template
  2446. if (Test-Path -Path "$gitTopLevel\.github\PULL_REQUEST_TEMPLATE.md") {
  2447. Read-PRBody (Resolve-Path "$gitTopLevel\.github\PULL_REQUEST_TEMPLATE.md").Path
  2448. } else {
  2449. while ([string]::IsNullOrWhiteSpace($SandboxScriptPath)) {
  2450. Write-Host
  2451. Write-Host -ForegroundColor 'Green' -Object 'PULL_REQUEST_TEMPLATE.md not found, input path'
  2452. $PRTemplate = Read-Host -Prompt 'PR Template' | TrimString
  2453. }
  2454. Read-PRBody "$PRTemplate"
  2455. }
  2456. }
  2457. }
  2458. # Restore the user's previous git settings to ensure we don't disrupt their normal flow
  2459. if ($_previousConfig) {
  2460. git config --replace core.safecrlf $_previousConfig
  2461. } else {
  2462. git config --unset core.safecrlf
  2463. }
  2464. } else {
  2465. Write-Host
  2466. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  2467. [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
  2468. exit
  2469. }
  2470. [Threading.Thread]::CurrentThread.CurrentUICulture = $callingUICulture
  2471. [Threading.Thread]::CurrentThread.CurrentCulture = $callingCulture
  2472. # Error levels for the ReturnValue class
  2473. Enum ErrorLevel {
  2474. Undefined = -1
  2475. Info = 0
  2476. Warning = 1
  2477. Error = 2
  2478. Critical = 3
  2479. }
  2480. # Custom class for validation and error checking
  2481. # `200` should be indicative of a success
  2482. # `400` should be indicative of a bad request
  2483. # `500` should be indicative of an internal error / other error
  2484. Class ReturnValue {
  2485. [int] $StatusCode
  2486. [string] $Title
  2487. [string] $Message
  2488. [ErrorLevel] $Severity
  2489. # Default Constructor
  2490. ReturnValue() {
  2491. }
  2492. # Overload 1; Creates a return value with only a status code and no descriptors
  2493. ReturnValue(
  2494. [int]$statusCode
  2495. ) {
  2496. $this.StatusCode = $statusCode
  2497. $this.Title = '-'
  2498. $this.Message = '-'
  2499. $this.Severity = -1
  2500. }
  2501. # Overload 2; Create a return value with all parameters defined
  2502. ReturnValue(
  2503. [int] $statusCode,
  2504. [string] $title,
  2505. [string] $message,
  2506. [ErrorLevel] $severity
  2507. ) {
  2508. $this.StatusCode = $statusCode
  2509. $this.Title = $title
  2510. $this.Message = $message
  2511. $this.Severity = $severity
  2512. }
  2513. # Static reference to a default success value
  2514. [ReturnValue] static Success() {
  2515. return [ReturnValue]::new(200, 'OK', 'The command completed successfully', 'Info')
  2516. }
  2517. # Static reference to a default internal error value
  2518. [ReturnValue] static GenericError() {
  2519. return [ReturnValue]::new(500, 'Internal Error', 'Value was not able to be saved successfully', 2)
  2520. }
  2521. # Static reference to a specific error relating to the pattern of user input
  2522. [ReturnValue] static PatternError() {
  2523. return [ReturnValue]::new(400, 'Invalid Pattern', 'The value entered does not match the pattern requirements defined in the manifest schema', 2)
  2524. }
  2525. # Static reference to a specific error relating to the length of user input
  2526. [ReturnValue] static LengthError([int]$MinLength, [int]$MaxLength) {
  2527. return [ReturnValue]::new(400, 'Invalid Length', "Length must be between $MinLength and $MaxLength characters", 2)
  2528. }
  2529. # Static reference to a specific error relating to the number of entries a user input
  2530. [ReturnValue] static MaxItemsError([int]$MaxEntries) {
  2531. return [ReturnValue]::new(400, 'Too many entries', "Number of entries must be less than or equal to $MaxEntries", 2)
  2532. }
  2533. # Returns the ReturnValue as a nicely formatted string
  2534. [string] ToString() {
  2535. return "[$($this.Severity)] ($($this.StatusCode)) $($this.Title) - $($this.Message)"
  2536. }
  2537. # Returns the ReturnValue as a nicely formatted string if the status code is not equal to 200
  2538. [string] ErrorString() {
  2539. if ($this.StatusCode -eq 200) {
  2540. return $null
  2541. } else {
  2542. return "[$($this.Severity)] $($this.Title) - $($this.Message)`n"
  2543. }
  2544. }
  2545. }
  2546. class UnmetDependencyException : Exception {
  2547. UnmetDependencyException([string] $message) : base($message) {}
  2548. UnmetDependencyException([string] $message, [Exception] $exception) : base($message, $exception) {}
  2549. }
  2550. class ManifestException : Exception {
  2551. ManifestException([string] $message) : base($message) {}
  2552. }