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
|
# 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
|
"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
|
- name: Show the toolchain
|
||||||
run: dotnet --info
|
run: dotnet --info
|
||||||
|
|
||||||
@@ -128,6 +132,12 @@ jobs:
|
|||||||
|
|
||||||
./Packaging/build-msix.ps1 @arguments
|
./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
|
# The artifact is where the package waits to be uploaded to Partner Center
|
||||||
- name: Keep the package
|
- name: Keep the package
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -135,3 +145,52 @@ jobs:
|
|||||||
name: msix-${{ steps.version.outputs.version }}
|
name: msix-${{ steps.version.outputs.version }}
|
||||||
path: artifacts/packages/
|
path: artifacts/packages/
|
||||||
if-no-files-found: error
|
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] $Publisher = 'CN=Aleksandr Neichev',
|
||||||
[string] $PublisherDisplayName = 'Aleksandr Neichev',
|
[string] $PublisherDisplayName = 'Aleksandr Neichev',
|
||||||
|
|
||||||
[ValidateSet('x64', 'arm64')]
|
|
||||||
[string[]] $Architectures = @('x64', 'arm64'),
|
|
||||||
|
|
||||||
[switch] $Install,
|
[switch] $Install,
|
||||||
|
|
||||||
[string] $OutputPath
|
[string] $OutputPath
|
||||||
@@ -187,7 +184,7 @@ foreach ($installed in @(Get-AppxPackage -Name $IdentityName)) {
|
|||||||
Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
|
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
|
||||||
|
|
||||||
$built = @()
|
Write-Host 'Building...' -ForegroundColor Cyan
|
||||||
|
|
||||||
$layout = Join-Path $layoutRoot 'x64'
|
$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
|
Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8
|
||||||
|
|
||||||
$package = Join-Path $packagesPath "CursorLang-$Version-$architecture.msix"
|
$result = Join-Path $packagesPath "CursorLang-$Version-x64.msix"
|
||||||
Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package)
|
Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $result)
|
||||||
|
|
||||||
$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) {
|
if ($Install) {
|
||||||
Write-Host 'Installing...' -ForegroundColor Cyan
|
Write-Host 'Installing...' -ForegroundColor Cyan
|
||||||
|
|||||||
@@ -282,9 +282,6 @@ Partner Center как есть.
|
|||||||
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
|
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
|
||||||
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
|
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
|
||||||
|
|
||||||
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
|
|
||||||
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
|
|
||||||
|
|
||||||
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
||||||
MSIX не может установить среду выполнения как зависимость пакета.
|
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
|
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.
|
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
|
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.
|
MSIX cannot install a runtime as a package dependency.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user