added msi to release pipeline
This commit is contained in:
@@ -84,6 +84,10 @@ jobs:
|
||||
# reserves the last one, so it carries nothing the tag could tell
|
||||
"version=$($tag.Substring(1)).0" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
|
||||
# The installer answers to nobody about a fourth number and takes the
|
||||
# tag as it is
|
||||
"plain=$($tag.Substring(1))" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
|
||||
- name: Show the toolchain
|
||||
run: dotnet --info
|
||||
|
||||
@@ -128,6 +132,12 @@ jobs:
|
||||
|
||||
./Packaging/build-msix.ps1 @arguments
|
||||
|
||||
# The other half of the release: the same application as an ordinary
|
||||
# installer, for handing round outside the Store. Nobody signs it, so
|
||||
# SmartScreen warns about it — see Packaging\installer.iss
|
||||
- name: Build the installer
|
||||
run: ./Packaging/build-installer.ps1 -Version ${{ steps.version.outputs.plain }}
|
||||
|
||||
# The artifact is where the package waits to be uploaded to Partner Center
|
||||
- name: Keep the package
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -135,3 +145,52 @@ jobs:
|
||||
name: msix-${{ steps.version.outputs.version }}
|
||||
path: artifacts/packages/
|
||||
if-no-files-found: error
|
||||
|
||||
# The .wixpdb next to each installer is left out on purpose: it is of use
|
||||
# only when something has to be traced back to the WiX source
|
||||
- name: Keep the installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: installer-${{ steps.version.outputs.plain }}
|
||||
path: artifacts/installers/*.msi
|
||||
if-no-files-found: error
|
||||
|
||||
# Only the installers go into the release. The MSIX stays in the artifacts
|
||||
# of the run: unsigned, it installs nowhere, and its one destination is
|
||||
# Partner Center
|
||||
- name: Publish the release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# The GITHUB_ names are what Gitea itself hands to the workflow — its
|
||||
# actions repeat those of GitHub, and the addresses in them point at
|
||||
# this Gitea instance. GITHUB_API_URL used not to reach the steps at
|
||||
# all, so the address is put together from the server one when empty
|
||||
$root = if ($env:GITHUB_API_URL) { $env:GITHUB_API_URL } else { "$env:GITHUB_SERVER_URL/api/v1" }
|
||||
$api = "$root/repos/$env:GITHUB_REPOSITORY/releases"
|
||||
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
|
||||
|
||||
# Gitea makes a release of its own for a pushed tag, so the release is
|
||||
# looked up first and only made when it is not there
|
||||
$release = $null
|
||||
try { $release = Invoke-RestMethod "$api/tags/$env:TAG" -Headers $headers } catch { }
|
||||
|
||||
if (-not $release) {
|
||||
$body = @{ tag_name = $env:TAG; name = $env:TAG; draft = $false; prerelease = $false } | ConvertTo-Json
|
||||
$release = Invoke-RestMethod $api -Method Post -Headers $headers -ContentType 'application/json' -Body $body
|
||||
}
|
||||
|
||||
foreach ($file in Get-ChildItem artifacts/installers -File -Filter *.msi) {
|
||||
# A tag can be pushed again after it was deleted; the old file of
|
||||
# the same name is dropped, otherwise the upload is refused
|
||||
$existing = $release.assets | Where-Object { $_.name -eq $file.Name }
|
||||
foreach ($asset in $existing) {
|
||||
Invoke-RestMethod "$api/$($release.id)/assets/$($asset.id)" -Method Delete -Headers $headers | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "Uploading $($file.Name)"
|
||||
Invoke-RestMethod "$api/$($release.id)/assets?name=$($file.Name)" -Method Post -Headers $headers -Form @{ attachment = $file } | Out-Null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<!--
|
||||
The installer project. It is not part of CursorLang.sln and is not meant to be
|
||||
built on its own: build-installer.ps1 publishes the application first and then
|
||||
builds this, passing in the folder it published to.
|
||||
|
||||
WiX 5 rather than the current 7: from version 6 onwards the toolset asks every
|
||||
build to accept the Open Source Maintenance Fee licence, which is a decision for
|
||||
a person to make and not for a build script. Version 5 carries no such
|
||||
requirement. Moving up later means changing the version here and adding
|
||||
-p:AcceptEula=true to the build.
|
||||
-->
|
||||
<Project Sdk="WixToolset.Sdk/5.0.2">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputName>CursorLang</OutputName>
|
||||
<OutputType>Package</OutputType>
|
||||
|
||||
<!--
|
||||
MSI validation still assumes an installation for the whole machine.
|
||||
Installing into the user's own profile trips three of its rules: a
|
||||
component whose key path is a file (ICE38), a folder it wants listed
|
||||
for removal (ICE64), and a warning that the files will not follow other
|
||||
users of the machine (ICE91). All three describe exactly what was
|
||||
intended here, so they are turned off rather than worked around.
|
||||
-->
|
||||
<SuppressIces>ICE38;ICE64;ICE91</SuppressIces>
|
||||
|
||||
<!--
|
||||
What the script passes in, gathered into the constants the source file
|
||||
reads. They are gathered here rather than handed over as one property
|
||||
from the command line: a semicolon in a property value is where MSBuild
|
||||
stops reading it, and the separator between constants is a semicolon.
|
||||
-->
|
||||
<DefineConstants>
|
||||
Version=$(CursorLangVersion);
|
||||
PayloadDir=$(PayloadDir);
|
||||
IconFile=$(IconFile);
|
||||
LicenseFile=$(LicenseFile)
|
||||
</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- CloseApplication and the launch at the end of the wizard come from here -->
|
||||
<PackageReference Include="WixToolset.Util.wixext" Version="5.0.2" />
|
||||
<PackageReference Include="WixToolset.UI.wixext" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,163 @@
|
||||
<#
|
||||
.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.'
|
||||
@@ -43,9 +43,6 @@ param(
|
||||
[string] $Publisher = 'CN=Aleksandr Neichev',
|
||||
[string] $PublisherDisplayName = 'Aleksandr Neichev',
|
||||
|
||||
[ValidateSet('x64', 'arm64')]
|
||||
[string[]] $Architectures = @('x64', 'arm64'),
|
||||
|
||||
[switch] $Install,
|
||||
|
||||
[string] $OutputPath
|
||||
@@ -187,7 +184,7 @@ foreach ($installed in @(Get-AppxPackage -Name $IdentityName)) {
|
||||
Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
|
||||
|
||||
$built = @()
|
||||
Write-Host 'Building...' -ForegroundColor Cyan
|
||||
|
||||
$layout = Join-Path $layoutRoot 'x64'
|
||||
|
||||
@@ -218,30 +215,8 @@ $manifest = (Get-Content $manifestTemplate -Raw).
|
||||
|
||||
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
|
||||
}
|
||||
$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
|
||||
|
||||
@@ -282,9 +282,6 @@ Partner Center как есть.
|
||||
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
|
||||
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
|
||||
|
||||
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
|
||||
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
|
||||
|
||||
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
||||
MSIX не может установить среду выполнения как зависимость пакета.
|
||||
|
||||
|
||||
@@ -276,9 +276,6 @@ sake of machines that run the x64 one under emulation anyway.
|
||||
Nothing here is signed: the Store signs the package itself, and for installing it
|
||||
on this machine the layout is registered instead — see `-Install` below.
|
||||
|
||||
Nothing here is signed: the Store signs the package itself, and for installing it
|
||||
on this machine the layout is registered instead — see `-Install` below.
|
||||
|
||||
The app ships with its own copy of .NET: Windows does not include .NET 10, and
|
||||
MSIX cannot install a runtime as a package dependency.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user