<# .SYNOPSIS Builds the CursorLang MSIX package. .DESCRIPTION Neither Visual Studio nor the Windows SDK is needed: makeappx arrives as a NuGet package (Tools\SdkTools.csproj) and the application is built by the plain .NET SDK. The package is handed to Partner Center as it comes out of here — the Store puts its own signature on it and does the rest. Nothing is signed here: a publicly trusted code signing certificate is not to be had, and the Store asks for none. The application is published with its own copy of .NET: Windows does not carry one, and MSIX cannot install the runtime as a package dependency. .PARAMETER IdentityName The identity of the package — from Partner Center, the "Product identity" section. It is handed out there together with the reserved application name; the default is only good enough for a check on your own machine. .PARAMETER Install Puts the package onto this machine to see it working. Developer mode has to be on; a signature is not needed, because what gets registered is the layout the package is made of rather than the package file. .EXAMPLE # Build and install in one go, to click through the application pwsh -File Packaging\build-msix.ps1 -Install .EXAMPLE # A build for the Store — the identity comes from Partner Center pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 ` -IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..." #> [CmdletBinding()] param( # Four numbers, the last one has to be 0: that is what the Store requires [string] $Version = '1.0.0.0', [string] $IdentityName = 'CursorLang', [string] $Publisher = 'CN=Aleksandr Neichev', [string] $PublisherDisplayName = 'Aleksandr Neichev', [switch] $Install, [string] $OutputPath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $root = $PSScriptRoot $repository = Split-Path -Parent $root $agentProject = Join-Path $repository 'CursorLang.Agent\CursorLang.Agent.csproj' $settingsProject = Join-Path $repository 'CursorLang.Settings\CursorLang.Settings.csproj' $assets = Join-Path $root 'Assets' $manifestTemplate = Join-Path $root 'AppxManifest.xml' $toolsProject = Join-Path $root 'Tools\SdkTools.csproj' if (-not $OutputPath) { $OutputPath = Join-Path $repository 'artifacts' } elseif (-not [System.IO.Path]::IsPathRooted($OutputPath)) { # Windows names the folder a package was registered from in full, and the # layout is looked for by that name below. A relative path would never be # found there, and the registration of a build gone by would be left behind $OutputPath = Join-Path (Get-Location).Path $OutputPath } $layoutRoot = Join-Path $OutputPath 'layout' $packagesPath = Join-Path $OutputPath 'packages' if ($Version -notmatch '^\d+\.\d+\.\d+\.0$') { throw "The version '$Version' does not fit: the Store takes four numbers ending in zero, for example 1.0.0.0." } if (-not (Test-Path $assets)) { throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first." } if ($Install) { # Checked before the build rather than after it: the build takes minutes, and # this does not get any truer while it runs $developerMode = Get-ItemPropertyValue ` 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' ` -Name 'AllowDevelopmentWithoutDevLicense' -ErrorAction SilentlyContinue if ($developerMode -ne 1) { throw 'Installing needs developer mode: Settings - System - For developers - Developer mode. Without a signature Windows registers a package no other way.' } } function Invoke-Tool { <# .SYNOPSIS Runs a program and fails the build if it returned an error. .DESCRIPTION A wrapper of our own is needed because PowerShell does not treat the failure of an external program as an error and quietly moves on. #> param( [string] $Path, [string[]] $Arguments ) & $Path @Arguments if ($LASTEXITCODE -ne 0) { throw "'$([System.IO.Path]::GetFileName($Path))' exited with code $LASTEXITCODE." } } function Get-SdkToolsPath { <# .SYNOPSIS Returns the folder of the restored Windows SDK Build Tools package. .DESCRIPTION The path is asked of MSBuild rather than searched for in the package cache: the project pins one version of the package, and MSBuild names exactly it. A search would also find the versions left over from other builds. #> $path = (& dotnet msbuild $toolsProject -getProperty:PkgMicrosoft_Windows_SDK_BuildTools -nologo) | Where-Object { $_ -and $_.Trim() } | Select-Object -Last 1 if ($LASTEXITCODE -ne 0 -or -not $path -or -not (Test-Path $path.Trim())) { throw 'The Windows SDK Build Tools package folder could not be found. Did the restore go through?' } return $path.Trim() } function Get-SdkTool { <# .SYNOPSIS Returns the path of a program from the Windows SDK Build Tools package. #> param( [string] $Name, [string] $PackagePath ) # The bitness of the program has nothing to do with the bitness of the # package being built: we take the one that runs on this machine $host64 = if ([Environment]::Is64BitOperatingSystem) { 'x64' } else { 'x86' } $tool = Get-ChildItem (Join-Path $PackagePath 'bin') -Recurse -Filter $Name -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match "\\$host64\\" } | Select-Object -First 1 if (-not $tool) { throw "'$Name' was not found in the Windows SDK Build Tools package." } return $tool.FullName } Write-Host 'Fetching the Windows SDK programs...' -ForegroundColor Cyan Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo') $sdkTools = Get-SdkToolsPath $makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools # A package registered out of the layout runs straight from that folder, and # the folder is about to be wiped. Left in place, the registration would point # at files that are gone — an application that neither starts nor uninstalls # the usual way. This happens whatever the build was asked for, so -Install has # no say in it. # # Only a registration made from this very folder is removed. A copy installed # from the Store answers to the same name and is none of our business foreach ($installed in @(Get-AppxPackage -Name $IdentityName)) { $location = $installed.InstallLocation if (-not $location) { continue } if (-not $location.TrimEnd('\').StartsWith($layoutRoot.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)) { continue } Write-Host "Removing the registered $($installed.PackageFullName)..." -ForegroundColor Cyan Remove-AppxPackage -Package $installed.PackageFullName } Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null Write-Host 'Building...' -ForegroundColor Cyan $layout = Join-Path $layoutRoot 'x64' foreach ($half in @($agentProject, $settingsProject)) { Invoke-Tool -Path 'dotnet' -Arguments @( 'publish', $half, '--configuration', 'Release', '--runtime', 'win-x64', '--self-contained', 'true', "-p:Version=$($Version.Substring(0, $Version.LastIndexOf('.')))", '--output', $layout, '--nologo' ) } # Debug symbols have no place in the package: they take up room, the user # has no use for them, and for crash reports the Store takes them separately Get-ChildItem $layout -Recurse -Filter '*.pdb' | Remove-Item -Force Copy-Item $assets -Destination (Join-Path $layout 'Assets') -Recurse -Force $manifest = (Get-Content $manifestTemplate -Raw). Replace('{IdentityName}', $IdentityName). Replace('{Publisher}', $Publisher). Replace('{PublisherDisplayName}', $PublisherDisplayName). Replace('{Version}', $Version). Replace('{Architecture}', 'x64') Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8 $result = Join-Path $packagesPath "CursorLang-$Version-x64.msix" Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $result) if ($Install) { Write-Host 'Installing...' -ForegroundColor Cyan # The layout is registered rather than the package file: the two hold the # same thing, but a package file Windows only installs when it is signed Add-AppxPackage -Register (Join-Path $layout 'AppxManifest.xml') } Write-Host '' Write-Host 'Done.' -ForegroundColor Green Write-Host " $result" Write-Host '' Write-Host ' This file is uploaded to Partner Center as it is.' if ($Install) { Write-Host '' Write-Host ' The application is installed and runs out of the layout folder, so' Write-Host ' building again would pull the files out from under it. This script' Write-Host ' takes care of that itself; to remove the application by hand:' Write-Host " Remove-AppxPackage (Get-AppxPackage -Name $IdentityName).PackageFullName" }