added packaging

This commit is contained in:
2026-08-11 15:00:52 +05:00
parent 86c0085472
commit d6886eae7c
13 changed files with 543 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap desktop rescap">
<Identity
Name="{IdentityName}"
Publisher="{Publisher}"
Version="{Version}"
ProcessorArchitecture="{Architecture}"/>
<Properties>
<DisplayName>CursorLang</DisplayName>
<PublisherDisplayName>{PublisherDisplayName}</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0"/>
</Dependencies>
<Resources>
<Resource Language="en-us"/>
<Resource Language="ru-ru"/>
</Resources>
<Applications>
<Application Id="App" Executable="CursorLang.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="CursorLang"
Description="Shows the keyboard layout at the cursor"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile
Square71x71Logo="Assets\Square71x71Logo.png"
Square310x310Logo="Assets\Square310x310Logo.png"
Wide310x150Logo="Assets\Wide310x150Logo.png"/>
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.startupTask" Executable="CursorLang.exe"
EntryPoint="Windows.FullTrustApplication">
<desktop:StartupTask TaskId="CursorLangStartup" Enabled="false" DisplayName="CursorLang"/>
</desktop:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
</Capabilities>
</Package>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+232
View File
@@ -0,0 +1,232 @@
<#
.SYNOPSIS
Draws the application icons: the logos for MSIX and the .ico for the exe.
.DESCRIPTION
The icons are drawn by code rather than kept as pictures so that the whole
set of sizes is rebuilt by a single command whenever another drawing is
wanted.
The current drawing is a placeholder: a dark rounded square with a white
"Aя". Before publishing to the Store it is worth replacing with a real one:
rewriting New-Logo is enough, the rest of the script does not depend on the
drawing.
.EXAMPLE
pwsh -File Packaging\New-Assets.ps1
#>
[CmdletBinding()]
param(
# Where to put the MSIX logos
[string] $AssetsPath,
# Where to put the .ico for the exe
[string] $IconPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# The defaults are worked out here rather than in the parameter declarations:
# in Windows PowerShell $PSScriptRoot is still empty inside param()
$root = $PSScriptRoot
if (-not $AssetsPath) { $AssetsPath = Join-Path $root 'Assets' }
if (-not $IconPath) { $IconPath = Join-Path $root '..\CursorLang\Resources\CursorLang.ico' }
Add-Type -AssemblyName System.Drawing
$Background = [System.Drawing.Color]::FromArgb(255, 0x20, 0x20, 0x20)
$Foreground = [System.Drawing.Color]::FromArgb(255, 0xFF, 0xFF, 0xFF)
$Glyph = 'Aя'
function New-Logo {
param(
[int] $Width,
[int] $Height,
# The plate under the glyph. The Windows taskbar draws its shortcut
# without one, so for those sizes only the white glyph remains
[bool] $WithPlate = $true
)
$bitmap = New-Object System.Drawing.Bitmap($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
try {
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
$graphics.Clear([System.Drawing.Color]::Transparent)
$side = [Math]::Min($Width, $Height)
if ($WithPlate) {
# The corner radius is taken as a share of the side so that small
# sizes do not turn into a circle and large ones do not look like a
# plain square
$radius = [Math]::Max(2, [int]($side * 0.18))
$plate = New-Object System.Drawing.Drawing2D.GraphicsPath
$plate.AddArc(0, 0, $radius * 2, $radius * 2, 180, 90)
$plate.AddArc($Width - $radius * 2 - 1, 0, $radius * 2, $radius * 2, 270, 90)
$plate.AddArc($Width - $radius * 2 - 1, $Height - $radius * 2 - 1, $radius * 2, $radius * 2, 0, 90)
$plate.AddArc(0, $Height - $radius * 2 - 1, $radius * 2, $radius * 2, 90, 90)
$plate.CloseFigure()
$brush = New-Object System.Drawing.SolidBrush($Background)
try { $graphics.FillPath($brush, $plate) } finally { $brush.Dispose(); $plate.Dispose() }
}
# Typographic metrics leave out the side bearings MeasureString reports by
# default. Measured with those bearings the glyph looks wider than it is,
# the fitted type comes out too small, and at 16 px the strokes wash out
[System.Drawing.StringFormat] $format = [System.Drawing.StringFormat]::GenericTypographic.Clone()
try {
# The type size is found by trying: with different tile sizes the same
# share of the side gives text that is either cramped or too small
$target = $side * 0.80
$ceiling = $side * 0.86
$fontSize = $side * 0.9
$font = $null
$measured = $null
for ($attempt = 0; $attempt -lt 20; $attempt++) {
if ($font) { $font.Dispose() }
$font = New-Object System.Drawing.Font('Segoe UI', $fontSize, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Pixel)
$measured = $graphics.MeasureString($Glyph, $font, ([System.Drawing.PointF]::new(0, 0)), $format)
if ($measured.Width -le $target -and $measured.Height -le $ceiling) { break }
# Both dimensions are pulled in at once, otherwise a tall glyph
# keeps overflowing while the width already fits
$scale = [Math]::Min($target / $measured.Width, $ceiling / $measured.Height)
$fontSize = $fontSize * $scale * 0.98
}
try {
# AntiAliasGridFit snaps the stems onto the pixel grid, so the
# glyph is placed at a whole pixel: a fractional origin spreads
# every stem across two pixels, which is what blurred the small
# sizes when the text was centred inside a RectangleF
$x = [Math]::Round(($Width - $measured.Width) / 2.0)
$y = [Math]::Round(($Height - $measured.Height) / 2.0)
$brush = New-Object System.Drawing.SolidBrush($Foreground)
$origin = New-Object System.Drawing.PointF($x, $y)
try { $graphics.DrawString($Glyph, $font, $brush, $origin, $format) }
finally { $brush.Dispose() }
}
finally {
$font.Dispose()
}
}
finally {
$format.Dispose()
}
}
finally {
$graphics.Dispose()
}
return $bitmap
}
function Save-Logo {
param(
[string] $Path,
[int] $Width,
[int] $Height,
[bool] $WithPlate = $true
)
$bitmap = New-Logo -Width $Width -Height $Height -WithPlate $WithPlate
try { $bitmap.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png) } finally { $bitmap.Dispose() }
Write-Host " $([System.IO.Path]::GetFileName($Path))"
}
function Save-Icon {
param(
[string] $Path,
[int[]] $Sizes
)
# The .ico is put together by hand: System.Drawing can only save a single
# size to that format, and the icon of the exe is needed in several at once
$images = @()
foreach ($size in $Sizes) {
$bitmap = New-Logo -Width $size -Height $size
try {
$stream = New-Object System.IO.MemoryStream
$bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
$images += , @{ Size = $size; Bytes = $stream.ToArray() }
$stream.Dispose()
}
finally {
$bitmap.Dispose()
}
}
$output = New-Object System.IO.MemoryStream
$writer = New-Object System.IO.BinaryWriter($output)
try {
$writer.Write([uint16]0) # reserved
$writer.Write([uint16]1) # type: icon
$writer.Write([uint16]$images.Count)
# The directory of entries comes before the pictures themselves, so the
# offsets are worked out beforehand: the header plus 16 bytes per entry
$offset = 6 + 16 * $images.Count
foreach ($image in $images) {
# 256 does not fit in a byte and is written as zero — the format
# means it that way
$dimension = if ($image.Size -ge 256) { 0 } else { $image.Size }
$writer.Write([byte]$dimension) # width
$writer.Write([byte]$dimension) # height
$writer.Write([byte]0) # palette colours: no palette
$writer.Write([byte]0) # reserved
$writer.Write([uint16]1) # planes
$writer.Write([uint16]32) # bits per pixel
$writer.Write([uint32]$image.Bytes.Length)
$writer.Write([uint32]$offset)
$offset += $image.Bytes.Length
}
foreach ($image in $images) {
$writer.Write($image.Bytes)
}
$writer.Flush()
[System.IO.File]::WriteAllBytes($Path, $output.ToArray())
}
finally {
$writer.Dispose()
$output.Dispose()
}
Write-Host " $([System.IO.Path]::GetFileName($Path))"
}
New-Item -ItemType Directory -Path $AssetsPath -Force | Out-Null
New-Item -ItemType Directory -Path (Split-Path -Parent $IconPath) -Force | Out-Null
Write-Host 'MSIX logos:'
Save-Logo -Path (Join-Path $AssetsPath 'StoreLogo.png') -Width 50 -Height 50
Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.png') -Width 44 -Height 44
Save-Logo -Path (Join-Path $AssetsPath 'Square71x71Logo.png') -Width 71 -Height 71
Save-Logo -Path (Join-Path $AssetsPath 'Square150x150Logo.png') -Width 150 -Height 150
Save-Logo -Path (Join-Path $AssetsPath 'Square310x310Logo.png') -Width 310 -Height 310
Save-Logo -Path (Join-Path $AssetsPath 'Wide310x150Logo.png') -Width 310 -Height 150
# The taskbar and the application list take the shortcut without a plate
Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-24_altform-unplated.png') -Width 24 -Height 24 -WithPlate $false
Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-32_altform-unplated.png') -Width 32 -Height 32 -WithPlate $false
Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-48_altform-unplated.png') -Width 48 -Height 48 -WithPlate $false
Write-Host 'The exe icon:'
Save-Icon -Path ([System.IO.Path]::GetFullPath($IconPath)) -Sizes @(16, 24, 32, 48, 64, 128, 256)
+24
View File
@@ -0,0 +1,24 @@
<!--
The project exists for the sake of a single dependency: the package brings
makeappx.exe. That way the MSIX build gets by without an installed Windows SDK
and repeats itself on any machine that has the .NET SDK. The project is not
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.
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584">
<!-- Only the programs from the package are needed, there are no assemblies to reference -->
<ExcludeAssets>all</ExcludeAssets>
<GeneratePathProperty>true</GeneratePathProperty>
</PackageReference>
</ItemGroup>
</Project>
+203
View File
@@ -0,0 +1,203 @@
<#
.SYNOPSIS
Builds the CursorLang MSIX package for the Microsoft Store.
.DESCRIPTION
Neither Visual Studio nor the Windows SDK is needed: makeappx arrives as a
NuGet package (Tools\SdkTools.csproj) and the application is built by the
plain .NET SDK.
The package is handed to Partner Center as it comes out of here — the Store
is where it gets everything else done to it.
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.
.PARAMETER IdentityName
The identity of the package — from Partner Center, the "Product identity"
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.
.EXAMPLE
# A check on your own machine: your architecture alone
pwsh -File Packaging\build-msix.ps1 -Architectures x64
.EXAMPLE
# A build for the Store — the identity comes from Partner Center
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
#>
[CmdletBinding()]
param(
# Four numbers, the last one has to be 0: that is what the Store requires
[string] $Version = '1.0.0.0',
[string] $IdentityName = 'CursorLang',
[string] $Publisher = 'CN=Aleksandr Neychev',
[string] $PublisherDisplayName = 'Aleksandr Neychev',
[ValidateSet('x64', 'arm64')]
[string[]] $Architectures = @('x64', 'arm64'),
[string] $OutputPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
$repository = Split-Path -Parent $root
$project = Join-Path $repository 'CursorLang\CursorLang.csproj'
$assets = Join-Path $root 'Assets'
$manifestTemplate = Join-Path $root 'AppxManifest.xml'
$toolsProject = Join-Path $root 'Tools\SdkTools.csproj'
if (-not $OutputPath) { $OutputPath = Join-Path $repository 'artifacts' }
$layoutRoot = Join-Path $OutputPath 'layout'
$packagesPath = Join-Path $OutputPath 'packages'
if ($Version -notmatch '^\d+\.\d+\.\d+\.0$') {
throw "The version '$Version' does not fit: the Store takes four numbers ending in zero, for example 1.0.0.0."
}
if (-not (Test-Path $assets)) {
throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first."
}
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 Get-SdkToolsPath {
<#
.SYNOPSIS
Returns the folder of the restored Windows SDK Build Tools package.
.DESCRIPTION
The path is asked of MSBuild rather than searched for in the package
cache: the project pins one version of the package, and MSBuild
names exactly it. A search would also find the versions left over
from other builds.
#>
$path = (& dotnet msbuild $toolsProject -getProperty:PkgMicrosoft_Windows_SDK_BuildTools -nologo) |
Where-Object { $_ -and $_.Trim() } |
Select-Object -Last 1
if ($LASTEXITCODE -ne 0 -or -not $path -or -not (Test-Path $path.Trim())) {
throw 'The Windows SDK Build Tools package folder could not be found. Did the restore go through?'
}
return $path.Trim()
}
function Get-SdkTool {
<#
.SYNOPSIS
Returns the path of a program from the Windows SDK Build Tools package.
#>
param(
[string] $Name,
[string] $PackagePath
)
# The bitness of the program has nothing to do with the bitness of the
# package being built: we take the one that runs on this machine
$host64 = if ([Environment]::Is64BitOperatingSystem) { 'x64' } else { 'x86' }
$tool = Get-ChildItem (Join-Path $PackagePath 'bin') -Recurse -Filter $Name -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "\\$host64\\" } |
Select-Object -First 1
if (-not $tool) {
throw "'$Name' was not found in the Windows SDK Build Tools package."
}
return $tool.FullName
}
Write-Host 'Fetching the Windows SDK programs...' -ForegroundColor Cyan
Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo')
$sdkTools = Get-SdkToolsPath
$makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools
Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
$built = @()
foreach ($architecture in $Architectures) {
Write-Host "Building $architecture..." -ForegroundColor Cyan
$layout = Join-Path $layoutRoot $architecture
Invoke-Tool -Path 'dotnet' -Arguments @(
'publish', $project,
'--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
}
$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
}
Write-Host ''
Write-Host 'Done.' -ForegroundColor Green
Write-Host " $result"
Write-Host ''
Write-Host ' This file is uploaded to Partner Center as it is.'