<# .SYNOPSIS Builds the CursorLang installer for handing the application round outside the Store. .DESCRIPTION Nothing has to be installed beyond the .NET SDK: WiX arrives as a NuGet package, the same way makeappx does for the MSIX build. Nobody signs the result, so Windows warns about an unknown publisher and the user has to insist. Short of a certificate from a trusted authority there is no way round that, and it is the reason this installer is meant for people who already know where the file came from. The application is published with its own copy of .NET: Windows carries no runtime of its own, and a self-contained build is tied to x64. .PARAMETER Install Runs the installer once it is built, to see the thing through the way a user would. .EXAMPLE # Build and run it, to see what a user sees pwsh -File Packaging\build-installer.ps1 -Install .EXAMPLE # What a release needs pwsh -File Packaging\build-installer.ps1 -Version 1.0.1 #> [CmdletBinding()] param( # Three numbers: unlike the Store, an installer keeps no place for a fourth [string] $Version = '1.0.0', [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' $installerProject = Join-Path $root 'Installer\CursorLang.wixproj' $icon = Join-Path $repository 'CursorLang.Core\Resources\CursorLang.ico' $license = Join-Path $repository 'LICENSE' if (-not $OutputPath) { $OutputPath = Join-Path $repository 'artifacts' } $stagingRoot = Join-Path $OutputPath 'installer-staging' $installersPath = Join-Path $OutputPath 'installers' if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw "The version '$Version' does not fit: an installer is versioned by three numbers, for example 1.0.0." } if (-not (Test-Path $icon)) { throw "The icon is missing from '$icon'. Git LFS may have left a pointer in its place: run git lfs pull." } 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 Write-LicenseRtf { <# .SYNOPSIS Turns the plain-text LICENSE into the RTF the wizard needs. .DESCRIPTION The licence page of a Windows Installer wizard reads RTF and nothing else, and keeping a second copy of the licence in the repository would mean keeping the two in step by hand. The conversion is as plain as it looks: the text carries no formatting to preserve. #> param([string] $Path) $text = (Get-Content $license -Raw) -replace '\\', '\\\\' -replace '{', '\{' -replace '}', '\}' $body = ($text -split "`r?`n") -join '\par' + "`r`n" Set-Content -Path $Path -Value "{\rtf1\ansi\deff0{\fonttbl{\f0 Segoe UI;}}\fs18 $body}" -Encoding ASCII } Remove-Item $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $installersPath -Force | Out-Null $licenseRtf = Join-Path $stagingRoot 'License.rtf' New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null Write-LicenseRtf -Path $licenseRtf Write-Host 'Building...' -ForegroundColor Cyan $staging = Join-Path $stagingRoot 'x64' # Both halves go into one folder: each of them looks for the other beside # itself — the tray menu opens the settings window, and the settings window # registers the agent for startup foreach ($half in @($agentProject, $settingsProject)) { Invoke-Tool -Path 'dotnet' -Arguments @( 'publish', $half, '--configuration', 'Release', '--runtime', 'win-x64', '--self-contained', 'true', "-p:Version=$Version", '--output', $staging, '--nologo' ) } # Debug symbols only make the download bigger; they are of no use to anyone # who installs the application Get-ChildItem $staging -Recurse -Filter '*.pdb' | Remove-Item -Force $installer = Join-Path $installersPath "CursorLang-$Version-x64.msi" Invoke-Tool -Path 'dotnet' -Arguments @( 'build', $installerProject, '--configuration', 'Release', # The architecture of the package is the architecture of what goes in it: # a self-contained build fits nothing else '-p:InstallerPlatform=x64', # Handed over one by one rather than as a ready list of constants: a # semicolon in a property value is where MSBuild stops reading it. The # project gathers them into DefineConstants itself "-p:CursorLangVersion=$Version", "-p:PayloadDir=$staging", "-p:IconFile=$icon", "-p:LicenseFile=$licenseRtf", "-p:OutputPath=$installersPath\", "-p:OutputName=CursorLang-$Version-x64", '--nologo' ) if ($Install) { Write-Host "Running $([System.IO.Path]::GetFileName($installer))..." -ForegroundColor Cyan # Started the way a user would, with the wizard showing: the point of -Install # is to see what the person on the other end sees Start-Process 'msiexec.exe' -ArgumentList '/i', "`"$installer`"" -Wait } Write-Host '' Write-Host 'Done.' -ForegroundColor Green Write-Host " $installer" Write-Host '' Write-Host ' Nobody signed it, so Windows warns about an unknown publisher.'