From cc439c1152d6eba752a4071be69fc0c7519c50c9 Mon Sep 17 00:00:00 2001 From: Kawa Date: Thu, 13 Aug 2026 15:13:42 +0200 Subject: [PATCH] Modified publish pipeline for WSL (flatpak, appimage builds) --- PACKAGING.md | 102 ++++++++++++++--- package.json | 3 + scripts/build-release.ps1 | 230 +++++++++++++++++++++++++++++++++++++- scripts/publish.ps1 | 26 ++++- 4 files changed, 334 insertions(+), 27 deletions(-) diff --git a/PACKAGING.md b/PACKAGING.md index b4c6726..cb0113a 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -69,6 +69,28 @@ Each `dist:*` script re-runs `vendor` and regenerates `build/icon.png` (`scripts/make-icon.cjs` rasterises the logo geometry with zlib only — no ImageMagick, no sharp). +### Why `desktopName` is `app.motionity.desktop.desktop` + +The doubled suffix is correct, do not trim it. Electron reads the **root-level** +`desktopName` from `package.json` and derives the Wayland `app_id` / X11 +`WM_CLASS` from it with the `.desktop` suffix stripped, so the value has to be +the desktop *file name*, not the app id. Flatpak in turn installs the entry as +`.desktop` and nothing can rename it — with `appId` +`app.motionity.desktop`, the file is `app.motionity.desktop.desktop`. + +`linux.syncDesktopName: true` makes electron-builder use the same base name for +the AppImage's embedded entry and write a matching `StartupWMClass`. Without the +pair, the build warns + +``` +electron uses desktopName as app_id / WM_CLASS for window association. + reason=desktopName is not set in package.json +``` + +and the running window is not linked to its launcher entry: GNOME shows a +generic icon and a second, unpinnable dock item instead of the installed app. +Changing `appId` means changing `desktopName` in step with it. + ### If the NSIS step fails with "Access denied" on `Motionity.exe` On a locked-down Windows machine the security agent can take an exclusive lock @@ -102,22 +124,58 @@ $env:ELECTRON_RUN_AS_NODE=$null; npm run dev # PowerShell ### Is WSL enough for AppImage and Flatpak? -**AppImage: yes.** electron-builder produces the squashfs itself, so no FUSE is -needed at build time. To *run* the result inside WSL you need `libfuse2` -(or `./Motionity-1.0.0-x64.AppImage --appimage-extract-and-run`), and WSLg on -Windows 11 gives you the GUI. +Yes, and `build-release.ps1 -UseWsl` does it for you from Windows: -**Flatpak: technically yes, practically annoying.** `flatpak-builder` runs under -WSL2 (the kernel has the user namespaces and `/dev/fuse` that bubblewrap needs), -but you must install the runtimes by hand first and there is no -`xdg-desktop-portal` to fall back on. If it fights you, build it in a Linux -container instead — it is the same command with fewer moving parts. +```powershell +./scripts/build-release.ps1 -UseWsl # .exe on Windows, Linux bundles in WSL +./scripts/build-release.ps1 -Targets linux -UseWsl # Linux bundles only +./scripts/publish.ps1 -Tag v2.0.1 -PublishRelease -UseWsl # same, then attach to the Gitea release +``` + +Both bundles have been built this way on this machine (`Ubuntu`, WSL2 kernel +6.18): a 172 MB AppImage and a 133 MB Flatpak, checksums verified with +`sha256sum -c`. Under the hood: + +- the `npm ci`, `vendor` and icon steps run **once on the Windows side**, and the + WSL build reads them back through `/mnt/c` — one worktree, no second checkout; +- only `linux-appimage` and `linux-flatpak` are delegated. `-UseWsl` on a Linux + host, or with no Linux target, warns and changes nothing; +- the distro is the first installed one that is not `docker-desktop`, override + with `-WslDistro`. `docker-desktop` is skipped deliberately: it is Docker's own + LinuxKit VM, with no apt and no home to install the flatpak runtimes into, and + it is usually the *default* distro, so a blind `wsl --` lands there; +- missing tooling fails **before** the build with the apt or `flatpak install` + line to run, because electron-builder's own error for an absent flatpak ref is + a bare exit code naming neither the ref nor the remote. + +**Why the build stages in `~/.cache/motionity-build` and not in `dist/`.** +electron-builder chmods every file it unpacks from the Electron zip, and `/mnt/c` +is mounted without the `metadata` option, so chmod is refused: + +``` +⨯ EPERM: operation not permitted, chmod '.../dist/linux-unpacked.tmp/locales/de.pak' +``` + +The alternative fix is `options = "metadata"` under `[automount]` in +`/etc/wsl.conf` plus a `wsl --shutdown` — a global, sudo-and-reboot change to the +distro. Building into ext4 and copying the two finished bundles back into `dist/` +needs neither, and is faster anyway. Reading `src/` over the mount is fine; +nothing chmods the input. The copy back is `cp -f`, never `cp -p` — preserving +modes means chmod, which is the EPERM being avoided. + +**AppImage** needs no extra tooling: electron-builder downloads its own appimage +bundle and writes the squashfs itself, no FUSE at build time. To *run* the result +inside WSL you need `libfuse2` (or +`./motionity-*.AppImage --appimage-extract-and-run`); WSLg gives you the GUI. + +**Flatpak** needs `flatpak-builder` and the runtimes installed by hand — the +WSL2 kernel has the user namespaces and `/dev/fuse` that bubblewrap wants, but +there is no `xdg-desktop-portal` to fall back on. **`.exe`: no.** Build it on the Windows side. Cross-building NSIS from Linux needs Wine and rules out signing. -This machine currently has no WSL distro other than `docker-desktop`, so the -Linux targets have not been run here. Setup, from PowerShell: +One-time distro setup, from PowerShell: ```powershell wsl --install -d Ubuntu @@ -127,7 +185,7 @@ Then inside Ubuntu: ```bash sudo apt update -sudo apt install -y nodejs npm libfuse2 # AppImage +sudo apt install -y nodejs npm libfuse2 # AppImage (libfuse2 only to run it) sudo apt install -y flatpak flatpak-builder elfutils # Flatpak flatpak remote-add --if-not-exists --user flathub \ https://dl.flathub.org/repo/flathub.flatpakrepo @@ -135,14 +193,22 @@ flatpak install --user -y flathub \ org.freedesktop.Platform//23.08 \ org.freedesktop.Sdk//23.08 \ org.electronjs.Electron2.BaseApp//23.08 - -cd /mnt/c/Users//git\ azuze/motionity-2 -npm install -npm run dist:linux ``` -Note that building on `/mnt/c` is slow. Copying the tree into the WSL -filesystem (`~/motionity`) is several times faster. +Those three refs must match `build.flatpak.runtimeVersion` / `baseVersion` in +`package.json`; `build-release.ps1` reads them from there when it checks. + +To build inside the distro directly instead — no `-UseWsl`, and faster still, +since `src/` is read locally too: + +```bash +git clone ~/motionity && cd ~/motionity +npm ci && npm run dist:linux +``` + +`wsl.exe` writes its own listings as UTF-16LE, which PowerShell 5.1 renders as +NUL-interleaved text — `wsl -l -v` can look like it has one distro when it has +two. `$env:WSL_UTF8 = "1"` fixes it. ## 2. Docker diff --git a/package.json b/package.json index d7848e0..66b2309 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "motionity", "productName": "Motionity", "version": "2.0.1", + "desktopName": "app.motionity.desktop.desktop", "description": "Web-based motion graphics editor with keyframing, masking, filters and text animations", "license": "MIT", "author": "Kawa", @@ -16,6 +17,7 @@ "dist:appimage": "npm run vendor && npm run icons && electron-builder --linux AppImage", "dist:flatpak": "npm run vendor && npm run icons && electron-builder --linux flatpak", "dist:linux": "npm run vendor && npm run icons && electron-builder --linux AppImage flatpak", + "dist:linux:wsl": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/build-release.ps1 -Targets linux -UseWsl", "docker:build": "docker build -t motionity:latest .", "docker:run": "docker run --rm -p 8080:8080 motionity:latest", "release:build": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/build-release.ps1", @@ -78,6 +80,7 @@ "icon": "build/icon.png", "category": "Graphics", "synopsis": "Motion graphics editor", + "syncDesktopName": true, "desktop": { "entry": { "Name": "Motionity", diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index 85c37c1..9410781 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -28,10 +28,20 @@ Windows builds the .exe targets; AppImage and Flatpak need a Linux host or WSL (see PACKAGING.md). Nothing here cross-builds. + -UseWsl makes that split automatic: the .exe targets run on Windows, and the + Linux ones are handed to a WSL distro over /mnt/c, against this same worktree. + The npm install, the vendor step and the icon all happen once on the Windows + side and the WSL build reads them through the mount, so the artifacts still + land in dist/ and the checksum step below sees every one of them. + .EXAMPLE ./scripts/build-release.ps1 Build every target at v. +.EXAMPLE + ./scripts/build-release.ps1 -UseWsl + Same, with AppImage and Flatpak built in the first non-docker WSL distro. + .EXAMPLE ./scripts/build-release.ps1 -Targets win -Tag v1.1.0 Windows installers only, named v1.1.0. @@ -40,6 +50,10 @@ ./scripts/build-release.ps1 -Targets win,linux-appimage Windows installers plus the Linux AppImage — no Flatpak. +.EXAMPLE + ./scripts/build-release.ps1 -Targets linux -UseWsl -WslDistro Ubuntu-24.04 + Both Linux bundles, in a named distro. + .EXAMPLE ./scripts/build-release.ps1 -SkipVendor -SkipDeps Reuse src/vendor/ and node_modules as they are — the fast rebuild. @@ -62,6 +76,14 @@ param( # Skip the npm install even when node_modules is missing. [switch]$SkipDeps, + # Build the Linux targets inside WSL instead of warning that they cannot be + # built on Windows. Ignored on a Linux host, where they build natively. + [switch]$UseWsl, + + # WSL distro to build in. Defaults to the first installed one that is not + # docker-desktop. + [string]$WslDistro, + # Remove dist/ before building. [switch]$Clean ) @@ -91,6 +113,153 @@ function Get-ArtifactName { return $Stem + '.${ext}' } +# --- WSL plumbing ------------------------------------------------------------- + +function Invoke-Wsl { + param([Parameter(Mandatory)][string]$Distro, [Parameter(Mandatory)][string]$Command) + Write-Host " > wsl -d $Distro -- $Command" -ForegroundColor DarkGray + # bash -lc, so PATH matches an interactive shell: node installed through nvm + # or fnm is not on the default non-login PATH. + & wsl.exe -d $Distro -e bash -lc $Command + if ($LASTEXITCODE -ne 0) { + throw "the WSL build in '$Distro' failed with exit code $LASTEXITCODE." + } +} + +function Test-WslCommand { + param([Parameter(Mandatory)][string]$Distro, [Parameter(Mandatory)][string]$Command) + & wsl.exe -d $Distro -e bash -lc $Command *> $null + return ($LASTEXITCODE -eq 0) +} + +function ConvertTo-BashArg { + <# + Single quotes, always. The artifactName arguments carry a literal ${ext} + for electron-builder to expand, and bash would expand it to nothing first + inside double quotes; the repo path has a space in it. + #> + param([Parameter(Mandatory)][string]$Value) + return "'" + $Value.Replace("'", "'\''") + "'" +} + +function Get-WslDistro { + <# + docker-desktop is excluded on purpose. It is Docker Desktop's own LinuxKit + VM: no apt, no user home to install the flatpak runtimes into — and it is + usually the *default* distro, so picking blind would land there. + #> + param([string]$Requested) + + if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { + throw "-UseWsl needs wsl.exe on PATH. Install a distro with 'wsl --install -d Ubuntu' (PACKAGING.md has the rest of the setup)." + } + + # wsl.exe writes its own listings as UTF-16LE, which PowerShell 5.1 reads back + # as NUL-interleaved text. WSL_UTF8 fixes it at the source; the -replace is the + # fallback for WSL older than 0.64. + $previousUtf8 = $env:WSL_UTF8 + $env:WSL_UTF8 = "1" + try { $listed = (& wsl.exe --list --quiet) -join "`n" } + finally { $env:WSL_UTF8 = $previousUtf8 } + + $distros = @(($listed -replace "`0", "") -split "`r?`n" | + ForEach-Object { $_.Trim() } | Where-Object { $_ }) + + if ($Requested) { + if ($distros -notcontains $Requested) { + throw "WSL distro '$Requested' is not installed. Installed: $($distros -join ', ')." + } + return $Requested + } + + $usable = @($distros | Where-Object { $_ -notlike "docker-desktop*" }) + if (-not $usable.Count) { + throw "no WSL distro that can build here (installed: $($distros -join ', ')). Run 'wsl --install -d Ubuntu', then the package setup in PACKAGING.md." + } + return $usable[0] +} + +function Get-WslPath { + param([Parameter(Mandatory)][string]$Distro, [Parameter(Mandatory)][string]$WindowsPath) + # -e wslpath rather than a shell: the backslashes and the space in the repo + # path then reach wslpath as one literal argv entry, unquoted and unmangled. + $out = (& wsl.exe -d $Distro -e wslpath -a -u $WindowsPath) + $path = (@($out) -join "").Replace("`0", "").Trim() + if ($LASTEXITCODE -ne 0 -or -not $path) { + throw "wslpath failed in '$Distro' for '$WindowsPath' — is the Windows drive mounted in that distro?" + } + return $path +} + +function Get-WslStageDir { + <# + Where the Linux build actually happens. It cannot be dist/ on /mnt/c: + electron-builder chmods every file it unpacks out of the Electron zip, and + drvfs answers chmod with EPERM unless /mnt/c was mounted with the metadata + option — a global change to the distro needing sudo and a wsl --shutdown. + + ⨯ EPERM: operation not permitted, chmod '.../linux-unpacked.tmp/locales/de.pak' + + Staging in the distro's own filesystem and copying the finished bundles back + needs none of that, and is faster besides. Reading src/ over the mount is + still fine — nothing chmods the input. + #> + param([Parameter(Mandatory)][string]$Distro) + # printf, not echo: no trailing newline to trim off the path. + $out = (& wsl.exe -d $Distro -e bash -lc 'printf %s "${XDG_CACHE_HOME:-$HOME/.cache}/motionity-build"') + $path = (@($out) -join "").Replace("`0", "").Trim() + if ($LASTEXITCODE -ne 0 -or $path -notlike "/*") { + throw "could not resolve a staging directory in '$Distro' (got '$path')." + } + return $path +} + +function Assert-WslBuildEnv { + <# + Fail before the build rather than during it. electron-builder's own error + for a missing flatpak ref is a bare flatpak-builder exit code that names + neither the ref nor the remote. + #> + param( + [Parameter(Mandatory)][string]$Distro, + [Parameter(Mandatory)][string[]]$WslTargets, + [Parameter(Mandatory)]$Pkg + ) + + if (-not (Test-WslCommand $Distro 'command -v node')) { + throw "no node in WSL distro '$Distro'. Inside it: sudo apt update && sudo apt install -y nodejs npm" + } + + # AppImage needs nothing else: electron-builder downloads its own appimage + # tooling and writes the squashfs itself. libfuse2 is only needed to *run* the + # result, which is not this script's job. + if ($WslTargets -notcontains "linux-flatpak") { return } + + if (-not (Test-WslCommand $Distro 'command -v flatpak-builder')) { + throw "no flatpak-builder in WSL distro '$Distro'. Inside it: sudo apt install -y flatpak flatpak-builder elfutils" + } + + $runtimeVersion = $Pkg.build.flatpak.runtimeVersion + $baseVersion = $Pkg.build.flatpak.baseVersion + if (-not $runtimeVersion) { $runtimeVersion = "23.08" } + if (-not $baseVersion) { $baseVersion = $runtimeVersion } + + $refs = @( + "org.freedesktop.Platform//$runtimeVersion", + "org.freedesktop.Sdk//$runtimeVersion", + "org.electronjs.Electron2.BaseApp//$baseVersion" + ) + $missing = @($refs | Where-Object { -not (Test-WslCommand $Distro "flatpak info $_") }) + if ($missing.Count) { + throw @" +flatpak refs missing in '$Distro': $($missing -join ', ') +Inside the distro: + flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak install --user -y flathub $($refs -join ' ') +"@ + } +} + $repoRoot = Split-Path -Parent $PSScriptRoot Push-Location $repoRoot try { @@ -119,11 +288,37 @@ try { } $resolvedTargets = @($resolvedTargets | Select-Object -Unique) - # electron-builder produces AppImage and Flatpak with Linux-only tooling - # (appimagetool, flatpak-builder). Warned rather than blocked: the same script - # runs under pwsh on a Linux box or in WSL, which is where that target belongs. - if (($resolvedTargets -like "linux-*") -and $env:OS -eq "Windows_NT") { - Write-Warning "the Linux targets need a Linux host or WSL — electron-builder cannot produce AppImage or Flatpak on Windows (PACKAGING.md has the WSL setup)." + # electron-builder produces AppImage and Flatpak with Linux-only tooling (its + # downloaded appimage bundle, and flatpak-builder). -UseWsl hands those two + # targets to a WSL distro; without it they stay a warning rather than an error, + # because the other right answer is running this whole script under pwsh on a + # Linux box, where they build natively. + $onWindows = ($env:OS -eq "Windows_NT") + $wslTargets = @($resolvedTargets | Where-Object { $_ -like "linux-*" }) + $useWslHere = $onWindows -and $UseWsl -and [bool]$wslTargets.Count + + if ($onWindows -and $wslTargets.Count -and -not $UseWsl) { + Write-Warning "the Linux targets need a Linux host or WSL — electron-builder cannot produce AppImage or Flatpak on Windows. Add -UseWsl to build them in a WSL distro (PACKAGING.md has the setup)." + } + if ($UseWsl -and -not $onWindows) { + Write-Warning "-UseWsl ignored: this is already a Linux host, so the Linux targets build natively." + } + if ($UseWsl -and $onWindows -and -not $wslTargets.Count) { + Write-Warning "-UseWsl ignored: no Linux target was requested (-Targets $($Targets -join ', '))." + } + + $wslRepo = $null + $wslStage = $null + if ($useWslHere) { + $WslDistro = Get-WslDistro -Requested $WslDistro + $wslRepo = Get-WslPath -Distro $WslDistro -WindowsPath $repoRoot + $wslStage = Get-WslStageDir -Distro $WslDistro + Write-Host "Linux targets go to WSL" -ForegroundColor Cyan + Write-Host " distro : $WslDistro" + Write-Host " worktree: $wslRepo" + Write-Host " staging : $wslStage (copied back into dist/)" + Assert-WslBuildEnv -Distro $WslDistro -WslTargets $wslTargets -Pkg $pkg + Write-Host "" } if ($Clean) { @@ -207,7 +402,30 @@ try { } } - Invoke-Checked $builder $builderArgs + if ($useWslHere -and $target -like "linux-*") { + # `node cli.js` rather than node_modules/.bin/electron-builder: the + # extensionless shim npm writes on Windows is a sh script, and whether + # it is executable across the mount depends on the drvfs options. + # + # The output goes to the distro's filesystem (see Get-WslStageDir) and + # the bundles are copied back afterwards. `cp -f`, never `cp -p`: + # preserving modes means chmod, which is the EPERM this avoids. + # + # The rm clears this tag's previous bundles from the staging directory, + # so the copy back cannot pick up an artifact from an earlier build that + # electron-builder did not overwrite this time round. + $quotedArgs = @($builderArgs | ForEach-Object { ConvertTo-BashArg $_ }) -join " " + $stage = ConvertTo-BashArg $wslStage + $wslCommand = "cd $(ConvertTo-BashArg $wslRepo)" + + " && mkdir -p $stage" + + " && rm -f $stage/$prefix-*" + + " && USE_HARD_LINKS=false node node_modules/electron-builder/cli.js $quotedArgs $(ConvertTo-BashArg "-c.directories.output=$wslStage")" + + " && cp -f $stage/$prefix-* $(ConvertTo-BashArg "$wslRepo/dist")/" + Invoke-Wsl -Distro $WslDistro -Command $wslCommand + } + else { + Invoke-Checked $builder $builderArgs + } Write-Host "" } diff --git a/scripts/publish.ps1 b/scripts/publish.ps1 index 3e510ac..ffff219 100644 --- a/scripts/publish.ps1 +++ b/scripts/publish.ps1 @@ -16,8 +16,8 @@ -BinariesOnly ships just the installers: no docker build, no docker login, no image push, and the release upload is implied. It takes the target list to build (win, linux-appimage, linux-flatpak — comma-separated) and that list - overrides -Targets. The Linux targets need a Linux host or WSL, which is where - -BinariesOnly linux-appimage,linux-flatpak belongs. + overrides -Targets. The Linux targets need a Linux host or WSL: on Windows, + add -UseWsl and build-release.ps1 hands them to a WSL distro. Credentials are read, in order of precedence: 1. -Username / -Password parameters @@ -45,6 +45,11 @@ ./scripts/publish.ps1 -BinariesOnly win,linux-appimage -Tag v1.1.0 Windows installers plus the Linux AppImage (no Flatpak), attached to v1.1.0. +.EXAMPLE + ./scripts/publish.ps1 -Tag v1.1.0 -PublishRelease -UseWsl + Full release from Windows: image push, .exe installers built natively, AppImage + and Flatpak built in WSL, everything attached to release v1.1.0. + .EXAMPLE ./scripts/publish.ps1 -BinariesOnly win -NoBinaryBuild -Tag v1.1.0 Retry a failed upload: attach the installers already in dist/ without rebuilding @@ -105,6 +110,11 @@ param( [string[]]$Targets = @("win", "linux"), [switch]$SkipVendor, + # Forwarded to build-release.ps1: build the Linux targets in a WSL distro + # instead of warning that Windows cannot produce them. + [switch]$UseWsl, + [string]$WslDistro, + # Reuse the installers already in dist/ instead of re-running the build. For # retrying a failed upload without paying for the build again. [switch]$NoBinaryBuild, @@ -413,7 +423,17 @@ try { # `& script.ps1` leaves $LASTEXITCODE untouched, and with -NoBuild no # docker command has reset it, so checking it would rethrow whatever the # caller's shell last failed at. - & (Join-Path $PSScriptRoot "build-release.ps1") -Tag $Tag -Targets $Targets -SkipVendor:$SkipVendor + # -WslDistro is only passed when set: build-release.ps1 treats an empty + # string as "not requested" either way, but splatting nothing keeps the + # -WhatIf/-Verbose trace readable. + $buildParams = @{ + Tag = $Tag + Targets = $Targets + SkipVendor = $SkipVendor + UseWsl = $UseWsl + } + if ($WslDistro) { $buildParams["WslDistro"] = $WslDistro } + & (Join-Path $PSScriptRoot "build-release.ps1") @buildParams } $artifacts = @(Get-ChildItem $distDir -Filter "motionity-$Tag-*" -File | ForEach-Object FullName)