Skip to content

Building and Packaging

Valkerran edited this page Sep 29, 2026 · 3 revisions

Building & Packaging

Developer builds

dotnet build PCEdit.slnx                                            # everything
dotnet build PCEdit.SaveFileHandler/PCEdit.SaveFileHandler.csproj   # fast inner loop, no workloads
dotnet run --project PCEdit.Desktop/PCEdit.Desktop.csproj           # the app

global.json pins the SDK feature band (10.0.4xx). Tests are in Testing.

NuGet lock files

Since v1.5.1 every project commits a packages.lock.json recording the exact version and content hash of every direct and transitive package (issue #40). Directory.Build.props turns on locked mode wherever CI=true — which GitHub sets on every runner — so any restore in CI, including the implicit one inside dotnet build / dotnet test, fails with NU1004 if a project's dependencies drift from its lock file. Locally, restore keeps updating the lock as usual.

Important

Changing a PackageReference means running dotnet restore and committing the updated lock files with it. CI will reject the change otherwise.

The lock files describe a restore with no RuntimeIdentifier (PCEdit.Desktop lists its four RIDs in <RuntimeIdentifiers>, so their graphs are included). A RID-specific restore does not match them — it fails in CI and rewrites the lock locally — so every publish restores first and then publishes with --no-restore. The deploy/ scripts do this for you; by hand:

dotnet restore PCEdit.Desktop/PCEdit.Desktop.csproj
dotnet publish PCEdit.Desktop/PCEdit.Desktop.csproj --no-restore -c Release -r linux-x64 --self-contained true -o out

A Dependabot PR can occasionally miss a lock file for a project affected only through a ProjectReference (dependabot-core#13950). That fails loudly with NU1004 on the PR; run dotnet restore on the branch and commit the lock.

Release artifacts

One script per platform, in deploy/. All are self-contained (the .NET runtime is bundled) and all default the version to <VersionPrefix> in the repo-root Directory.Build.props.

Platform Command Output
Linux deploy/build-appimage.sh artifacts/PCEdit-<version>-<build>.x86_64.AppImage
Windows deploy/build-windows.ps1 artifacts/PCEdit-<version>-win-x64.zip
macOS deploy/build-macos.sh <rid> artifacts/PCEdit-<version>-macos-{x64,arm64}.zip

Pass an explicit version as the last argument (-Version for the PowerShell script) to override.

Important

Local builds are for testing only. Shipped artifacts come from the CI Release workflow — see Releasing. The Linux one especially: building on a newer distro silently raises the glibc floor.

Linux AppImage

Built with PupNet Deploy. Requirements: the .NET 10 SDK and Python 3; the KuiperZone.PupNet global tool (pinned version) and appimagetool are downloaded by the script if missing. FUSE is not needed at build time.

Since v1.4.1 appimagetool is pinned to a release (1.9.1) and verified against a SHA-256 before it is made executable — it used to come from the moving continuous tag, unverified. A failed or truncated download fails the check, so nothing unverified is ever run. To upgrade, change APPIMAGETOOL_VERSION and APPIMAGETOOL_SHA256 in build-appimage.sh together (the hash is the asset's digest on the GitHub release page).

File in deploy/ Purpose
pcedit.pupnet.conf PupNet configuration (identity, publish args, output)
pcedit.desktop Desktop entry template
com.valkerran.pcedit.metainfo.xml AppStream metadata — generated, do not hand-edit
icon/ Scalable pcedit.svg + rasterised PNGs (16–512 px)
verify-app-local-icu.sh Asserts a Linux build carries its own ICU

The AppStream file is regenerated from the string catalog by tools/i18n/gen_metainfo.py (which build-appimage.sh runs automatically), so the store listing stays in sync across all 15 locales.

The glibc rule

An AppImage does not bundle glibc. Build on the oldest practical base so the bundled runtime links against an old glibc and still runs on newer distros:

  • Release artifacts come from the CI job on ubuntu-22.04.
  • A build from a rolling or bleeding-edge distro (or WSL Ubuntu 24.04) raises the floor silently and breaks older targets. Use those to build for testing, never to ship.

ICU is bundled — and only on Linux

A self-contained publish does not include ICU: .NET dlopen()s the system copy and FailFasts at startup on a distro that has none. openSUSE Tumbleweed ships without it, which made the AppImage unlaunchable there (issue #4).

So PCEdit.Desktop.csproj references Microsoft.ICU.ICU4C.Runtime.linux-x64 and sets the System.Globalization.AppLocalIcu switch for the linux-x64 RID only.

The package is referenced unconditionally, and it is the per-RID package rather than the Microsoft.ICU.ICU4C.Runtime meta-package (since v1.5.1). That keeps restore identical whatever is being built, which the lock files depend on: referenced conditionally, the one shared restore never included ICU, and a Linux publish that skipped its own restore shipped without it. Its libraries live under runtimes/linux-x64/native, so Windows and macOS publishes carry none of them. The switch stays RID-conditional — and must not be widened to other Linux RIDs, since this package carries ICU for linux-x64 only.

Warning

<AppLocalIcuVersion> and the switch value must stay identical — the switch is the libicu*.so.<version> filename suffix. Neither is visible to ldd (the load is a dlopen), which is why verify-app-local-icu.sh guards both, from build-appimage.sh and from CI.

Windows and macOS use the OS ICU. Consequences worth knowing: the AppImage grew from ~43 MB to ~56.5 MB, and every distro now gets the same ICU 72 collation and CLDR data rather than whatever the system shipped.

Fonts

The bundled UI fonts have no CJK glyphs; zh-Hans / zh-Hant / ja / ko rely on the OS fallback through fontconfig. Noto CJK is 40 MB+ per weight and is deliberately not bundled.

Runtime libraries expected on the target

libX11, libICE, libSM, fontconfig, libGL — present on any desktop Linux install. A bare container or WSL rootfs may lack libICE / libSM, which Avalonia dlopens; the failure is a DllNotFoundException that ldd never predicted.

Windows

deploy/build-windows.ps1
deploy/build-windows.ps1 -Version 1.2.0

dotnet restore, then dotnet publish -r win-x64 --self-contained --no-restore, then Compress-Archive. The user unzips and runs PCEdit.exe — no installer, no .NET prerequisite. DPI awareness comes from PCEdit.Desktop/app.manifest. Not code-signed, so SmartScreen prompts on first run.

macOS

deploy/build-macos.sh osx-arm64          # Apple Silicon
deploy/build-macos.sh osx-x64 1.2.0      # Intel, explicit version

dotnet restore, then dotnet publish -r <rid> --self-contained --no-restore into a hand-assembled PCEdit.app — publish output in Contents/MacOS/, a generated Info.plist, and Contents/Resources/PCEdit.icns built from the 512 px icon via sips + iconutil (skipped on a non-macOS host). Zipped with ditto.

Not signed or notarised, so Gatekeeper quarantines it; LSMinimumSystemVersion is 11.0. The macos-latest runners are Apple Silicon, so osx-x64 is a self-contained cross-publish.

Verifying a Linux build

WSLg gives WSL2 a working Wayland + X11 display, so the AppImage opens a GUI there.

cd artifacts
chmod +x PCEdit-*.AppImage
./PCEdit-*.AppImage --appimage-extract-and-run
WAYLAND_DISPLAY= ./PCEdit-*.AppImage --appimage-extract-and-run    # force the X11 path
LANG=ja_JP.UTF-8 ./PCEdit-*.AppImage --appimage-extract-and-run    # locale + CJK glyphs

Per run, confirm: the window renders; Standard-2.json loads, edits, saves and round-trips; the disclaimer shows once and the acknowledgement persists; no missing-glyph boxes in a CJK locale.

Pre-release portability testing

A single distro only proves "runs on that distro". Portability is validated across several WSL2 distros on one Windows box, using the CI-built AppImage — do not rebuild per distro. The matrix (Ubuntu 22.04 / 24.04, Debian, Fedora, openSUSE Tumbleweed, Arch, optionally Ubuntu 20.04), the per-distro dependency commands, and the smoke test are in deploy/README.md.

Gate releases on the Ubuntu 22.04 / 24.04 / Fedora / Arch rows being green.

Continuous integration

.github/workflows/ci.yml, on every push to main and every pull request:

Job Steps
build-test Locked restore · both test projects · build PCEdit.Desktop · check the Avalonia build-telemetry targets are not imported · publish linux-x64 and run verify-app-local-icu.sh · regenerate the localization satellites, the AppStream metadata and the item catalog and fail on any diff
lock-files-cross-os A locked restore on windows-latest and macos-latest, so a graph that resolves differently per OS fails on the PR rather than mid-release
version-guard <VersionPrefix> is X.Y.Z and not behind the newest release tag

.github/workflows/release.yml builds and publishes the artifacts — see Releasing.

Workflow hardening (v1.4.1):

  • Every action is pinned to a full commit SHA, with its version in a trailing comment.
  • Tokens are least-privilege: CI is contents: read; in the Release workflow only the final release job gets contents: write, and no checkout keeps a git credential.
  • Dependabot (.github/dependabot.yml) checks NuGet and GitHub Actions weekly. Avalonia and the test tooling are grouped so they move together; the Microsoft.ICU.ICU4C.Runtime* packages are excluded, because its version is also the libicu filename suffix and deserves a deliberate distro-matrix run.

Clone this wiki locally