<# .SYNOPSIS Builds the CursorLang MSIX package for the Microsoft Store. .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 is where it gets everything else done to it. 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 # A check on your own machine: your architecture alone pwsh -File Packaging\build-msix.ps1 -Architectures x64 .EXAMPLE # Build and install in one go, to click through the application pwsh -File Packaging\build-msix.ps1 -Architectures x64 -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 12345AleksandrNeychev.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 Neychev', [string] $PublisherDisplayName = 'Aleksandr Neychev', [ValidateSet('x64', 'arm64')] [string[]] $Architectures = @('x64', 'arm64'), [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' } $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) { # Both things below are checked before the build rather than after it: the # build takes minutes, and neither of them gets 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.' } $machineArchitecture = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } if ($Architectures -notcontains $machineArchitecture) { throw "This machine is $machineArchitecture, and that architecture is not being built. Add it to -Architectures, or drop -Install." } } 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 $built = @() foreach ($architecture in $Architectures) { Write-Host "Building $architecture..." -ForegroundColor Cyan $layout = Join-Path $layoutRoot $architecture foreach ($half in @($agentProject, $settingsProject)) { Invoke-Tool -Path 'dotnet' -Arguments @( 'publish', $half, '--configuration', 'Release', '--runtime', "win-$architecture", '--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}', $architecture) Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8 $package = Join-Path $packagesPath "CursorLang-$Version-$architecture.msix" Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package) $built += $package } $result = $built[0] if ($built.Count -gt 1) { Write-Host 'Bringing the architectures into one package...' -ForegroundColor Cyan # makeappx bundle takes everything from a folder, so the separate packages # are gathered into one of their own first — otherwise the results of # earlier builds would end up in the bundle $bundleInput = Join-Path $OutputPath 'bundle' Remove-Item $bundleInput -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null $built | ForEach-Object { Copy-Item $_ -Destination $bundleInput } $result = Join-Path $packagesPath "CursorLang-$Version.msixbundle" Invoke-Tool -Path $makeappx -Arguments @('bundle', '/o', '/d', $bundleInput, '/p', $result, '/bv', $Version) Remove-Item $bundleInput -Recurse -Force } if ($Install) { Write-Host "Installing the $machineArchitecture build..." -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 $layoutRoot "$machineArchitecture\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" }