mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Signed release pipeline (macOS/Windows/Linux/Web); README community note
Ports photocraft's release workflow + packaging (signing, notarization, MSI/DMG/deb/rpm/AppImage/web), adds `cargo xtask version`. File-type associations omitted (route B); app keeps its own .desktop. Fresh WiX UpgradeCode. README 'about' note updated. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
75e4e3ced0
commit
453f97b549
24 files changed
+1633
-3
No files matched your search
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
LightCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
|
||||
|
||||
wix build lightcraft.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
|
||||
|
||||
Per-machine install into Program Files, a Start Menu shortcut, `.pcraft` owned by LightCraft,
|
||||
and LightCraft offered under "Open with" for PSD/PSB and the image formats it reads (without
|
||||
taking over the user's default app for them).
|
||||
-->
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package
|
||||
Name="LightCraft"
|
||||
Manufacturer="Learning Machines LLC"
|
||||
Version="$(var.Version)"
|
||||
UpgradeCode="229BB552-5EFD-479A-BA86-8CB6D926390C"
|
||||
Scope="perMachine"
|
||||
Compressed="yes"
|
||||
Language="1033">
|
||||
<SummaryInformation Description="LightCraft image editor" Manufacturer="Learning Machines LLC" />
|
||||
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other
|
||||
(MSI versions are numeric, so 1.2.3-rc.1 and 1.2.3-rc.2 are both 1.2.3). -->
|
||||
<MajorUpgrade AllowSameVersionUpgrades="yes"
|
||||
DowngradeErrorMessage="A newer version of LightCraft is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
||||
|
||||
<Icon Id="LightcraftIcon" SourceFile="$(var.IconPath)" />
|
||||
<Property Id="ARPPRODUCTICON" Value="LightcraftIcon" />
|
||||
<Property Id="ARPURLINFOABOUT" Value="https://getartcraft.com/apps/lightcraft" />
|
||||
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/lightcraft" />
|
||||
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
|
||||
|
||||
<StandardDirectory Id="ProgramFiles6432Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="LightCraft" />
|
||||
</StandardDirectory>
|
||||
<StandardDirectory Id="ProgramMenuFolder" />
|
||||
|
||||
<Feature Id="Main" Title="LightCraft" Level="1">
|
||||
<ComponentGroupRef Id="LightcraftFiles" />
|
||||
</Feature>
|
||||
</Package>
|
||||
|
||||
<Fragment>
|
||||
<ComponentGroup Id="LightcraftFiles" Directory="INSTALLFOLDER">
|
||||
<Component Id="LightcraftApp" Guid="8A1D3A39-F7C1-47B1-97AE-40C80BF82F4A">
|
||||
<File Id="LightcraftExe" Source="$(var.BinDir)\lightcraft.exe" KeyPath="yes">
|
||||
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="LightCraft"
|
||||
Description="LightCraft image editor" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
|
||||
Icon="LightcraftIcon" IconIndex="0" />
|
||||
</File>
|
||||
|
||||
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Value="[#LightcraftExe]" Type="string" />
|
||||
</Component>
|
||||
|
||||
<Component Id="LightcraftCli">
|
||||
<File Id="LightcraftCliExe" Source="$(var.BinDir)\lightcraft-cli.exe" KeyPath="yes" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -0,0 +1,100 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build, sign and package LightCraft for Windows.
|
||||
|
||||
.DESCRIPTION
|
||||
Produces, in $env:DIST (default: dist/release):
|
||||
lightcraft-<version>-windows-<arch>.msi per-machine installer (WiX v5)
|
||||
lightcraft-<version>-windows-<arch>-portable.zip lightcraft.exe + lightcraft-cli.exe
|
||||
|
||||
The binaries link the C runtime statically (+crt-static), so neither the MSI nor the portable
|
||||
zip needs the Visual C++ redistributable. Signing is delegated to sign.ps1 (skipped with a
|
||||
warning when no signing secrets are set).
|
||||
|
||||
Needs: Rust (MSVC toolchain + the target), the Windows SDK (rc.exe, signtool.exe),
|
||||
and WiX v5: dotnet tool install --global wix --version 5.0.2
|
||||
|
||||
.EXAMPLE
|
||||
pwsh packaging/windows/package.ps1 -Arch x64
|
||||
pwsh packaging/windows/package.ps1 -Arch x86 -SkipBuild
|
||||
#>
|
||||
param(
|
||||
[ValidateSet('x64', 'x86')] [string] $Arch = 'x64',
|
||||
[switch] $SkipBuild
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
|
||||
function Invoke-Native([string] $What, [scriptblock] $Block) {
|
||||
Write-Output "==> $What"
|
||||
& $Block
|
||||
if ($LASTEXITCODE -ne 0) { throw "$What failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
|
||||
# The version lives in one place: [workspace.package] version in the root Cargo.toml.
|
||||
$Version = $env:LIGHTCRAFT_VERSION
|
||||
if (-not $Version) {
|
||||
$inPkg = $false
|
||||
foreach ($line in Get-Content (Join-Path $Root 'Cargo.toml')) {
|
||||
if ($line -match '^\s*\[') { $inPkg = ($line.Trim() -eq '[workspace.package]'); continue }
|
||||
if ($inPkg -and $line -match '^\s*version\s*=\s*"([^"]+)"') { $Version = $Matches[1]; break }
|
||||
}
|
||||
}
|
||||
if (-not $Version) { throw 'could not read [workspace.package] version from Cargo.toml' }
|
||||
# MSI ProductVersion is numeric (major.minor.build); pre-release tags are dropped there.
|
||||
$MsiVersion = ($Version -split '-')[0]
|
||||
|
||||
$Target = if ($Arch -eq 'x64') { 'x86_64-pc-windows-msvc' } else { 'i686-pc-windows-msvc' }
|
||||
$Dist = if ($env:DIST) { $env:DIST } else { Join-Path $Root 'dist\release' }
|
||||
$TargetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { Join-Path $Root 'target' }
|
||||
New-Item -ItemType Directory -Force -Path $Dist | Out-Null
|
||||
|
||||
if (-not $env:LIGHTCRAFT_BUILD_SHA) { $env:LIGHTCRAFT_BUILD_SHA = (git -C $Root rev-parse HEAD 2>$null) }
|
||||
if (-not $env:LIGHTCRAFT_BUILD_DATE) { $env:LIGHTCRAFT_BUILD_DATE = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd') }
|
||||
|
||||
Write-Output "LightCraft $Version for Windows $Arch ($Target)"
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
# Static CRT: no VC++ redistributable needed. Scoped to the target so host build scripts and
|
||||
# proc-macros are unaffected.
|
||||
$flagVar = 'CARGO_TARGET_' + ($Target.ToUpper() -replace '-', '_') + '_RUSTFLAGS'
|
||||
[Environment]::SetEnvironmentVariable($flagVar, '-C target-feature=+crt-static')
|
||||
# Fail the build (rather than warn) if the icon/VERSIONINFO can't be embedded.
|
||||
$env:LIGHTCRAFT_REQUIRE_WINRES = '1'
|
||||
Invoke-Native "cargo build ($Target)" { cargo build --release --locked -p lightcraft -p lightcraft-cli --target $Target }
|
||||
}
|
||||
|
||||
$Bin = Join-Path $TargetDir "$Target\release"
|
||||
$Stage = Join-Path $TargetDir "windows-package\$Arch"
|
||||
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
|
||||
Copy-Item (Join-Path $Bin 'lightcraft.exe'), (Join-Path $Bin 'lightcraft-cli.exe') $Stage
|
||||
|
||||
& (Join-Path $PSScriptRoot 'sign.ps1') (Join-Path $Stage 'lightcraft.exe') (Join-Path $Stage 'lightcraft-cli.exe')
|
||||
|
||||
# ---- MSI ---------------------------------------------------------------------------------------
|
||||
$Msi = Join-Path $Dist "lightcraft-$Version-windows-$Arch.msi"
|
||||
Invoke-Native 'wix build' {
|
||||
wix build (Join-Path $PSScriptRoot 'lightcraft.wxs') -arch $Arch `
|
||||
-d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\lightcraft.ico')" `
|
||||
-o $Msi
|
||||
}
|
||||
# wix writes its debug symbols (.wixpdb) next to the MSI; keep them out of the release assets.
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($Msi, '.wixpdb'))
|
||||
& (Join-Path $PSScriptRoot 'sign.ps1') $Msi
|
||||
|
||||
# ---- portable zip ------------------------------------------------------------------------------
|
||||
$Portable = Join-Path $TargetDir "windows-package\lightcraft-$Version-windows-$Arch-portable"
|
||||
Remove-Item -Recurse -Force $Portable -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $Portable | Out-Null
|
||||
Copy-Item (Join-Path $Stage '*.exe') $Portable
|
||||
foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') {
|
||||
$p = Join-Path $Root $f
|
||||
if (Test-Path $p) { Copy-Item $p $Portable }
|
||||
}
|
||||
$Zip = Join-Path $Dist "lightcraft-$Version-windows-$Arch-portable.zip"
|
||||
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $Portable -DestinationPath $Zip
|
||||
|
||||
Invoke-Native 'lightcraft-cli --version' { & (Join-Path $Stage 'lightcraft-cli.exe') --version }
|
||||
Get-Item $Msi, $Zip | Format-Table Name, Length
|
||||
@@ -0,0 +1,95 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Authenticode-sign files with signtool, using whichever signing material is configured.
|
||||
|
||||
.DESCRIPTION
|
||||
Tries, in order:
|
||||
1. A code-signing certificate file: WINDOWS_CERTIFICATE (base64 .pfx), WINDOWS_CERTIFICATE_PASSWORD
|
||||
2. Azure Trusted Signing: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET,
|
||||
AZURE_SIGNING_ENDPOINT (e.g. https://eus.codesigning.azure.net),
|
||||
AZURE_SIGNING_ACCOUNT, AZURE_CERT_PROFILE
|
||||
With neither, it prints a warning and leaves the files unsigned (exit code 0), so test builds
|
||||
still produce installers.
|
||||
|
||||
This is the one place to adapt when the signing setup changes (an HSM/cloud key, a different
|
||||
timestamp server, ...). Optional overrides: WINDOWS_TIMESTAMP_URL, SIGNTOOL (path to signtool.exe).
|
||||
|
||||
.EXAMPLE
|
||||
pwsh packaging/windows/sign.ps1 dist/lightcraft.exe dist/Lightcraft.msi
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)]
|
||||
[string[]] $Files
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-Warn([string] $Message) {
|
||||
if ($env:GITHUB_ACTIONS) { Write-Output "::warning::$Message" } else { Write-Warning $Message }
|
||||
}
|
||||
|
||||
function Find-SignTool {
|
||||
if ($env:SIGNTOOL) { return $env:SIGNTOOL }
|
||||
$onPath = Get-Command signtool.exe -ErrorAction SilentlyContinue
|
||||
if ($onPath) { return $onPath.Source }
|
||||
$kits = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
|
||||
$found = Get-ChildItem -Path $kits -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } |
|
||||
Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if (-not $found) { throw 'signtool.exe not found (install the Windows SDK)' }
|
||||
return $found.FullName
|
||||
}
|
||||
|
||||
function Invoke-SignTool([string[]] $Arguments) {
|
||||
& $script:SignTool @Arguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "signtool $($Arguments[0]) failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
|
||||
$Files = $Files | ForEach-Object { (Resolve-Path $_).Path }
|
||||
$haveCert = [bool]$env:WINDOWS_CERTIFICATE
|
||||
$azureVars = 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', 'AZURE_SIGNING_ENDPOINT', 'AZURE_SIGNING_ACCOUNT', 'AZURE_CERT_PROFILE'
|
||||
$haveAzure = -not ($azureVars | Where-Object { -not [Environment]::GetEnvironmentVariable($_) })
|
||||
|
||||
if (-not $haveCert -and -not $haveAzure) {
|
||||
Write-Warn "Windows signing secrets not set (WINDOWS_CERTIFICATE or AZURE_*): leaving unsigned: $($Files -join ', ')"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$script:SignTool = Find-SignTool
|
||||
$common = @('sign', '/v', '/fd', 'SHA256', '/td', 'SHA256', '/d', 'LightCraft', '/du', 'https://github.com/storytold/lightcraft')
|
||||
$tmp = Join-Path ([IO.Path]::GetTempPath()) "lightcraft-sign-$PID"
|
||||
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
||||
|
||||
try {
|
||||
if ($haveCert) {
|
||||
Write-Output "Signing with WINDOWS_CERTIFICATE: $($Files -join ', ')"
|
||||
$pfx = Join-Path $tmp 'cert.pfx'
|
||||
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE))
|
||||
$ts = if ($env:WINDOWS_TIMESTAMP_URL) { $env:WINDOWS_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
|
||||
$signArgs = $common + @('/tr', $ts, '/f', $pfx)
|
||||
if ($env:WINDOWS_CERTIFICATE_PASSWORD) { $signArgs += @('/p', $env:WINDOWS_CERTIFICATE_PASSWORD) }
|
||||
Invoke-SignTool ($signArgs + $Files)
|
||||
}
|
||||
else {
|
||||
Write-Output "Signing with Azure Trusted Signing ($env:AZURE_SIGNING_ACCOUNT / $env:AZURE_CERT_PROFILE): $($Files -join ', ')"
|
||||
# The dlib authenticates with DefaultAzureCredential, which reads AZURE_TENANT_ID,
|
||||
# AZURE_CLIENT_ID and AZURE_CLIENT_SECRET from the environment.
|
||||
$pkg = Join-Path $tmp 'trusted-signing.zip'
|
||||
Invoke-WebRequest -Uri 'https://www.nuget.org/api/v2/package/Microsoft.Trusted.Signing.Client' -OutFile $pkg
|
||||
Expand-Archive -Path $pkg -DestinationPath (Join-Path $tmp 'client') -Force
|
||||
$dlib = Get-ChildItem -Path (Join-Path $tmp 'client') -Recurse -Filter Azure.CodeSigning.Dlib.dll |
|
||||
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1
|
||||
if (-not $dlib) { throw 'Azure.CodeSigning.Dlib.dll not found in Microsoft.Trusted.Signing.Client' }
|
||||
$metadata = Join-Path $tmp 'metadata.json'
|
||||
@{
|
||||
Endpoint = $env:AZURE_SIGNING_ENDPOINT
|
||||
CodeSigningAccountName = $env:AZURE_SIGNING_ACCOUNT
|
||||
CertificateProfileName = $env:AZURE_CERT_PROFILE
|
||||
} | ConvertTo-Json | Set-Content -Path $metadata -Encoding utf8
|
||||
$ts = if ($env:WINDOWS_TIMESTAMP_URL) { $env:WINDOWS_TIMESTAMP_URL } else { 'http://timestamp.acs.microsoft.com' }
|
||||
Invoke-SignTool ($common + @('/tr', $ts, '/dlib', $dlib.FullName, '/dmdf', $metadata) + $Files)
|
||||
}
|
||||
Invoke-SignTool (@('verify', '/pa', '/v') + $Files)
|
||||
}
|
||||
finally {
|
||||
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
Reference in new issue
Block a user