This commit is contained in:
@@ -2,10 +2,17 @@
|
|||||||
# packs the MSIX with the version taken from the tag — three numbers of the tag
|
# packs the MSIX with the version taken from the tag — three numbers of the tag
|
||||||
# and a zero the Store keeps for itself.
|
# and a zero the Store keeps for itself.
|
||||||
#
|
#
|
||||||
|
# The package is built twice, because the two places it goes to want different
|
||||||
|
# things of it. The Store gets a package with the identity reserved in Partner
|
||||||
|
# Center and no signature — Partner Center signs it there. The release gets a
|
||||||
|
# package signed here, with SSL.com's certificate the private key of which never
|
||||||
|
# leaves their HSM; Windows installs nothing else. The two only differ inside,
|
||||||
|
# so the Store one carries a suffix in its name and never leaves the artifacts.
|
||||||
|
#
|
||||||
# The same requirements to the runner as in pull-request.yml apply: Windows, the
|
# The same requirements to the runner as in pull-request.yml apply: Windows, the
|
||||||
# .NET 10 SDK and an interactive desktop session for the tests. makeappx comes
|
# .NET 10 SDK and an interactive desktop session for the tests. makeappx and
|
||||||
# with a NuGet package (Packaging\Tools\SdkTools.csproj), so the Windows SDK does
|
# signtool come with a NuGet package (Packaging\Tools\SdkTools.csproj), so the
|
||||||
# not have to be installed.
|
# Windows SDK does not have to be installed.
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -77,6 +84,25 @@ 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 signing account is asked about before anything is built rather than
|
||||||
|
# at the step that needs it: a release without a signed package is not a
|
||||||
|
# release, and finding that out after the build and the tests costs the
|
||||||
|
# whole run
|
||||||
|
- name: Check the signing credentials
|
||||||
|
env:
|
||||||
|
ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }}
|
||||||
|
ESIGNER_PASSWORD: ${{ secrets.ESIGNER_PASSWORD }}
|
||||||
|
ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$missing = @('ESIGNER_USERNAME', 'ESIGNER_PASSWORD', 'ESIGNER_TOTP_SECRET') |
|
||||||
|
Where-Object { -not (Get-Item "Env:$_" -ErrorAction SilentlyContinue).Value }
|
||||||
|
|
||||||
|
if ($missing) {
|
||||||
|
throw "The repository secrets $($missing -join ', ') are not set. They are the SSL.com account the package is signed with; the TOTP secret is the one eSigner hands out for automated signing, not a six-digit code."
|
||||||
|
}
|
||||||
|
|
||||||
- name: Show the toolchain
|
- name: Show the toolchain
|
||||||
run: dotnet --info
|
run: dotnet --info
|
||||||
|
|
||||||
@@ -97,7 +123,12 @@ jobs:
|
|||||||
# The package comes out as Partner Center wants it — the Store puts its own
|
# The package comes out as Partner Center wants it — the Store puts its own
|
||||||
# signature on it. The identity comes from repository variables and falls
|
# signature on it. The identity comes from repository variables and falls
|
||||||
# back to the defaults of the script when a variable is not set.
|
# back to the defaults of the script when a variable is not set.
|
||||||
- name: Pack the MSIX
|
#
|
||||||
|
# This one is picked up by hand and uploaded to Partner Center, so it goes
|
||||||
|
# no further than the artifacts of the run: attached to the release it
|
||||||
|
# would sit there as a package nobody can install, next to one that
|
||||||
|
# installs — telling the two apart is what the suffix in the name is for
|
||||||
|
- name: Pack the MSIX for the Store
|
||||||
env:
|
env:
|
||||||
IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }}
|
IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }}
|
||||||
PUBLISHER: ${{ vars.MSIX_PUBLISHER }}
|
PUBLISHER: ${{ vars.MSIX_PUBLISHER }}
|
||||||
@@ -105,7 +136,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
$arguments = @{ Version = '${{ steps.version.outputs.version }}' }
|
$arguments = @{
|
||||||
|
Version = '${{ steps.version.outputs.version }}'
|
||||||
|
PackageSuffix = 'store'
|
||||||
|
OutputPath = 'artifacts/store'
|
||||||
|
}
|
||||||
|
|
||||||
# An empty variable is left out rather than passed on: the script has
|
# An empty variable is left out rather than passed on: the script has
|
||||||
# defaults of its own, and an empty string would wipe them
|
# defaults of its own, and an empty string would wipe them
|
||||||
@@ -121,11 +156,123 @@ jobs:
|
|||||||
|
|
||||||
./Packaging/build-msix.ps1 @arguments
|
./Packaging/build-msix.ps1 @arguments
|
||||||
|
|
||||||
- name: Keep the packages
|
- name: Keep the Store packages
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: msix-store-${{ steps.version.outputs.version }}
|
||||||
|
path: artifacts/store/packages/
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# eSigner CKA is a key storage provider: it puts the certificate into the
|
||||||
|
# store of this user and answers signtool's requests for the private key
|
||||||
|
# over SSL.com's API, so the key itself never comes down to the runner.
|
||||||
|
# From signtool's side it looks like a certificate on a token, minus the
|
||||||
|
# token and minus the person who would plug it in.
|
||||||
|
#
|
||||||
|
# The TOTP secret is what stands in for that person: eSigner hands it out
|
||||||
|
# once, for automated signing, and the tool makes the codes out of it
|
||||||
|
# itself
|
||||||
|
- name: Load the signing certificate
|
||||||
|
id: certificate
|
||||||
|
env:
|
||||||
|
CKA_URL: https://github.com/SSLcom/eSignerCKA/releases/download/v1.0.6/SSL.COM-eSigner-CKA_1.0.6.zip
|
||||||
|
ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }}
|
||||||
|
ESIGNER_PASSWORD: ${{ secrets.ESIGNER_PASSWORD }}
|
||||||
|
ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$temporary = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP }
|
||||||
|
$archive = Join-Path $temporary 'eSignerCKA.zip'
|
||||||
|
$unpacked = Join-Path $temporary 'eSignerCKA'
|
||||||
|
|
||||||
|
# Everything of the adapter's own — the installation and the master
|
||||||
|
# key it keeps the account in — lives outside the workspace: the
|
||||||
|
# workspace is what gets packed and uploaded
|
||||||
|
$suite = Join-Path $env:USERPROFILE '.signingsuite'
|
||||||
|
$installation = Join-Path $suite 'eSignerCKA'
|
||||||
|
|
||||||
|
Remove-Item $unpacked -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
New-Item -ItemType Directory -Path $suite -Force | Out-Null
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri $env:CKA_URL -OutFile $archive
|
||||||
|
Expand-Archive -Path $archive -DestinationPath $unpacked -Force
|
||||||
|
|
||||||
|
$installer = Get-ChildItem $unpacked -Recurse -Filter '*.exe' | Select-Object -First 1
|
||||||
|
if (-not $installer) { throw "No installer inside the eSigner CKA archive at '$env:CKA_URL'." }
|
||||||
|
|
||||||
|
# /CURRENTUSER, so that the certificate lands in the store of the user
|
||||||
|
# the build runs as — the same one signtool then looks in
|
||||||
|
& $installer.FullName /CURRENTUSER /VERYSILENT /SUPPRESSMSGBOXES "/DIR=$installation" | Out-Null
|
||||||
|
|
||||||
|
$tool = Join-Path $installation 'eSignerCKATool.exe'
|
||||||
|
if (-not (Test-Path $tool)) { throw "eSigner CKA did not install: '$tool' is not there." }
|
||||||
|
|
||||||
|
& $tool config -mode product -user $env:ESIGNER_USERNAME -pass $env:ESIGNER_PASSWORD -totp $env:ESIGNER_TOTP_SECRET -key (Join-Path $suite 'master.key') -r
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "eSigner CKA turned down the account: the tool exited with code $LASTEXITCODE." }
|
||||||
|
|
||||||
|
# A run of the same runner could have left a certificate loaded from
|
||||||
|
# another account; unload says nothing when there is nothing to
|
||||||
|
# unload, and its exit code is of no interest for that reason
|
||||||
|
& $tool unload | Out-Null
|
||||||
|
|
||||||
|
& $tool load
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "eSigner CKA could not load the certificate: the tool exited with code $LASTEXITCODE." }
|
||||||
|
|
||||||
|
# An account may hold more than one certificate — a renewal leaves the
|
||||||
|
# old one behind — and the one that lives longest is the one to sign
|
||||||
|
# with: a signature made by a certificate about to expire is timestamped
|
||||||
|
# and stays good, but the next release would have to be made anyway
|
||||||
|
$certificate = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert |
|
||||||
|
Sort-Object NotAfter -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $certificate) {
|
||||||
|
throw 'eSigner CKA loaded nothing into the certificate store. Does the account hold a code signing certificate — a document signature is a different thing and cannot sign a package.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Signing as $($certificate.Subject), good until $($certificate.NotAfter.ToString('yyyy-MM-dd'))."
|
||||||
|
|
||||||
|
"thumbprint=$($certificate.Thumbprint)" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||||
|
"subject=$($certificate.Subject)" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||||
|
|
||||||
|
# The package of the release carries the identity the app is known by and
|
||||||
|
# the publisher of the certificate — Windows works out the family name of
|
||||||
|
# a package from the two together, so an update replaces the installed
|
||||||
|
# version only while both stay as they were. The names have no suffix:
|
||||||
|
# that is what the app looks for when it checks for an update
|
||||||
|
- name: Pack and sign the MSIX for the release
|
||||||
|
env:
|
||||||
|
IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }}
|
||||||
|
PUBLISHER: ${{ steps.certificate.outputs.subject }}
|
||||||
|
PUBLISHER_DISPLAY_NAME: ${{ vars.MSIX_PUBLISHER_DISPLAY_NAME }}
|
||||||
|
THUMBPRINT: ${{ steps.certificate.outputs.thumbprint }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$arguments = @{
|
||||||
|
Version = '${{ steps.version.outputs.version }}'
|
||||||
|
OutputPath = 'artifacts/release'
|
||||||
|
Publisher = $env:PUBLISHER
|
||||||
|
CertificateThumbprint = $env:THUMBPRINT
|
||||||
|
}
|
||||||
|
|
||||||
|
$variables = @{
|
||||||
|
IdentityName = $env:IDENTITY_NAME
|
||||||
|
PublisherDisplayName = $env:PUBLISHER_DISPLAY_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($name in $variables.Keys) {
|
||||||
|
if ($variables[$name]) { $arguments[$name] = $variables[$name] }
|
||||||
|
}
|
||||||
|
|
||||||
|
./Packaging/build-msix.ps1 @arguments
|
||||||
|
|
||||||
|
- name: Keep the signed packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: msix-${{ steps.version.outputs.version }}
|
name: msix-${{ steps.version.outputs.version }}
|
||||||
path: artifacts/packages/
|
path: artifacts/release/packages/
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
# Gitea creates a release of its own for a pushed tag, so the release is
|
# Gitea creates a release of its own for a pushed tag, so the release is
|
||||||
@@ -153,7 +300,9 @@ jobs:
|
|||||||
$release = Invoke-RestMethod $api -Method Post -Headers $headers -ContentType 'application/json' -Body $body
|
$release = Invoke-RestMethod $api -Method Post -Headers $headers -ContentType 'application/json' -Body $body
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($file in Get-ChildItem artifacts/packages -File) {
|
# Only the signed packages: the Store one stays in the artifacts of
|
||||||
|
# the run, where whoever uploads it to Partner Center picks it up
|
||||||
|
foreach ($file in Get-ChildItem artifacts/release/packages -File) {
|
||||||
# A tag can be pushed again after it was deleted; the old file of
|
# A tag can be pushed again after it was deleted; the old file of
|
||||||
# the same name is dropped, otherwise the upload is refused
|
# the same name is dropped, otherwise the upload is refused
|
||||||
$existing = $release.assets | Where-Object { $_.name -eq $file.Name }
|
$existing = $release.assets | Where-Object { $_.name -eq $file.Name }
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>1.0.0.0</FileVersion>
|
||||||
<Product>CursorLang</Product>
|
<Product>CursorLang</Product>
|
||||||
<Company>Aleksandr Neychev</Company>
|
<Company>Aleksandr Neichev</Company>
|
||||||
<Description>Shows the keyboard layout at the cursor</Description>
|
<Description>Shows the keyboard layout at the cursor</Description>
|
||||||
<Copyright>Copyright (c) 2026</Copyright>
|
<Copyright>Copyright (c) 2026</Copyright>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>1.0.0.0</FileVersion>
|
||||||
<Product>CursorLang</Product>
|
<Product>CursorLang</Product>
|
||||||
<Company>Aleksandr Neychev</Company>
|
<Company>Aleksandr Neichev</Company>
|
||||||
<Description>Shared part of CursorLang: models, settings, layout tracking, updates</Description>
|
<Description>Shared part of CursorLang: models, settings, layout tracking, updates</Description>
|
||||||
<Copyright>Copyright (c) 2026</Copyright>
|
<Copyright>Copyright (c) 2026</Copyright>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>1.0.0.0</FileVersion>
|
||||||
<Product>CursorLang</Product>
|
<Product>CursorLang</Product>
|
||||||
<Company>Aleksandr Neychev</Company>
|
<Company>Aleksandr Neichev</Company>
|
||||||
<Description>Settings window of CursorLang</Description>
|
<Description>Settings window of CursorLang</Description>
|
||||||
<Copyright>Copyright (c) 2026</Copyright>
|
<Copyright>Copyright (c) 2026</Copyright>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
+108
-12
@@ -1,14 +1,17 @@
|
|||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Builds the CursorLang MSIX package for the Microsoft Store.
|
Builds the CursorLang MSIX package.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Neither Visual Studio nor the Windows SDK is needed: makeappx arrives as a
|
Neither Visual Studio nor the Windows SDK is needed: makeappx and signtool
|
||||||
NuGet package (Tools\SdkTools.csproj) and the application is built by the
|
arrive as a NuGet package (Tools\SdkTools.csproj) and the application is
|
||||||
plain .NET SDK.
|
built by the plain .NET SDK.
|
||||||
|
|
||||||
The package is handed to Partner Center as it comes out of here — the Store
|
The package comes out of here in one of two shapes. Left unsigned it goes to
|
||||||
is where it gets everything else done to it.
|
Partner Center as it is — the Store puts its own signature on it and does the
|
||||||
|
rest. Signed with -CertificateThumbprint it is a package anyone can install
|
||||||
|
from a release, because Windows only takes a package whose signature it
|
||||||
|
trusts.
|
||||||
|
|
||||||
The application is published with its own copy of .NET: Windows does not
|
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.
|
carry one, and MSIX cannot install the runtime as a package dependency.
|
||||||
@@ -18,6 +21,17 @@
|
|||||||
section. It is handed out there together with the reserved application name;
|
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.
|
the default is only good enough for a check on your own machine.
|
||||||
|
|
||||||
|
.PARAMETER PackageSuffix
|
||||||
|
Goes at the end of the file names. The two builds of a release differ in the
|
||||||
|
identity inside them and in nothing a file listing shows, so the one meant
|
||||||
|
for the Store is told apart by a suffix of its own.
|
||||||
|
|
||||||
|
.PARAMETER CertificateThumbprint
|
||||||
|
Signs the packages with the certificate of this thumbprint from the personal
|
||||||
|
store of the current user. The private key is none of this script's business:
|
||||||
|
signtool asks the store for it, and behind a cloud certificate — eSigner CKA
|
||||||
|
among them — the store answers over the network.
|
||||||
|
|
||||||
.PARAMETER Install
|
.PARAMETER Install
|
||||||
Puts the package onto this machine to see it working. Developer mode has to
|
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
|
be on; a signature is not needed, because what gets registered is the layout
|
||||||
@@ -33,8 +47,14 @@
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
# A build for the Store — the identity comes from Partner Center
|
# A build for the Store — the identity comes from Partner Center
|
||||||
|
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 -PackageSuffix store `
|
||||||
|
-IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# A build for a release — the publisher is the subject of the certificate
|
||||||
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
||||||
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
|
-Publisher "CN=Aleksandr Neichev, O=Aleksandr Neichev, C=KZ" `
|
||||||
|
-CertificateThumbprint A1B2C3D4E5F60718293A4B5C6D7E8F9012345678
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
@@ -42,12 +62,21 @@ param(
|
|||||||
[string] $Version = '1.0.0.0',
|
[string] $Version = '1.0.0.0',
|
||||||
|
|
||||||
[string] $IdentityName = 'CursorLang',
|
[string] $IdentityName = 'CursorLang',
|
||||||
[string] $Publisher = 'CN=Aleksandr Neychev',
|
[string] $Publisher = 'CN=Aleksandr Neichev',
|
||||||
[string] $PublisherDisplayName = 'Aleksandr Neychev',
|
[string] $PublisherDisplayName = 'Aleksandr Neichev',
|
||||||
|
|
||||||
[ValidateSet('x64', 'arm64')]
|
[ValidateSet('x64', 'arm64')]
|
||||||
[string[]] $Architectures = @('x64', 'arm64'),
|
[string[]] $Architectures = @('x64', 'arm64'),
|
||||||
|
|
||||||
|
[string] $PackageSuffix,
|
||||||
|
|
||||||
|
[string] $CertificateThumbprint,
|
||||||
|
|
||||||
|
# SSL.com's timestamp server, to go with the certificate the pipeline signs
|
||||||
|
# with. A signature without a timestamp is only good while the certificate
|
||||||
|
# is: the day it expires the package stops installing everywhere at once
|
||||||
|
[string] $TimestampUrl = 'http://ts.ssl.com',
|
||||||
|
|
||||||
[switch] $Install,
|
[switch] $Install,
|
||||||
|
|
||||||
[string] $OutputPath
|
[string] $OutputPath
|
||||||
@@ -64,7 +93,14 @@ $assets = Join-Path $root 'Assets'
|
|||||||
$manifestTemplate = Join-Path $root 'AppxManifest.xml'
|
$manifestTemplate = Join-Path $root 'AppxManifest.xml'
|
||||||
$toolsProject = Join-Path $root 'Tools\SdkTools.csproj'
|
$toolsProject = Join-Path $root 'Tools\SdkTools.csproj'
|
||||||
|
|
||||||
if (-not $OutputPath) { $OutputPath = Join-Path $repository 'artifacts' }
|
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'
|
$layoutRoot = Join-Path $OutputPath 'layout'
|
||||||
$packagesPath = Join-Path $OutputPath 'packages'
|
$packagesPath = Join-Path $OutputPath 'packages'
|
||||||
|
|
||||||
@@ -76,6 +112,26 @@ if (-not (Test-Path $assets)) {
|
|||||||
throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first."
|
throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($CertificateThumbprint) {
|
||||||
|
# Both things below are checked before the build for the same reason the
|
||||||
|
# ones under -Install are: the build takes minutes and neither answer
|
||||||
|
# changes while it runs
|
||||||
|
|
||||||
|
$certificate = Get-ChildItem "Cert:\CurrentUser\My\$CertificateThumbprint" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if (-not $certificate) {
|
||||||
|
throw "No certificate with the thumbprint '$CertificateThumbprint' in the personal store of this user. A cloud certificate has to be loaded into the store first — eSigner CKA is what does that."
|
||||||
|
}
|
||||||
|
|
||||||
|
# The Publisher of a package is not a name of the publisher's choosing: it
|
||||||
|
# is the subject of the certificate the package is signed with, letter for
|
||||||
|
# letter. signtool turns down a package that claims any other, and it does
|
||||||
|
# so at the very end — after everything has already been built
|
||||||
|
if ($certificate.Subject -ne $Publisher) {
|
||||||
|
throw "The publisher '$Publisher' is not the subject of the certificate, which reads '$($certificate.Subject)'. A package carries the name of whoever signs it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($Install) {
|
if ($Install) {
|
||||||
# Both things below are checked before the build rather than after it: the
|
# 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
|
# build takes minutes, and neither of them gets any truer while it runs
|
||||||
@@ -160,11 +216,34 @@ function Get-SdkTool {
|
|||||||
return $tool.FullName
|
return $tool.FullName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Invoke-Signing {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Signs a package with the certificate the build was given.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[string] $Path,
|
||||||
|
[string] $SignTool
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "Signing $([System.IO.Path]::GetFileName($Path))..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
Invoke-Tool -Path $SignTool -Arguments @(
|
||||||
|
'sign',
|
||||||
|
'/fd', 'sha256',
|
||||||
|
'/tr', $TimestampUrl,
|
||||||
|
'/td', 'sha256',
|
||||||
|
'/sha1', $CertificateThumbprint,
|
||||||
|
$Path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host 'Fetching the Windows SDK programs...' -ForegroundColor Cyan
|
Write-Host 'Fetching the Windows SDK programs...' -ForegroundColor Cyan
|
||||||
Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo')
|
Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo')
|
||||||
|
|
||||||
$sdkTools = Get-SdkToolsPath
|
$sdkTools = Get-SdkToolsPath
|
||||||
$makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools
|
$makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools
|
||||||
|
$signtool = if ($CertificateThumbprint) { Get-SdkTool -Name 'signtool.exe' -PackagePath $sdkTools } else { $null }
|
||||||
|
|
||||||
# A package registered out of the layout runs straight from that folder, and
|
# 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
|
# the folder is about to be wiped. Left in place, the registration would point
|
||||||
@@ -189,6 +268,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
|
||||||
|
|
||||||
|
$suffix = if ($PackageSuffix) { "-$PackageSuffix" } else { '' }
|
||||||
$built = @()
|
$built = @()
|
||||||
|
|
||||||
foreach ($architecture in $Architectures) {
|
foreach ($architecture in $Architectures) {
|
||||||
@@ -223,7 +303,7 @@ foreach ($architecture in $Architectures) {
|
|||||||
|
|
||||||
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"
|
$package = Join-Path $packagesPath "CursorLang-$Version-$architecture$suffix.msix"
|
||||||
Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package)
|
Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package)
|
||||||
|
|
||||||
$built += $package
|
$built += $package
|
||||||
@@ -242,12 +322,23 @@ if ($built.Count -gt 1) {
|
|||||||
New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null
|
New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null
|
||||||
$built | ForEach-Object { Copy-Item $_ -Destination $bundleInput }
|
$built | ForEach-Object { Copy-Item $_ -Destination $bundleInput }
|
||||||
|
|
||||||
$result = Join-Path $packagesPath "CursorLang-$Version.msixbundle"
|
$result = Join-Path $packagesPath "CursorLang-$Version$suffix.msixbundle"
|
||||||
Invoke-Tool -Path $makeappx -Arguments @('bundle', '/o', '/d', $bundleInput, '/p', $result, '/bv', $Version)
|
Invoke-Tool -Path $makeappx -Arguments @('bundle', '/o', '/d', $bundleInput, '/p', $result, '/bv', $Version)
|
||||||
|
|
||||||
Remove-Item $bundleInput -Recurse -Force
|
Remove-Item $bundleInput -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($CertificateThumbprint) {
|
||||||
|
# Signing comes after the bundle rather than before it: makeappx copies the
|
||||||
|
# packages into the bundle as they are, and a signature on what lies inside
|
||||||
|
# says nothing about the bundle around it. Windows asks the outer file, so
|
||||||
|
# every file that leaves here is signed on its own — each one of them is a
|
||||||
|
# package someone may install
|
||||||
|
foreach ($package in (@($built) + @($result) | Select-Object -Unique)) {
|
||||||
|
Invoke-Signing -Path $package -SignTool $signtool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($Install) {
|
if ($Install) {
|
||||||
Write-Host "Installing the $machineArchitecture build..." -ForegroundColor Cyan
|
Write-Host "Installing the $machineArchitecture build..." -ForegroundColor Cyan
|
||||||
|
|
||||||
@@ -261,7 +352,12 @@ Write-Host 'Done.' -ForegroundColor Green
|
|||||||
Write-Host " $result"
|
Write-Host " $result"
|
||||||
|
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
|
|
||||||
|
if ($CertificateThumbprint) {
|
||||||
|
Write-Host ' This file is signed: Windows installs it on any machine.'
|
||||||
|
} else {
|
||||||
Write-Host ' This file is uploaded to Partner Center as it is.'
|
Write-Host ' This file is uploaded to Partner Center as it is.'
|
||||||
|
}
|
||||||
|
|
||||||
if ($Install) {
|
if ($Install) {
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
|
|||||||
+37
-10
@@ -186,8 +186,8 @@ MSIX всегда выполняются в контексте вошедшег
|
|||||||
Windows: тот показывает издателя, спрашивает подтверждение и заменяет
|
Windows: тот показывает издателя, спрашивает подтверждение и заменяет
|
||||||
установленную версию. Подпись проверяет Windows, поэтому приложенный к выпуску
|
установленную версию. Подпись проверяет Windows, поэтому приложенный к выпуску
|
||||||
пакет должен быть подписан — неподписанный установится только на машине в режиме
|
пакет должен быть подписан — неподписанный установится только на машине в режиме
|
||||||
разработчика. Работающее приложение до перезапуска продолжает жить на старых
|
разработчика; пайплайн подписывает то, что прикладывает. Работающее приложение
|
||||||
файлах.
|
до перезапуска продолжает жить на старых файлах.
|
||||||
|
|
||||||
У приложения, установленного из Store, раздела обновлений нет вовсе: его
|
У приложения, установленного из Store, раздела обновлений нет вовсе: его
|
||||||
обновляет Store, а пакет со стороны Windows поверх него всё равно не примет.
|
обновляет Store, а пакет со стороны Windows поверх него всё равно не примет.
|
||||||
@@ -263,8 +263,8 @@ dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings
|
|||||||
Пайплайны лежат в `.gitea/workflows` и работают на Gitea Actions. Запрос
|
Пайплайны лежат в `.gitea/workflows` и работают на Gitea Actions. Запрос
|
||||||
на слияние в `master` собирается и проверяется тестами; по тегу вида `v1.2.3`
|
на слияние в `master` собирается и проверяется тестами; по тегу вида `v1.2.3`
|
||||||
проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом
|
проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом
|
||||||
с приложенными пакетами. Номер версии берётся только из тега — тег любого
|
с приложенными подписанными пакетами. Номер версии берётся только из тега — тег
|
||||||
другого вида останавливает прогон в самом начале. Версия пакета получается
|
любого другого вида останавливает прогон в самом начале. Версия пакета получается
|
||||||
`1.2.3.0`: Store принимает четыре числа и последнее оставляет себе, так что тег
|
`1.2.3.0`: Store принимает четыре числа и последнее оставляет себе, так что тег
|
||||||
на него не влияет.
|
на него не влияет.
|
||||||
|
|
||||||
@@ -276,10 +276,29 @@ dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings
|
|||||||
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
|
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
|
||||||
сеансе, пропускают себя: показать окно там негде.
|
сеансе, пропускают себя: показать окно там негде.
|
||||||
|
|
||||||
Пакет, который несёт релиз, загружается в Partner Center как есть. Identity
|
Пакет собирается дважды: Store и релиз хотят от него разного.
|
||||||
берётся из переменных репозитория, а если те не заданы — из значений по
|
|
||||||
умолчанию в скрипте: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER`
|
Тот, что для Store, несёт identity, зарезервированную в Partner Center, и не
|
||||||
и `MSIX_PUBLISHER_DISPLAY_NAME`.
|
подписан — подпись на него ставит сам Partner Center. Дальше артефактов прогона
|
||||||
|
он не уходит: его забирают и загружают руками. Что это он, видно по имени —
|
||||||
|
`CursorLang-1.2.3.0-store.msixbundle`. Identity берётся из переменных
|
||||||
|
репозитория, а если те не заданы — из значений по умолчанию в скрипте:
|
||||||
|
`MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` и `MSIX_PUBLISHER_DISPLAY_NAME`.
|
||||||
|
|
||||||
|
Тот, что приложен к релизу, пайплайн подписывает сертификатом от SSL.com:
|
||||||
|
закрытый ключ остаётся в их HSM и на runner не попадает — на нём ставится
|
||||||
|
eSigner CKA как поставщик хранилища ключей, и `signtool` спрашивает ключ у него
|
||||||
|
так же, как спросил бы у токена. Учётная запись — три секрета репозитория:
|
||||||
|
`ESIGNER_USERNAME`, `ESIGNER_PASSWORD` и `ESIGNER_TOTP_SECRET`; последний — тот
|
||||||
|
секрет, который eSigner выдаёт для автоматической подписи, а не код с телефона.
|
||||||
|
Проверяются они до сборки, а не на шаге подписи: релиз без подписанного пакета —
|
||||||
|
не релиз.
|
||||||
|
|
||||||
|
`Publisher` у него не `MSIX_PUBLISHER`, а subject этого сертификата, прочитанный
|
||||||
|
прямо в прогоне: пакет, называющий кого-то другого, Windows считает подделкой.
|
||||||
|
Вместе с identity этот subject задаёт family name пакета, поэтому от релиза к
|
||||||
|
релизу оба должны оставаться прежними — иначе обновление встанет рядом со старой
|
||||||
|
версией, а не заменит её.
|
||||||
|
|
||||||
## Сборка пакета MSIX
|
## Сборка пакета MSIX
|
||||||
|
|
||||||
@@ -297,13 +316,21 @@ powershell -File Packaging\build-msix.ps1 -Architectures x64
|
|||||||
|
|
||||||
# Для Partner Center — identity та, что зарезервирована там
|
# Для Partner Center — identity та, что зарезервирована там
|
||||||
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
||||||
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
|
-IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
|
||||||
```
|
```
|
||||||
|
|
||||||
Результат — `artifacts\packages\CursorLang-<версия>.msixbundle`, покрывающий x64
|
Результат — `artifacts\packages\CursorLang-<версия>.msixbundle`, покрывающий x64
|
||||||
и arm64; рядом лежат пакеты отдельных архитектур. Bundle загружается в Partner
|
и arm64; рядом лежат пакеты отдельных архитектур. Bundle загружается в Partner
|
||||||
Center как есть.
|
Center как есть.
|
||||||
|
|
||||||
|
`-CertificateThumbprint` подписывает всё, что собрано, сертификатом с этим
|
||||||
|
отпечатком из личного хранилища текущего пользователя; `-Publisher` тогда должен
|
||||||
|
называть его subject — скрипт говорит об этом до сборки, а не после. Обычно
|
||||||
|
подписывает пайплайн, но то же работает и руками, как только eSigner CKA — или
|
||||||
|
сертификат любого другого рода — положит сертификат в хранилище.
|
||||||
|
`-PackageSuffix` дописывается в конец имён файлов: сборки для Store и для релиза
|
||||||
|
различаются тем, что внутри, и больше ничем.
|
||||||
|
|
||||||
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
||||||
MSIX не может установить среду выполнения как зависимость пакета.
|
MSIX не может установить среду выполнения как зависимость пакета.
|
||||||
|
|
||||||
@@ -324,5 +351,5 @@ pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
|
|||||||
Удалить вручную:
|
Удалить вручную:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeychev.CursorLang).PackageFullName
|
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeichev.CursorLang).PackageFullName
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -181,8 +181,8 @@ The package is downloaded to the temp folder and handed to the Windows app
|
|||||||
installer: it shows the publisher, asks for a confirmation and replaces the
|
installer: it shows the publisher, asks for a confirmation and replaces the
|
||||||
installed version. Windows checks the signature, so the package attached to a
|
installed version. Windows checks the signature, so the package attached to a
|
||||||
release has to be signed — an unsigned one installs nowhere but a machine in
|
release has to be signed — an unsigned one installs nowhere but a machine in
|
||||||
developer mode. The running app keeps working off the old files until it is
|
developer mode; the pipeline signs what it attaches. The running app keeps
|
||||||
restarted.
|
working off the old files until it is restarted.
|
||||||
|
|
||||||
An app installed from the Store has no updates section at all: the Store
|
An app installed from the Store has no updates section at all: the Store
|
||||||
updates it, and a package from the side is something Windows would not accept
|
updates it, and a package from the side is something Windows would not accept
|
||||||
@@ -257,7 +257,7 @@ running agent and fails if any part of the WPF renderer is in it.
|
|||||||
|
|
||||||
The pipelines live in `.gitea/workflows` and run on Gitea Actions. A pull
|
The pipelines live in `.gitea/workflows` and run on Gitea Actions. A pull
|
||||||
request into `master` is built and tested; a tag of the form `v1.2.3` is built,
|
request into `master` is built and tested; a tag of the form `v1.2.3` is built,
|
||||||
tested, packed into an MSIX and published as a release with the packages
|
tested, packed into an MSIX and published as a release with the signed packages
|
||||||
attached. The version is taken from the tag alone — a tag shaped any other way
|
attached. The version is taken from the tag alone — a tag shaped any other way
|
||||||
stops the run right at the start. The package version ends up as `1.2.3.0`: the
|
stops the run right at the start. The package version ends up as `1.2.3.0`: the
|
||||||
Store takes four numbers and keeps the last one for itself, so the tag has no
|
Store takes four numbers and keeps the last one for itself, so the tag has no
|
||||||
@@ -271,9 +271,31 @@ need a desktop of their own — the end-to-end ones, and those that ask for the
|
|||||||
foreground window or the caret — skip themselves on a runner that lives as a
|
foreground window or the caret — skip themselves on a runner that lives as a
|
||||||
service in session 0, where there is no desktop to show a window on.
|
service in session 0, where there is no desktop to show a window on.
|
||||||
|
|
||||||
The package the release carries goes to Partner Center as it is. The identity
|
The package is built twice over, because the Store and a release want different
|
||||||
comes from repository variables and falls back to the defaults of the script when
|
things of it.
|
||||||
unset: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` and `MSIX_PUBLISHER_DISPLAY_NAME`.
|
|
||||||
|
The Store one carries the identity reserved in Partner Center and no signature —
|
||||||
|
Partner Center signs it there. It never leaves the artifacts of the run: someone
|
||||||
|
picks it up and uploads it by hand. Its name says which one it is —
|
||||||
|
`CursorLang-1.2.3.0-store.msixbundle`. The identity comes from repository
|
||||||
|
variables and falls back to the defaults of the script when unset:
|
||||||
|
`MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` and `MSIX_PUBLISHER_DISPLAY_NAME`.
|
||||||
|
|
||||||
|
The one attached to the release is signed by the pipeline with a certificate
|
||||||
|
from SSL.com, whose private key stays in their HSM and never comes down to the
|
||||||
|
runner: eSigner CKA is installed on it as a key storage provider, and `signtool`
|
||||||
|
asks that for the key the way it would ask a token. The account is three
|
||||||
|
repository secrets — `ESIGNER_USERNAME`, `ESIGNER_PASSWORD` and
|
||||||
|
`ESIGNER_TOTP_SECRET`; the last one is the secret eSigner hands out for
|
||||||
|
automated signing, not a code read off a phone. They are checked before the
|
||||||
|
build rather than at the signing step: a release without a signed package is
|
||||||
|
not a release.
|
||||||
|
|
||||||
|
Its `Publisher` is not `MSIX_PUBLISHER` but the subject of that certificate,
|
||||||
|
read off it in the run — Windows takes a package naming anyone else for a
|
||||||
|
forgery. Together with the identity that subject decides the family name of the
|
||||||
|
package, so both have to stay as they are from release to release, or an update
|
||||||
|
installs beside the old version instead of replacing it.
|
||||||
|
|
||||||
## Building the MSIX package
|
## Building the MSIX package
|
||||||
|
|
||||||
@@ -291,13 +313,22 @@ powershell -File Packaging\build-msix.ps1 -Architectures x64
|
|||||||
|
|
||||||
# For Partner Center — the identity is the one reserved there
|
# For Partner Center — the identity is the one reserved there
|
||||||
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
||||||
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
|
-IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
|
||||||
```
|
```
|
||||||
|
|
||||||
The result is `artifacts\packages\CursorLang-<version>.msixbundle` covering x64
|
The result is `artifacts\packages\CursorLang-<version>.msixbundle` covering x64
|
||||||
and arm64; next to it lie the packages of single architectures. Upload the bundle
|
and arm64; next to it lie the packages of single architectures. Upload the bundle
|
||||||
to Partner Center as it is.
|
to Partner Center as it is.
|
||||||
|
|
||||||
|
`-CertificateThumbprint` signs everything the build produces, with the
|
||||||
|
certificate of that thumbprint out of the personal store of the current user;
|
||||||
|
`-Publisher` then has to name its subject, and the script says so before it
|
||||||
|
starts building rather than after. The pipeline is what normally signs, but the
|
||||||
|
same works by hand once eSigner CKA — or a certificate of any other kind — has
|
||||||
|
put a certificate into the store. `-PackageSuffix` goes at the end of the file
|
||||||
|
names: the builds for the Store and for a release differ in what is inside them
|
||||||
|
and in nothing else.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -318,5 +349,5 @@ the Store answers to the same name and is left alone.
|
|||||||
To remove it by hand:
|
To remove it by hand:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeychev.CursorLang).PackageFullName
|
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeichev.CursorLang).PackageFullName
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user