diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
index 23834ff..9dce398 100644
--- a/.gitea/workflows/release.yml
+++ b/.gitea/workflows/release.yml
@@ -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
+ }
diff --git a/CursorLang.Agent/CursorLang.Agent.csproj b/CursorLang.Agent/CursorLang.Agent.csproj
index 8f2b472..e25e9f8 100644
--- a/CursorLang.Agent/CursorLang.Agent.csproj
+++ b/CursorLang.Agent/CursorLang.Agent.csproj
@@ -14,10 +14,10 @@
app.manifest
..\CursorLang.Core\Resources\CursorLang.ico
AnyCPU
- win-x64;win-arm64
+ win-x64
true
false
- true
+ true
1.0.0
1.0.0.0
1.0.0.0
diff --git a/CursorLang.Core/CursorLang.Core.csproj b/CursorLang.Core/CursorLang.Core.csproj
index a447091..c52d5c3 100644
--- a/CursorLang.Core/CursorLang.Core.csproj
+++ b/CursorLang.Core/CursorLang.Core.csproj
@@ -11,7 +11,7 @@
CursorLang.Core
CursorLang.Core
AnyCPU
- win-x64;win-arm64
+ win-x64
1.0.0
1.0.0.0
1.0.0.0
diff --git a/CursorLang.Settings/CursorLang.Settings.csproj b/CursorLang.Settings/CursorLang.Settings.csproj
index 4f82c24..9a98efd 100644
--- a/CursorLang.Settings/CursorLang.Settings.csproj
+++ b/CursorLang.Settings/CursorLang.Settings.csproj
@@ -16,10 +16,10 @@
app.manifest
..\CursorLang.Core\Resources\CursorLang.ico
AnyCPU
- win-x64;win-arm64
+ win-x64
true
false
- true
+ true
1.0.0
1.0.0.0
1.0.0.0
diff --git a/Packaging/Installer/Package.wxs b/Packaging/Installer/Package.wxs
new file mode 100644
index 0000000..1a512c5
--- /dev/null
+++ b/Packaging/Installer/Package.wxs
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Packaging/Tools/SdkTools.csproj b/Packaging/Tools/SdkTools.csproj
index cf72376..e6690d4 100644
--- a/Packaging/Tools/SdkTools.csproj
+++ b/Packaging/Tools/SdkTools.csproj
@@ -5,6 +5,9 @@
meant to be built — build-msix.ps1 restores it and takes the program from the
packages folder. It is kept out of the build in CursorLang.sln for the same
reason.
+
+ The installer needs nothing from here: WiX arrives with its own project, at
+ Installer\CursorLang.wixproj.
-->
diff --git a/Packaging/build-msix.ps1 b/Packaging/build-msix.ps1
index 446e0b4..f8daf26 100644
--- a/Packaging/build-msix.ps1
+++ b/Packaging/build-msix.ps1
@@ -25,13 +25,9 @@
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
+ pwsh -File Packaging\build-msix.ps1 -Install
.EXAMPLE
# A build for the Store — the identity comes from Partner Center
@@ -47,9 +43,6 @@ param(
[string] $Publisher = 'CN=Aleksandr Neichev',
[string] $PublisherDisplayName = 'Aleksandr Neichev',
- [ValidateSet('x64', 'arm64')]
- [string[]] $Architectures = @('x64', 'arm64'),
-
[switch] $Install,
[string] $OutputPath
@@ -86,9 +79,8 @@ if (-not (Test-Path $assets)) {
}
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
-
+ # Checked before the build rather than after it: the build takes minutes, and
+ # this does not get any truer while it runs
$developerMode = Get-ItemPropertyValue `
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' `
-Name 'AllowDevelopmentWithoutDevLicense' -ErrorAction SilentlyContinue
@@ -96,12 +88,6 @@ if ($Install) {
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 {
@@ -198,71 +184,46 @@ 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
-foreach ($architecture in $Architectures) {
- Write-Host "Building $architecture..." -ForegroundColor Cyan
+$layout = Join-Path $layoutRoot 'x64'
- $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
+foreach ($half in @($agentProject, $settingsProject)) {
+ Invoke-Tool -Path 'dotnet' -Arguments @(
+ 'publish', $half,
+ '--configuration', 'Release',
+ '--runtime', 'win-x64',
+ '--self-contained', 'true',
+ "-p:Version=$($Version.Substring(0, $Version.LastIndexOf('.')))",
+ '--output', $layout,
+ '--nologo'
+ )
}
-$result = $built[0]
+# 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
-if ($built.Count -gt 1) {
- Write-Host 'Bringing the architectures into one package...' -ForegroundColor Cyan
+Copy-Item $assets -Destination (Join-Path $layout 'Assets') -Recurse -Force
- # 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 }
+$manifest = (Get-Content $manifestTemplate -Raw).
+ Replace('{IdentityName}', $IdentityName).
+ Replace('{Publisher}', $Publisher).
+ Replace('{PublisherDisplayName}', $PublisherDisplayName).
+ Replace('{Version}', $Version).
+ Replace('{Architecture}', 'x64')
- $result = Join-Path $packagesPath "CursorLang-$Version.msixbundle"
- Invoke-Tool -Path $makeappx -Arguments @('bundle', '/o', '/d', $bundleInput, '/p', $result, '/bv', $Version)
+Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8
- 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 the $machineArchitecture build..." -ForegroundColor Cyan
+ Write-Host 'Installing...' -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")
+ Add-AppxPackage -Register (Join-Path $layout 'AppxManifest.xml')
}
Write-Host ''
diff --git a/README.RU.md b/README.RU.md
index c688230..ddf543c 100644
--- a/README.RU.md
+++ b/README.RU.md
@@ -265,17 +265,19 @@ Identity берётся из переменных репозитория, а е
# Разово: отрисовать логотипы и иконку exe (уже в репозитории, повторить после правок)
powershell -File Packaging\New-Assets.ps1
-# Проверка на своей машине: только своя архитектура
-powershell -File Packaging\build-msix.ps1 -Architectures x64
+# Проверка на своей машине
+powershell -File Packaging\build-msix.ps1
# Для Partner Center — identity та, что зарезервирована там
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
```
-Результат — `artifacts\packages\CursorLang-<версия>.msixbundle`, покрывающий x64
-и arm64; рядом лежат пакеты отдельных архитектур. Bundle загружается в Partner
-Center как есть.
+Результат — `artifacts\packages\CursorLang-<версия>-x64.msix`. Он загружается в
+Partner Center как есть.
+
+Собирается только x64. Сборка под arm64 удвоила бы вес каждого релиза ради
+машин, которые и так выполняют x64 через эмуляцию.
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
@@ -286,7 +288,7 @@ MSIX не может установить среду выполнения как
Чтобы посмотреть, как пакет работает на этой машине, соберите его с `-Install`:
```powershell
-pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
+pwsh -File Packaging\build-msix.ps1 -Install
```
Приложение появится в меню «Пуск» как любое установленное. Регистрируется не сам
@@ -302,3 +304,39 @@ pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
```powershell
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeichev.CursorLang).PackageFullName
```
+
+## Сборка установщика
+
+То же приложение собирается и обычным MSI — чтобы раздавать помимо Store: пока
+Store не вынес решение или если так его и не вынесет. Ставить ничего, кроме .NET
+SDK, не нужно: WiX приезжает пакетом NuGet, как и `makeappx`.
+
+```powershell
+# Собрать и запустить — посмотреть глазами пользователя
+pwsh -File Packaging\build-installer.ps1 -Install
+
+# Всё, что нужно релизу
+pwsh -File Packaging\build-installer.ps1 -Version 1.0.1
+```
+
+Результат — `artifacts\installers\CursorLang-<версия>-x64.msi`. Всё лежит внутри
+`.msi` — отдельного архива рядом с ним нет.
+
+Устанавливается только для текущего пользователя, в
+`%LOCALAPPDATA%\Programs\CursorLang`, поэтому не просит ни прав администратора,
+ни подтверждения. Удаление идёт через «Параметры» — «Приложения», как у любой
+программы, и уносит с собой запись автозапуска: иначе Windows продолжала бы
+показывать в автозагрузке приложение, которого уже нет.
+
+Установщик никто не подписывает, поэтому Windows предупреждает о неизвестном
+издателе и пользователю приходится настоять. Покупка сертификата это сразу не
+снимет: SmartScreen смотрит на репутацию, а у нового сертификата её нет, пока
+приложение не наберёт установок.
+
+Про проект WiX стоит знать две вещи, прежде чем его править. Он закреплён на WiX
+5, а не на нынешней 7: начиная с шестой версии инструмент требует принимать
+лицензию Open Source Maintenance Fee — бесплатную при доходе меньше $10 000 в
+год, но принимать её должен человек, а не сборочный скрипт. И он отключает три
+проверки ICE: MSI по-прежнему исходит из установки на всю машину, а установка в
+профиль пользователя нарушает правила, которые описывают ровно то, что здесь и
+задумано.
diff --git a/README.md b/README.md
index 2eb3b5a..9a34915 100644
--- a/README.md
+++ b/README.md
@@ -259,17 +259,19 @@ to submitting for certification — is written up in
# One-off: draw the logos and the exe icon (already committed, rerun after edits)
powershell -File Packaging\New-Assets.ps1
-# A check on your own machine: your architecture alone
-powershell -File Packaging\build-msix.ps1 -Architectures x64
+# A check on your own machine
+powershell -File Packaging\build-msix.ps1
# For Partner Center — the identity is the one reserved there
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
```
-The result is `artifacts\packages\CursorLang-.msixbundle` covering x64
-and arm64; next to it lie the packages of single architectures. Upload the bundle
-to Partner Center as it is.
+The result is `artifacts\packages\CursorLang--x64.msix`. Upload it to
+Partner Center as it is.
+
+Only x64 is built. An arm64 build would double the size of every release for the
+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.
@@ -280,7 +282,7 @@ MSIX cannot install a runtime as a package dependency.
To see the package working on this machine, build it with `-Install`:
```powershell
-pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
+pwsh -File Packaging\build-msix.ps1 -Install
```
The application then shows up in the Start menu like any installed one. What
@@ -296,3 +298,39 @@ To remove it by hand:
```powershell
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeichev.CursorLang).PackageFullName
```
+
+## Building the installer
+
+The same application also comes as an ordinary MSI, for handing round outside the
+Store — to try it out before the Store has passed judgement, or if it never does.
+Nothing has to be installed beyond the .NET SDK: WiX comes from a NuGet package,
+the same way `makeappx` does.
+
+```powershell
+# Build it and run it afterwards, to see what a user sees
+pwsh -File Packaging\build-installer.ps1 -Install
+
+# Everything a release needs
+pwsh -File Packaging\build-installer.ps1 -Version 1.0.1
+```
+
+The result is `artifacts\installers\CursorLang--x64.msi`. Everything
+travels inside the .msi; there is no cabinet to send alongside it.
+
+It installs for the current user alone, into `%LOCALAPPDATA%\Programs\CursorLang`,
+and so asks for no administrator rights and no consent dialog. Uninstalling goes
+through Apps in Settings like any other program and takes the startup entry with
+it — otherwise Windows would go on listing an application that is no longer there.
+
+Nobody signs the installer, so Windows warns about an unknown publisher and the
+user has to insist. Buying a certificate would not silence it at once either:
+SmartScreen goes by reputation, and a fresh certificate has none until enough
+people have installed the application.
+
+Two things about the WiX project are worth knowing before touching it. It pins
+WiX 5 rather than the current 7: from version 6 the toolset asks every build to
+accept the Open Source Maintenance Fee licence — free below $10,000 of yearly
+revenue, but a decision for a person rather than for a build script. And it turns
+off three ICE validation rules: MSI still assumes an installation for the whole
+machine, and installing into the user's own profile trips rules that describe
+exactly what was intended here.