mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
423 lines
16 KiB
PowerShell
423 lines
16 KiB
PowerShell
|
|
#Requires -Version 5.1
|
||
|
|
|
||
|
|
[CmdletBinding()]
|
||
|
|
Param(
|
||
|
|
[Parameter(Mandatory=$false)]
|
||
|
|
[ValidateSet("arm64", "x64")]
|
||
|
|
[string]$Platform = "x64",
|
||
|
|
|
||
|
|
[Parameter(Mandatory=$false)]
|
||
|
|
[ValidateSet("Debug", "Release")]
|
||
|
|
[string]$Configuration = "Release",
|
||
|
|
|
||
|
|
[switch]$Clean,
|
||
|
|
[switch]$ForceCert,
|
||
|
|
[switch]$NoSign,
|
||
|
|
[switch]$CIBuild
|
||
|
|
)
|
||
|
|
|
||
|
|
# PowerToys sparse packaging helper.
|
||
|
|
# Generates a sparse MSIX (no payload) that grants package identity to selected Win32 components.
|
||
|
|
# Multiple applications (PowerOCR, Settings UI, etc.) can share this single sparse identity.
|
||
|
|
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
|
||
|
|
$isCIBuild = $false
|
||
|
|
if ($CIBuild.IsPresent) {
|
||
|
|
$isCIBuild = $true
|
||
|
|
} elseif ($env:CIBuild) {
|
||
|
|
$isCIBuild = $env:CIBuild -ieq 'true'
|
||
|
|
}
|
||
|
|
|
||
|
|
$currentPublisherHint = $script:Config.CertSubject
|
||
|
|
|
||
|
|
# Configuration constants - centralized management
|
||
|
|
$script:Config = @{
|
||
|
|
IdentityName = "Microsoft.PowerToys.SparseApp"
|
||
|
|
SparseMsixName = "PowerToysSparse.msix"
|
||
|
|
CertPrefix = "PowerToysSparse"
|
||
|
|
CertSubject = 'CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US'
|
||
|
|
CertValidMonths = 12
|
||
|
|
}
|
||
|
|
|
||
|
|
#region Helper Functions
|
||
|
|
|
||
|
|
function Find-WindowsSDKTool {
|
||
|
|
[CmdletBinding()]
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[string]$ToolName,
|
||
|
|
|
||
|
|
[Parameter(Mandatory=$false)]
|
||
|
|
[string]$Architecture = "x64"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Simple fallback: check common Windows SDK locations
|
||
|
|
$commonPaths = @(
|
||
|
|
"${env:ProgramFiles}\Windows Kits\10\bin\*\$Architecture\$ToolName",
|
||
|
|
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\$Architecture\$ToolName",
|
||
|
|
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x86\$ToolName" # SignTool fallback
|
||
|
|
)
|
||
|
|
|
||
|
|
foreach ($pattern in $commonPaths) {
|
||
|
|
$found = Get-ChildItem $pattern -ErrorAction SilentlyContinue |
|
||
|
|
Sort-Object Name -Descending |
|
||
|
|
Select-Object -First 1
|
||
|
|
if ($found) {
|
||
|
|
Write-BuildLog "Found $ToolName at: $($found.FullName)" -Level Info
|
||
|
|
return $found.FullName
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw "$ToolName not found. Please ensure Windows SDK is installed."
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-CertificateValidity {
|
||
|
|
param([string]$ThumbprintFile)
|
||
|
|
|
||
|
|
if (-not (Test-Path $ThumbprintFile)) { return $false }
|
||
|
|
|
||
|
|
try {
|
||
|
|
$thumb = (Get-Content $ThumbprintFile -Raw).Trim()
|
||
|
|
if (-not $thumb) { return $false }
|
||
|
|
$cert = Get-Item "cert:\CurrentUser\My\$thumb" -ErrorAction Stop
|
||
|
|
return $cert.HasPrivateKey -and $cert.NotAfter -gt (Get-Date)
|
||
|
|
} catch {
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Write-BuildLog {
|
||
|
|
param([string]$Message, [string]$Level = "Info")
|
||
|
|
|
||
|
|
$colors = @{ Error = "Red"; Warning = "Yellow"; Success = "Green"; Info = "Cyan" }
|
||
|
|
$color = if ($colors.ContainsKey($Level)) { $colors[$Level] } else { "White" }
|
||
|
|
|
||
|
|
Write-Host "[$(Get-Date -f 'HH:mm:ss')] $Message" -ForegroundColor $color
|
||
|
|
}
|
||
|
|
|
||
|
|
function Stop-FileProcesses {
|
||
|
|
[CmdletBinding()]
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[string]$FilePath
|
||
|
|
)
|
||
|
|
|
||
|
|
# This function is kept for compatibility but simplified since
|
||
|
|
# the staging directory approach resolves the file lock issues
|
||
|
|
Write-Verbose "File process check for: $FilePath"
|
||
|
|
}
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
# Environment diagnostics for troubleshooting
|
||
|
|
Write-BuildLog "Starting PackageIdentity build process..." -Level Info
|
||
|
|
Write-BuildLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
|
||
|
|
try {
|
||
|
|
$execPolicy = Get-ExecutionPolicy
|
||
|
|
Write-BuildLog "Execution Policy: $execPolicy" -Level Info
|
||
|
|
} catch {
|
||
|
|
Write-BuildLog "Execution Policy: Unable to determine (MSBuild environment)" -Level Info
|
||
|
|
}
|
||
|
|
Write-BuildLog "Current User: $env:USERNAME" -Level Info
|
||
|
|
Write-BuildLog "Build Platform: $Platform, Configuration: $Configuration" -Level Info
|
||
|
|
|
||
|
|
# Check for Visual Studio environment
|
||
|
|
if ($env:VSINSTALLDIR) {
|
||
|
|
Write-BuildLog "Running in Visual Studio environment: $env:VSINSTALLDIR" -Level Info
|
||
|
|
}
|
||
|
|
|
||
|
|
# Ensure certificate provider is available
|
||
|
|
try {
|
||
|
|
# Force load certificate provider for MSBuild environment
|
||
|
|
if (-not (Get-PSProvider -PSProvider Certificate -ErrorAction SilentlyContinue)) {
|
||
|
|
Write-BuildLog "Loading certificate provider..." -Level Warning
|
||
|
|
Import-Module Microsoft.PowerShell.Security -Force
|
||
|
|
}
|
||
|
|
if (-not (Test-Path 'Cert:\CurrentUser')) {
|
||
|
|
Write-BuildLog "Certificate drive not available, attempting to initialize..." -Level Warning
|
||
|
|
Import-Module PKI -ErrorAction SilentlyContinue
|
||
|
|
# Try to access the certificate store to force initialization
|
||
|
|
Get-ChildItem "Cert:\CurrentUser\My" -ErrorAction SilentlyContinue | Out-Null
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
Write-BuildLog ("Note: Certificate provider setup may need manual configuration: {0}" -f $_) -Level Warning
|
||
|
|
}
|
||
|
|
|
||
|
|
# Project root folder (now set to current script folder for local builds)
|
||
|
|
$ProjectRoot = $PSScriptRoot
|
||
|
|
$UserFolder = Join-Path $ProjectRoot '.user'
|
||
|
|
if (-not (Test-Path $UserFolder)) { New-Item -ItemType Directory -Path $UserFolder | Out-Null }
|
||
|
|
|
||
|
|
# Certificate file paths using configuration
|
||
|
|
$prefix = $script:Config.CertPrefix
|
||
|
|
$CertThumbFile, $CertCerFile = @('.thumbprint', '.cer') |
|
||
|
|
ForEach-Object { Join-Path $UserFolder "$prefix.certificate.sample$_" }
|
||
|
|
|
||
|
|
# Clean option: remove bin/obj and uninstall existing sparse package if present
|
||
|
|
if ($Clean) {
|
||
|
|
Write-BuildLog "Cleaning build artifacts..." -Level Info
|
||
|
|
'bin','obj' | ForEach-Object {
|
||
|
|
$target = Join-Path $ProjectRoot $_
|
||
|
|
if (Test-Path $target) { Remove-Item $target -Recurse -Force }
|
||
|
|
}
|
||
|
|
Write-BuildLog "Attempting to remove existing sparse package (best effort)" -Level Info
|
||
|
|
try { Get-AppxPackage -Name $script:Config.IdentityName | Remove-AppxPackage } catch {}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Force certificate regeneration if requested
|
||
|
|
if ($ForceCert -and (Test-Path $UserFolder)) {
|
||
|
|
Write-BuildLog "ForceCert specified: removing existing certificate artifacts..." -Level Warning
|
||
|
|
Remove-Item $UserFolder -Recurse -Force
|
||
|
|
New-Item -ItemType Directory -Path $UserFolder | Out-Null
|
||
|
|
}
|
||
|
|
|
||
|
|
# Ensure dev cert (development only; not for production use) - skip if NoSign specified
|
||
|
|
$needNewCert = -not $NoSign -and (-not (Test-Path $CertThumbFile) -or $ForceCert -or -not (Test-CertificateValidity -ThumbprintFile $CertThumbFile))
|
||
|
|
|
||
|
|
if ($needNewCert) {
|
||
|
|
Write-BuildLog "Generating development certificate (prefix=$($script:Config.CertPrefix))..." -Level Info
|
||
|
|
|
||
|
|
# Clear stale files in the certificate cache
|
||
|
|
if (Test-Path $UserFolder) {
|
||
|
|
Get-ChildItem -Path $UserFolder | ForEach-Object {
|
||
|
|
if ($_.PSIsContainer) {
|
||
|
|
Remove-Item $_.FullName -Recurse -Force
|
||
|
|
} else {
|
||
|
|
Remove-Item $_.FullName -Force
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (-not (Test-Path $UserFolder)) {
|
||
|
|
New-Item -ItemType Directory -Path $UserFolder | Out-Null
|
||
|
|
}
|
||
|
|
|
||
|
|
$now = Get-Date
|
||
|
|
$expiration = $now.AddMonths($script:Config.CertValidMonths)
|
||
|
|
# Subject MUST match <Identity Publisher="..."> inside AppxManifest.xml
|
||
|
|
$friendlyName = "PowerToys Dev Sparse Cert Create=$now"
|
||
|
|
$keyFriendly = "PowerToys Dev Sparse Key Create=$now"
|
||
|
|
|
||
|
|
$certStore = 'cert:\CurrentUser\My'
|
||
|
|
$ekuOid = '2.5.29.37'
|
||
|
|
$ekuValue = '1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13'
|
||
|
|
$eku = "$ekuOid={text}$ekuValue"
|
||
|
|
|
||
|
|
$cert = New-SelfSignedCertificate -CertStoreLocation $certStore `
|
||
|
|
-NotAfter $expiration `
|
||
|
|
-Subject $script:Config.CertSubject `
|
||
|
|
-FriendlyName $friendlyName `
|
||
|
|
-KeyFriendlyName $keyFriendly `
|
||
|
|
-KeyDescription $keyFriendly `
|
||
|
|
-TextExtension $eku
|
||
|
|
|
||
|
|
# Export certificate files
|
||
|
|
Set-Content -Path $CertThumbFile -Value $cert.Thumbprint -Force
|
||
|
|
Export-Certificate -Cert $cert -FilePath $CertCerFile -Force | Out-Null
|
||
|
|
}
|
||
|
|
|
||
|
|
# Determine output directory - using PowerToys standard structure
|
||
|
|
# Navigate to PowerToys root (two levels up from src/PackageIdentity)
|
||
|
|
$PowerToysRoot = Split-Path (Split-Path $ProjectRoot -Parent) -Parent
|
||
|
|
$outDir = Join-Path $PowerToysRoot "$Platform\$Configuration"
|
||
|
|
|
||
|
|
if (-not (Test-Path $outDir)) {
|
||
|
|
Write-BuildLog "Creating output directory: $outDir" -Level Info
|
||
|
|
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||
|
|
}
|
||
|
|
|
||
|
|
# PackageIdentity folder (this script location) containing the sparse manifest and assets
|
||
|
|
$sparseDir = $PSScriptRoot
|
||
|
|
$manifestPath = Join-Path $sparseDir 'AppxManifest.xml'
|
||
|
|
if (-not (Test-Path $manifestPath)) { throw "Missing AppxManifest.xml in PackageIdentity folder: $manifestPath" }
|
||
|
|
|
||
|
|
$versionPropsPath = Join-Path $PowerToysRoot 'src\Version.props'
|
||
|
|
$targetManifestVersion = $null
|
||
|
|
$versionCandidate = $null
|
||
|
|
if (Test-Path $versionPropsPath) {
|
||
|
|
try {
|
||
|
|
[xml]$propsXml = Get-Content -Path $versionPropsPath -Raw
|
||
|
|
$versionCandidate = $propsXml.Project.PropertyGroup.Version
|
||
|
|
} catch {
|
||
|
|
Write-BuildLog ("Unable to read version from {0}: {1}" -f $versionPropsPath, $_) -Level Warning
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
Write-BuildLog "Version.props not found at $versionPropsPath; manifest version will remain unchanged." -Level Warning
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($versionCandidate) {
|
||
|
|
$targetManifestVersion = $versionCandidate.Trim()
|
||
|
|
if (($targetManifestVersion -split '\.').Count -lt 4) {
|
||
|
|
$targetManifestVersion = "$targetManifestVersion.0"
|
||
|
|
}
|
||
|
|
Write-BuildLog "Using sparse package version from Version.props: $targetManifestVersion" -Level Info
|
||
|
|
} else {
|
||
|
|
Write-BuildLog "No version value provided; manifest version will remain unchanged." -Level Info
|
||
|
|
}
|
||
|
|
|
||
|
|
# Find MakeAppx.exe from Windows SDK
|
||
|
|
try {
|
||
|
|
$hostSdkArchitecture = if ([System.Environment]::Is64BitProcess) { 'x64' } else { 'x86' }
|
||
|
|
$makeAppxPath = Find-WindowsSDKTool -ToolName "makeappx.exe" -Architecture $hostSdkArchitecture
|
||
|
|
} catch {
|
||
|
|
Write-Error "MakeAppx.exe not found. Please ensure Windows SDK is installed."
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
# Pack sparse MSIX from PackageIdentity folder
|
||
|
|
$msixPath = Join-Path $outDir $script:Config.SparseMsixName
|
||
|
|
|
||
|
|
# Clean up existing MSIX file
|
||
|
|
if (Test-Path $msixPath) {
|
||
|
|
Write-BuildLog "Removing existing MSIX file..." -Level Info
|
||
|
|
try {
|
||
|
|
Remove-Item $msixPath -Force -ErrorAction Stop
|
||
|
|
Write-BuildLog "Successfully removed existing MSIX file" -Level Success
|
||
|
|
} catch {
|
||
|
|
Write-BuildLog ("Warning: Could not remove existing MSIX file: {0}" -f $_) -Level Warning
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Create a clean staging directory to avoid file lock issues
|
||
|
|
$stagingDir = Join-Path $outDir "staging"
|
||
|
|
if (Test-Path $stagingDir) {
|
||
|
|
Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
|
||
|
|
}
|
||
|
|
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
|
||
|
|
|
||
|
|
try {
|
||
|
|
Write-BuildLog "Creating clean staging directory for packaging..." -Level Info
|
||
|
|
|
||
|
|
# Copy only essential files to staging directory to avoid file locks
|
||
|
|
$essentialFiles = @(
|
||
|
|
"AppxManifest.xml"
|
||
|
|
"Images\*"
|
||
|
|
)
|
||
|
|
|
||
|
|
foreach ($filePattern in $essentialFiles) {
|
||
|
|
$sourcePath = Join-Path $sparseDir $filePattern
|
||
|
|
$relativePath = $filePattern
|
||
|
|
|
||
|
|
if ($filePattern.Contains('\')) {
|
||
|
|
$targetDir = Join-Path $stagingDir (Split-Path $relativePath -Parent)
|
||
|
|
if (-not (Test-Path $targetDir)) {
|
||
|
|
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($filePattern.EndsWith('\*')) {
|
||
|
|
# Copy directory contents
|
||
|
|
$sourceDir = $sourcePath.TrimEnd('\*')
|
||
|
|
$targetDir = Join-Path $stagingDir (Split-Path $relativePath.TrimEnd('\*') -Parent)
|
||
|
|
if (Test-Path $sourceDir) {
|
||
|
|
Copy-Item -Path "$sourceDir\*" -Destination $targetDir -Force -ErrorAction SilentlyContinue
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
# Copy single file
|
||
|
|
$targetPath = Join-Path $stagingDir $relativePath
|
||
|
|
if (Test-Path $sourcePath) {
|
||
|
|
Copy-Item -Path $sourcePath -Destination $targetPath -Force -ErrorAction SilentlyContinue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Ensure publisher matches the dev certificate for local builds
|
||
|
|
$manifestStagingPath = Join-Path $stagingDir 'AppxManifest.xml'
|
||
|
|
$shouldUseDevPublisher = -not $isCIBuild
|
||
|
|
if (Test-Path $manifestStagingPath) {
|
||
|
|
try {
|
||
|
|
[xml]$manifestXml = Get-Content -Path $manifestStagingPath -Raw
|
||
|
|
$identityNode = $manifestXml.Package.Identity
|
||
|
|
$manifestChanged = $false
|
||
|
|
if ($identityNode) {
|
||
|
|
$currentPublisherHint = $identityNode.Publisher
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($identityNode) {
|
||
|
|
if ($targetManifestVersion -and $identityNode.Version -ne $targetManifestVersion) {
|
||
|
|
Write-BuildLog "Updating manifest version to $targetManifestVersion" -Level Info
|
||
|
|
$identityNode.SetAttribute('Version', $targetManifestVersion)
|
||
|
|
$manifestChanged = $true
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($shouldUseDevPublisher -and $identityNode.Publisher -ne $script:Config.CertSubject) {
|
||
|
|
Write-BuildLog "Updating manifest publisher for local build" -Level Warning
|
||
|
|
$identityNode.SetAttribute('Publisher', $script:Config.CertSubject)
|
||
|
|
$manifestChanged = $true
|
||
|
|
}
|
||
|
|
$currentPublisherHint = $identityNode.Publisher
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($manifestChanged) {
|
||
|
|
$manifestXml.Save($manifestStagingPath)
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
Write-BuildLog ("Unable to adjust manifest metadata: {0}" -f $_) -Level Warning
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-BuildLog "Staging directory prepared with essential files only" -Level Success
|
||
|
|
|
||
|
|
# Pack MSIX using staging directory
|
||
|
|
Write-BuildLog "Packing sparse MSIX ($($script:Config.SparseMsixName)) from staging -> $msixPath" -Level Info
|
||
|
|
|
||
|
|
& $makeAppxPath pack /d $stagingDir /p $msixPath /nv /o
|
||
|
|
|
||
|
|
if ($LASTEXITCODE -eq 0 -and (Test-Path $msixPath)) {
|
||
|
|
Write-BuildLog "MSIX packaging completed successfully" -Level Success
|
||
|
|
} else {
|
||
|
|
Write-BuildLog "MakeAppx failed with exit code $LASTEXITCODE" -Level Error
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
# Clean up staging directory
|
||
|
|
if (Test-Path $stagingDir) {
|
||
|
|
try {
|
||
|
|
Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
|
||
|
|
Write-BuildLog "Cleaned up staging directory" -Level Info
|
||
|
|
} catch {
|
||
|
|
Write-BuildLog ("Warning: Could not clean up staging directory: {0}" -f $_) -Level Warning
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Sign package (skip if NoSign specified for CI scenarios)
|
||
|
|
if ($NoSign) {
|
||
|
|
Write-BuildLog "Skipping signing (NoSign specified for CI build)" -Level Warning
|
||
|
|
} else {
|
||
|
|
# Use certificate thumbprint for signing (safer, no password)
|
||
|
|
$certThumbprint = (Get-Content -Path $CertThumbFile -Raw).Trim()
|
||
|
|
try {
|
||
|
|
$signToolPath = Find-WindowsSDKTool -ToolName "signtool.exe"
|
||
|
|
} catch {
|
||
|
|
Write-Error "SignTool.exe not found. Please ensure Windows SDK is installed."
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
Write-BuildLog "Signing sparse MSIX using cert thumbprint $certThumbprint..." -Level Info
|
||
|
|
& $signToolPath sign /fd SHA256 /sha1 $certThumbprint $msixPath
|
||
|
|
if ($LASTEXITCODE -ne 0) {
|
||
|
|
Write-Warning "SignTool failed (exit $LASTEXITCODE). Ensure the certificate is in CurrentUser\\My and try -ForceCert if needed."
|
||
|
|
exit $LASTEXITCODE
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$publisherHintFile = Join-Path $UserFolder "$($script:Config.CertPrefix).publisher.txt"
|
||
|
|
try {
|
||
|
|
Set-Content -Path $publisherHintFile -Value $currentPublisherHint -Force -NoNewline
|
||
|
|
} catch {
|
||
|
|
Write-BuildLog ("Unable to write publisher hint: {0}" -f $_) -Level Warning
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-BuildLog "`nPackage created: $msixPath" -Level Success
|
||
|
|
|
||
|
|
if ($NoSign) {
|
||
|
|
Write-BuildLog "UNSIGNED package created for CI build. Sign before deployment." -Level Warning
|
||
|
|
} else {
|
||
|
|
Write-BuildLog "Install the dev certificate (once): $CertCerFile" -Level Info
|
||
|
|
Write-BuildLog "Identity Name: $($script:Config.IdentityName)" -Level Info
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-BuildLog "Register sparse package:" -Level Info
|
||
|
|
Write-BuildLog " Add-AppxPackage -Path `"$msixPath`" -ExternalLocation `"$outDir`"" -Level Warning
|
||
|
|
Write-BuildLog "(If already installed and you changed manifest only): Add-AppxPackage -Register `"$manifestPath`" -ExternalLocation `"$outDir`" -ForceApplicationShutdown" -Level Warning
|