Compare commits
4
Commits
71a8d3e6fb
..
v0.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08192d317e | ||
|
|
3bf2018a62 | ||
|
|
cb39198e0a | ||
|
|
a777b71b1a |
@@ -26,12 +26,40 @@ jobs:
|
|||||||
runs-on: windows-x64
|
runs-on: windows-x64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# lfs: true is not a nicety: the exe icon lives in Git LFS, and without it
|
|
||||||
# the checkout leaves a text pointer that the build cannot read as an icon
|
|
||||||
- name: Check out the sources
|
- name: Check out the sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
lfs: true
|
# The exe icon lives in Git LFS, and without it the checkout leaves a text
|
||||||
|
# pointer that the build cannot read as an icon.
|
||||||
|
#
|
||||||
|
# The objects are fetched here rather than by `lfs: true` on the checkout:
|
||||||
|
# that way they arrive over a request the LFS endpoint accepts. See the
|
||||||
|
# comment on the header below
|
||||||
|
- name: Fetch the LFS objects
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# actions/checkout leaves its own token in the config as an
|
||||||
|
# http.<server>/.extraheader, and git-lfs sends that header on to the
|
||||||
|
# LFS endpoint, which turns down the token of a workflow: every object
|
||||||
|
# comes back 401 and the fetch gives up. The repository is public and
|
||||||
|
# its LFS objects are readable without a token at all, so the header
|
||||||
|
# simply goes. A private repository would need credentials of its own
|
||||||
|
# in lfs.url instead
|
||||||
|
$keys = git config --local --list --name-only | Where-Object { $_ -like '*.extraheader' }
|
||||||
|
foreach ($key in $keys) { git config --local --unset-all $key }
|
||||||
|
|
||||||
|
git lfs pull
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git lfs pull ended with exit code $LASTEXITCODE." }
|
||||||
|
|
||||||
|
# A pointer left in place of a file shows itself much later and in a
|
||||||
|
# way that is hard to read back: the build breaks on the icon
|
||||||
|
$pointers = git lfs ls-files --name-only |
|
||||||
|
Where-Object { (Get-Content $_ -TotalCount 1) -like 'version https://git-lfs*' }
|
||||||
|
|
||||||
|
if ($pointers) {
|
||||||
|
throw "Git LFS left pointers instead of files: $($pointers -join ', ')."
|
||||||
|
}
|
||||||
|
|
||||||
- name: Show the toolchain
|
- name: Show the toolchain
|
||||||
run: dotnet --info
|
run: dotnet --info
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# The release: a tag of the form v1.2.3.0 builds the solution, runs the tests
|
# The release: a tag of the form v1.2.3 builds the solution, runs the tests and
|
||||||
# and packs the MSIX with the version taken from 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.
|
||||||
#
|
#
|
||||||
# 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 comes
|
||||||
@@ -21,28 +22,60 @@ jobs:
|
|||||||
runs-on: windows-x64
|
runs-on: windows-x64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# lfs: true is not a nicety: the exe icon and the MSIX logos live in Git
|
|
||||||
# LFS, and without it the checkout leaves text pointers in their place —
|
|
||||||
# the build fails on the icon and the package would carry broken logos
|
|
||||||
- name: Check out the sources
|
- name: Check out the sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
lfs: true
|
|
||||||
|
|
||||||
# The tag is the only place the version comes from: the Store takes four
|
# The exe icon and the MSIX logos live in Git LFS, and without them the
|
||||||
# numbers ending in zero, so anything else is stopped here rather than
|
# checkout leaves text pointers in their place — the build fails on the
|
||||||
# halfway through the packaging
|
# icon and the package would carry broken logos.
|
||||||
|
#
|
||||||
|
# They are fetched here rather than by `lfs: true` on the checkout: that
|
||||||
|
# way the objects arrive over a request the LFS endpoint accepts. See the
|
||||||
|
# comment on the header below
|
||||||
|
- name: Fetch the LFS objects
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# actions/checkout leaves its own token in the config as an
|
||||||
|
# http.<server>/.extraheader, and git-lfs sends that header on to the
|
||||||
|
# LFS endpoint, which turns down the token of a workflow: every object
|
||||||
|
# comes back 401 and the fetch gives up. The repository is public and
|
||||||
|
# its LFS objects are readable without a token at all, so the header
|
||||||
|
# simply goes. A private repository would need credentials of its own
|
||||||
|
# in lfs.url instead
|
||||||
|
$keys = git config --local --list --name-only | Where-Object { $_ -like '*.extraheader' }
|
||||||
|
foreach ($key in $keys) { git config --local --unset-all $key }
|
||||||
|
|
||||||
|
git lfs pull
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git lfs pull ended with exit code $LASTEXITCODE." }
|
||||||
|
|
||||||
|
# A pointer left in place of a file shows itself much later and in a
|
||||||
|
# way that is hard to read back: the icon breaks the build, and a logo
|
||||||
|
# quietly ends up broken inside the package
|
||||||
|
$pointers = git lfs ls-files --name-only |
|
||||||
|
Where-Object { (Get-Content $_ -TotalCount 1) -like 'version https://git-lfs*' }
|
||||||
|
|
||||||
|
if ($pointers) {
|
||||||
|
throw "Git LFS left pointers instead of files: $($pointers -join ', ')."
|
||||||
|
}
|
||||||
|
|
||||||
|
# The tag is the only place the version comes from, and it is a plain
|
||||||
|
# version of three numbers — the same shape the application itself looks
|
||||||
|
# for in the releases when it checks for an update. A tag of any other
|
||||||
|
# shape is stopped here rather than halfway through the packaging
|
||||||
- name: Read the version from the tag
|
- name: Read the version from the tag
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$tag = '${{ github.ref_name }}'
|
$tag = '${{ github.ref_name }}'
|
||||||
|
|
||||||
if ($tag -notmatch '^v\d+\.\d+\.\d+\.0$') {
|
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
throw "The tag '$tag' does not fit: a release is tagged as v1.2.3.0 — four numbers ending in zero."
|
throw "The tag '$tag' does not fit: a release is tagged as v1.2.3 — three numbers. A fourth one does not belong in the tag: the Store keeps the revision for itself, and the package always gets a zero there."
|
||||||
}
|
}
|
||||||
|
|
||||||
"version=$($tag.Substring(1))" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
|
# The package takes four numbers with a zero at the end: the Store
|
||||||
|
# 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
|
||||||
|
|
||||||
- name: Show the toolchain
|
- name: Show the toolchain
|
||||||
run: dotnet --info
|
run: dotnet --info
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace CursorLang.Tests;
|
namespace CursorLang.Tests;
|
||||||
|
|
||||||
@@ -14,6 +16,9 @@ namespace CursorLang.Tests;
|
|||||||
///
|
///
|
||||||
/// If the application is already running in this session, the checks skip
|
/// If the application is already running in this session, the checks skip
|
||||||
/// themselves: meddling with someone else's running instance is not their business.
|
/// themselves: meddling with someone else's running instance is not their business.
|
||||||
|
///
|
||||||
|
/// They skip themselves where there is no desktop to show a window on either — on a
|
||||||
|
/// build agent living as a Windows service, for one.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class EndToEndTests
|
public sealed class EndToEndTests
|
||||||
{
|
{
|
||||||
@@ -59,6 +64,16 @@ public sealed class EndToEndTests
|
|||||||
/// <summary>A started application that shuts down together with the check.</summary>
|
/// <summary>A started application that shuts down together with the check.</summary>
|
||||||
private sealed class Launch : IDisposable
|
private sealed class Launch : IDisposable
|
||||||
{
|
{
|
||||||
|
private const int UOI_NAME = 2;
|
||||||
|
private const string InteractiveWindowStation = "WinSta0";
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr GetProcessWindowStation();
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern bool GetUserObjectInformation(IntPtr hObj, int nIndex,
|
||||||
|
StringBuilder pvInfo, int nLength, out int lpnLengthNeeded);
|
||||||
|
|
||||||
private Launch(Process process) => Process = process;
|
private Launch(Process process) => Process = process;
|
||||||
|
|
||||||
internal Process Process { get; }
|
internal Process Process { get; }
|
||||||
@@ -66,6 +81,11 @@ public sealed class EndToEndTests
|
|||||||
/// <summary>Starts the application first — making sure the place is free.</summary>
|
/// <summary>Starts the application first — making sure the place is free.</summary>
|
||||||
internal static Launch Start()
|
internal static Launch Start()
|
||||||
{
|
{
|
||||||
|
if (!HasInteractiveDesktop())
|
||||||
|
{
|
||||||
|
Assert.Skip("There is no interactive desktop here — the application has nowhere to show its window");
|
||||||
|
}
|
||||||
|
|
||||||
if (Process.GetProcessesByName("CursorLang").Length > 0)
|
if (Process.GetProcessesByName("CursorLang").Length > 0)
|
||||||
{
|
{
|
||||||
Assert.Skip("The application is already running — this check keeps out of someone else's run");
|
Assert.Skip("The application is already running — this check keeps out of someone else's run");
|
||||||
@@ -116,6 +136,25 @@ public sealed class EndToEndTests
|
|||||||
return IntPtr.Zero;
|
return IntPtr.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether there is a desktop here to show a window on. A service gets a
|
||||||
|
/// window station of its own — "Service-0x0-3e7$" and the like: a window can
|
||||||
|
/// be created there, yet nothing shows it. Only "WinSta0" is the interactive one.
|
||||||
|
/// </summary>
|
||||||
|
private static bool HasInteractiveDesktop()
|
||||||
|
{
|
||||||
|
IntPtr station = GetProcessWindowStation();
|
||||||
|
if (station == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = new StringBuilder(256);
|
||||||
|
|
||||||
|
return GetUserObjectInformation(station, UOI_NAME, name, name.Capacity * sizeof(char), out _)
|
||||||
|
&& name.ToString().Equals(InteractiveWindowStation, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private static string ExecutablePath()
|
private static string ExecutablePath()
|
||||||
{
|
{
|
||||||
string configured = Assembly.GetExecutingAssembly()
|
string configured = Assembly.GetExecutingAssembly()
|
||||||
|
|||||||
@@ -124,9 +124,7 @@ public sealed class LayoutPopupWindowTests
|
|||||||
{
|
{
|
||||||
var settings = new AppSettings
|
var settings = new AppSettings
|
||||||
{
|
{
|
||||||
PlacementMode = PopupPlacementMode.FixedPoint,
|
PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = position, ScreenMargin = 24,
|
||||||
ScreenPosition = position,
|
|
||||||
ScreenMargin = 24,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using var popup = Popup.Create(settings);
|
using var popup = Popup.Create(settings);
|
||||||
@@ -160,9 +158,7 @@ public sealed class LayoutPopupWindowTests
|
|||||||
{
|
{
|
||||||
var settings = new AppSettings
|
var settings = new AppSettings
|
||||||
{
|
{
|
||||||
PlacementMode = PopupPlacementMode.AtCursor,
|
PlacementMode = PopupPlacementMode.AtCursor, CursorSide = AnchorSide.BottomRight, CursorOffset = 16,
|
||||||
CursorSide = AnchorSide.BottomRight,
|
|
||||||
CursorOffset = 16,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using var popup = Popup.Create(settings);
|
using var popup = Popup.Create(settings);
|
||||||
@@ -213,23 +209,48 @@ public sealed class LayoutPopupWindowTests
|
|||||||
{
|
{
|
||||||
var settings = new AppSettings
|
var settings = new AppSettings
|
||||||
{
|
{
|
||||||
PlacementMode = PopupPlacementMode.AtCaret,
|
PlacementMode = PopupPlacementMode.AtCaret, CaretSide = AnchorSide.BottomRight, CaretOffset = 8,
|
||||||
CaretSide = AnchorSide.BottomRight,
|
|
||||||
CaretOffset = 8,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using var popup = Popup.Create(settings);
|
using var popup = Popup.Create(settings);
|
||||||
popup.ViewModel.ShortName = "RU";
|
popup.ViewModel.ShortName = "RU";
|
||||||
|
|
||||||
PopupWindowNative.Point before = PopupWindowNative.GetCursorPosition();
|
PopupWindowNative.Point before = default;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < 10; attempt++)
|
||||||
|
{
|
||||||
|
if (CaretNative.TryGetCaretRect() is not null)
|
||||||
|
{
|
||||||
|
Assert.Skip("The foreground window reported a caret of its own");
|
||||||
|
}
|
||||||
|
|
||||||
|
before = PopupWindowNative.GetCursorPosition();
|
||||||
popup.Window.ShowPopup();
|
popup.Window.ShowPopup();
|
||||||
|
PopupWindowNative.Point after = PopupWindowNative.GetCursorPosition();
|
||||||
|
|
||||||
|
if (before.X == after.X && before.Y == after.Y)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt == 9)
|
||||||
|
{
|
||||||
|
Assert.Skip("The cursor kept moving the whole time");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
||||||
|
double scale = PopupWindowNative.GetScaleAt(before);
|
||||||
|
|
||||||
// A gentle check: a window holding the input focus may still have a caret
|
PopupWindowNative.Point expected = PopupLayout.NearAnchor(
|
||||||
Assert.True(bounds.Right > bounds.Left);
|
PopupLayout.AsAnchor(before),
|
||||||
Assert.True(bounds.Bottom > bounds.Top);
|
settings.CaretSide,
|
||||||
Assert.NotEqual(default, before);
|
PopupLayout.ToPixels(settings.CaretOffset, scale),
|
||||||
|
bounds.Right - bounds.Left,
|
||||||
|
bounds.Bottom - bounds.Top);
|
||||||
|
|
||||||
|
Assert.Equal(expected.X, bounds.Left);
|
||||||
|
Assert.Equal(expected.Y, bounds.Top);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,8 +287,7 @@ public sealed class LayoutPopupWindowTests
|
|||||||
{
|
{
|
||||||
var settings = new AppSettings
|
var settings = new AppSettings
|
||||||
{
|
{
|
||||||
PlacementMode = PopupPlacementMode.FixedPoint,
|
PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = ScreenPosition.TopLeft,
|
||||||
ScreenPosition = ScreenPosition.TopLeft,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using var popup = Popup.Create(settings);
|
using var popup = Popup.Create(settings);
|
||||||
|
|||||||
+34
-11
@@ -135,28 +135,51 @@ dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage
|
|||||||
композиционный корень в `App.xaml.cs`, который вместо этого проверяется сквозным
|
композиционный корень в `App.xaml.cs`, который вместо этого проверяется сквозным
|
||||||
запуском приложения.
|
запуском приложения.
|
||||||
|
|
||||||
|
## Непрерывная сборка
|
||||||
|
|
||||||
|
Пайплайны лежат в `.gitea/workflows` и работают на Gitea Actions. Запрос
|
||||||
|
на слияние в `master` собирается и проверяется тестами; по тегу вида `v1.2.3`
|
||||||
|
проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом
|
||||||
|
с приложенными пакетами. Номер версии берётся только из тега — тег любого
|
||||||
|
другого вида останавливает прогон в самом начале. Версия пакета получается
|
||||||
|
`1.2.3.0`: Store принимает четыре числа и последнее оставляет себе, так что тег
|
||||||
|
на него не влияет.
|
||||||
|
|
||||||
|
Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём —
|
||||||
|
.NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка
|
||||||
|
остаётся текстовой заглушкой и сборка на ней падает. Работать runner лучше
|
||||||
|
в интерактивном сеансе рабочего стола: тесты поднимают настоящие окна, и те
|
||||||
|
проверки, которым нужен свой рабочий стол — сквозные, а также те, что просят
|
||||||
|
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
|
||||||
|
сеансе, пропускают себя: показать окно там негде.
|
||||||
|
|
||||||
|
Пакет, который несёт релиз, загружается в Partner Center как есть. Identity
|
||||||
|
берётся из переменных репозитория, а если те не заданы — из значений по
|
||||||
|
умолчанию в скрипте: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER`
|
||||||
|
и `MSIX_PUBLISHER_DISPLAY_NAME`.
|
||||||
|
|
||||||
## Сборка пакета MSIX
|
## Сборка пакета MSIX
|
||||||
|
|
||||||
Ни Visual Studio, ни Windows SDK не требуются — `makeappx` и `signtool`
|
Ни Visual Studio, ни Windows SDK не требуются — `makeappx` поставляется из
|
||||||
поставляются из пакета NuGet. Порядок публикации целиком — от регистрации
|
пакета NuGet. Порядок публикации целиком — от регистрации разработчика
|
||||||
разработчика до отправки на проверку — описан в
|
до отправки на проверку — описан в
|
||||||
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md).
|
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md).
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разово: отрисовать логотипы и иконку exe (уже в репозитории, повторить после правок)
|
# Разово: отрисовать логотипы и иконку exe (уже в репозитории, повторить после правок)
|
||||||
powershell -File Packaging\New-Assets.ps1
|
powershell -File Packaging\New-Assets.ps1
|
||||||
|
|
||||||
# Сборка и подпись для локальной проверки
|
# Проверка на своей машине: только своя архитектура
|
||||||
powershell -File Packaging\build-msix.ps1 -Architectures x64 -Sign
|
powershell -File Packaging\build-msix.ps1 -Architectures x64
|
||||||
|
|
||||||
# Сборка для Store — identity берётся из Partner Center
|
# Для 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 12345Alrakis.CursorLang -Publisher "CN=ABCD1234-..."
|
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
|
||||||
```
|
```
|
||||||
|
|
||||||
Результат — `artifacts\packages\CursorLang-<version>.msixbundle`, покрывающий x64
|
Результат — `artifacts\packages\CursorLang-<версия>.msixbundle`, покрывающий x64
|
||||||
и arm64. Загружать в Partner Center его нужно неподписанным — Store подписывает
|
и arm64; рядом лежат пакеты отдельных архитектур. Bundle загружается в Partner
|
||||||
пакет собственным сертификатом.
|
Center как есть.
|
||||||
|
|
||||||
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
||||||
MSIX не может установить среду выполнения как зависимость пакета.
|
MSIX не может установить среду выполнения как зависимость пакета.
|
||||||
@@ -166,5 +189,5 @@ MSIX не может установить среду выполнения как
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
|
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
|
||||||
Remove-AppxPackage (Get-AppxPackage -Name Alrakis.CursorLang).PackageFullName
|
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -133,28 +133,50 @@ What stays uncovered is what a test process cannot reach: the code paths that
|
|||||||
require an MSIX package identity — `StartupTask` and the package data folder —
|
require an MSIX package identity — `StartupTask` and the package data folder —
|
||||||
and the composition root in `App.xaml.cs`, which is exercised end-to-end instead.
|
and the composition root in `App.xaml.cs`, which is exercised end-to-end instead.
|
||||||
|
|
||||||
|
## Continuous integration
|
||||||
|
|
||||||
|
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,
|
||||||
|
tested, packed into an MSIX and published as a release with the packages
|
||||||
|
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
|
||||||
|
Store takes four numbers and keeps the last one for itself, so the tag has no
|
||||||
|
say in it.
|
||||||
|
|
||||||
|
Both pipelines ask for a Windows runner labelled `windows-x64` with the .NET 10
|
||||||
|
SDK and git-lfs on it. The checkout pulls LFS files: without them the icon is a
|
||||||
|
text pointer and the build fails on it. The runner is better off working in an
|
||||||
|
interactive desktop session: the tests raise real windows, and the checks that
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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`.
|
||||||
|
|
||||||
## Building the MSIX package
|
## Building the MSIX package
|
||||||
|
|
||||||
Neither Visual Studio nor the Windows SDK is required — `makeappx` and
|
Neither Visual Studio nor the Windows SDK is required — `makeappx` comes from a
|
||||||
`signtool` come from a NuGet package. The full publishing procedure — from
|
NuGet package. The full publishing procedure — from opening a developer account
|
||||||
opening a developer account to submitting for certification — is written up in
|
to submitting for certification — is written up in
|
||||||
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md) (Russian only).
|
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md) (Russian only).
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# One-off: draw the logos and the exe icon (already committed, rerun after edits)
|
# One-off: draw the logos and the exe icon (already committed, rerun after edits)
|
||||||
powershell -File Packaging\New-Assets.ps1
|
powershell -File Packaging\New-Assets.ps1
|
||||||
|
|
||||||
# Build and sign for local testing
|
# A check on your own machine: your architecture alone
|
||||||
powershell -File Packaging\build-msix.ps1 -Architectures x64 -Sign
|
powershell -File Packaging\build-msix.ps1 -Architectures x64
|
||||||
|
|
||||||
# Build for the Store — identity comes from Partner Center
|
# 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 12345Alrakis.CursorLang -Publisher "CN=ABCD1234-..."
|
-IdentityName 12345AleksandrNeychev.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. Upload it to Partner Center unsigned — the Store signs it with its own
|
and arm64; next to it lie the packages of single architectures. Upload the bundle
|
||||||
certificate.
|
to Partner Center as it is.
|
||||||
|
|
||||||
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.
|
||||||
@@ -164,5 +186,5 @@ needs Developer Mode and no signature at all:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
|
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
|
||||||
Remove-AppxPackage (Get-AppxPackage -Name Alrakis.CursorLang).PackageFullName
|
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user