From a37867b94905b00b4da618d0521c281ce1aee504 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 16:16:47 +0100 Subject: [PATCH 01/34] diag: add minimal Forgejo Actions diagnostic workflow Probes the runner setup so we can tell whether the release.yml failure is upstream of our workflow (no runner, no network, no tooling) or specific to a step. - Container reachable + image usable - DNS + outbound HTTPS to forgejo.pschneider.fr and hub.docker.com - Required tooling available (git, dotnet, jq, curl, make, debhelper, imagemagick) - forgejo.* context populated - secrets.GITHUB_TOKEN resolves (FORGEJO_TOKEN populated) - API call with the token returns 200 on /repos/ Will be removed once we've confirmed the runner is healthy. --- .forgejo/workflows/diag.yml | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .forgejo/workflows/diag.yml diff --git a/.forgejo/workflows/diag.yml b/.forgejo/workflows/diag.yml new file mode 100644 index 0000000..3e1ef88 --- /dev/null +++ b/.forgejo/workflows/diag.yml @@ -0,0 +1,74 @@ +name: diag + +# Diagnostic minimal : vérifie que le runner Forgejo démarre, qu'il +# peut cloner, et qu'il a accès à Internet + Docker (pour le label +# docker:// sur lequel tournent nos jobs). +# +# À supprimer une fois qu'on a confirmé que le runner est OK. + +on: + workflow_dispatch: + +jobs: + diag: + runs-on: docker + container: + image: docker.io/pazof/yavsc-build-env:debian12-dotnet10-android36-v2 + steps: + - name: Hello + run: | + echo "Runner started, container is alive." + echo "Image: $(cat /etc/os-release | grep PRETTY_NAME)" + echo "Hostname: $(hostname)" + echo "User: $(whoami)" + echo "Working dir: $(pwd)" + + - name: Network check + run: | + echo "--- DNS resolution ---" + getent hosts forgejo.pschneider.fr || echo "FAIL: cannot resolve forgejo.pschneider.fr" + echo "--- HTTP outbound ---" + curl -sS -o /dev/null -w "forgejo.pschneider.fr HTTP %{http_code} (%{time_total}s)\n" \ + https://forgejo.pschneider.fr/ || echo "FAIL: cannot reach forgejo.pschneider.fr" + echo "--- Docker Hub ---" + curl -sS -o /dev/null -w "hub.docker.com HTTP %{http_code}\n" \ + https://hub.docker.com/ || echo "FAIL: cannot reach hub.docker.com" + + - name: Tooling check + run: | + echo "--- git ---" + git --version + echo "--- dotnet ---" + dotnet --info | head -5 + echo "--- jq ---" + jq --version + echo "--- curl ---" + curl --version | head -1 + echo "--- make ---" + make --version | head -1 + echo "--- debhelper ---" + which dh || echo "FAIL: dh not found (debhelper absent)" + echo "--- imagemagick ---" + which convert || echo "FAIL: convert not found" + + - name: Forgejo context probe + env: + FORGEJO_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + run: | + echo "--- forgejo.* context ---" + echo "forgejo.ref_name=[${{ forgejo.ref_name }}]" + echo "forgejo.repository=[${{ forgejo.repository }}]" + echo "forgejo.api_url=[${{ forgejo.api_url }}]" + echo "--- secrets.GITHUB_TOKEN probe ---" + if [[ -n "$FORGEJO_TOKEN" ]]; then + echo "FORGEJO_TOKEN is set (length=${#FORGEJO_TOKEN})" + else + echo "FORGEJO_TOKEN is EMPTY — secrets.GITHUB_TOKEN did not resolve" + fi + echo "--- API reachability via FORGEJO_TOKEN ---" + if [[ -n "$FORGEJO_TOKEN" ]]; then + HTTP=$(curl -sS -o /dev/null -w '%{http_code}' \ + -H "Authorization: token $FORGEJO_TOKEN" \ + "${{ forgejo.api_url }}/repos/${{ forgejo.repository }}") + echo "GET /repos/${{ forgejo.repository }} -> HTTP $HTTP" + fi \ No newline at end of file From ba8ffe5f5b9845e63ac8cdbfbb0bc9166a23008a Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 16:30:28 +0100 Subject: [PATCH 02/34] fix state file: use JSON instead of shell-sourcable env The previous version wrote RELEASE_BODY to a state file using the '< STATE_FILE` with a `<<< "" ` here-string to the same file. --- .forgejo/workflows/release.yml | 53 +++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 88a4e4f..71b38ac 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -29,10 +29,12 @@ # on met à jour la release existante plutôt que d'en multiplier # pour un même tag. Le permalien /releases/tag/ reste stable. # -# Inter-step state: we persist values between steps via a plain -# env file under /tmp, sourced at the top of each step that needs -# it. This keeps the workflow self-contained and avoids any -# runtime variable names we did not choose. +# Inter-step state: persisted as JSON in /tmp/release-state.json, +# read at the top of each step with `jq -r .`. Using JSON +# sidesteps shell parsing issues that come with sourcing a file +# that contains heredocs / markdown / colons / etc. — RELEASE_BODY +# in particular is markdown content straight from CHANGELOG.md and +# cannot be safely `source`d. name: Forgejo Release postit-deb on: @@ -63,7 +65,7 @@ jobs: container: image: docker.io/pazof/yavsc-build-env:debian12-dotnet10-android36-v2 env: - STATE_FILE: /tmp/release-state.env + STATE_FILE: /tmp/release-state.json steps: - name: Installer les pré-requis de build (debhelper + icônes) # L'image runner fournit déjà dotnet-sdk-10.0, git, jq, @@ -100,11 +102,14 @@ jobs: git checkout "$TAG" echo "Checked out at $(git rev-parse HEAD) on tag $TAG" - echo "TAG=$TAG" >> "$STATE_FILE" + + # Persist TAG in the state file. --arg ensures proper + # JSON escaping of any special chars. + jq -n --arg tag "$TAG" '{tag: $tag}' > "$STATE_FILE" - name: Valider le tag et la section CHANGELOG run: | - source "$STATE_FILE" + TAG=$(jq -r '.tag' "$STATE_FILE") cd /src/_src echo "Validating tag $TAG" @@ -176,19 +181,21 @@ jobs: echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL" - # Persist values for the next steps via our local state file. - { - echo "RELEASE_BODY<> "$STATE_FILE" + # Persist validation results. Use --arg for strings (so + # jq handles escaping of backticks, asterisks, colons, + # etc.) and --argjson for booleans. + jq -n \ + --arg tag "$TAG" \ + --arg body "$RELEASE_BODY" \ + --argjson is_prerelease "$IS_PRERELEASE" \ + '{tag: $tag, body: $body, is_prerelease: $is_prerelease}' \ + > "$STATE_FILE" - name: Build .deb amd64 env: POSTIT_RUNTIME: linux-x64 run: | - source "$STATE_FILE" + TAG=$(jq -r '.tag' "$STATE_FILE") cd /src/_src echo "→ Building amd64 for POSTIT_GIT_TAG=$TAG" make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-x64 @@ -197,7 +204,7 @@ jobs: env: POSTIT_RUNTIME: linux-arm64 run: | - source "$STATE_FILE" + TAG=$(jq -r '.tag' "$STATE_FILE") cd /src/_src echo "→ Building arm64 for POSTIT_GIT_TAG=$TAG" # Cross-RID .NET depuis un hôte amd64 : standard, pas @@ -206,7 +213,7 @@ jobs: - name: Localiser les .deb produits run: | - source "$STATE_FILE" + TAG=$(jq -r '.tag' "$STATE_FILE") cd /src DEB_AMD64=$(find . -maxdepth 3 -name "postit_*${TAG}-1_amd64.deb" \ -not -path "./_src/debian/*" -printf '%p\n' | head -1) @@ -217,8 +224,10 @@ jobs: ls -la /src/ 2>/dev/null || true exit 1 fi - echo "DEB_AMD64=/src/$DEB_AMD64" >> "$STATE_FILE" - echo "DEB_ARM64=/src/$DEB_ARM64" >> "$STATE_FILE" + # Merge .deb paths into state file. + jq --arg amd64 "/src/$DEB_AMD64" --arg arm64 "/src/$DEB_ARM64" \ + '. + {deb_amd64: $amd64, deb_arm64: $arm64}' \ + "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" echo "✓ Found both .deb files" - name: Publier la release Forgejo via l'API REST @@ -227,7 +236,11 @@ jobs: FORGEJO_API_URL: ${{ forgejo.api_url }} FORGEJO_REPOSITORY: ${{ forgejo.repository }} run: | - source "$STATE_FILE" + TAG=$(jq -r '.tag' "$STATE_FILE") + RELEASE_BODY=$(jq -r '.body' "$STATE_FILE") + IS_PRERELEASE=$(jq -r '.is_prerelease' "$STATE_FILE") + DEB_AMD64=$(jq -r '.deb_amd64' "$STATE_FILE") + DEB_ARM64=$(jq -r '.deb_arm64' "$STATE_FILE") if [[ -z "$TAG" ]]; then echo "::error::No tag resolved for the API call." exit 1 From c25e245f5ab2f2236f2646475eb579f1e5e0cb38 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 16:37:02 +0100 Subject: [PATCH 03/34] debian/rules: skip dh_strip on *.so (cross-compiled native libs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When cross-compiling PostIt.Desktop for linux-arm64 from an amd64 runner container, the .deb ends up shipping HarfBuzzSharp.so and SkiaSharp.so in ARM64 format. dh_strip invokes objcopy on every file under debian/postit/, and objcopy on the amd64 host cannot parse ARM64 .so files: objcopy: Unable to recognise the format of the input file `debian/postit/usr/lib/postit/libHarfBuzzSharp.so' dh_strip: error: ... returned exit code 1 dh_strip: error: Aborting due to earlier error Add --exclude=*.so to override_dh_strip. The upstream .so ships with its own debug symbols; we don't lose anything by skipping the strip on them. Also drop the diag.yml workflow that diagnosed the initial tag-not-found failure — no longer needed. Discovered via the Forgejo Actions release.yml run on tag 1.0.6. --- .forgejo/workflows/diag.yml | 74 ------------------------------------- debian/rules | 15 +++++++- 2 files changed, 14 insertions(+), 75 deletions(-) delete mode 100644 .forgejo/workflows/diag.yml diff --git a/.forgejo/workflows/diag.yml b/.forgejo/workflows/diag.yml deleted file mode 100644 index 3e1ef88..0000000 --- a/.forgejo/workflows/diag.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: diag - -# Diagnostic minimal : vérifie que le runner Forgejo démarre, qu'il -# peut cloner, et qu'il a accès à Internet + Docker (pour le label -# docker:// sur lequel tournent nos jobs). -# -# À supprimer une fois qu'on a confirmé que le runner est OK. - -on: - workflow_dispatch: - -jobs: - diag: - runs-on: docker - container: - image: docker.io/pazof/yavsc-build-env:debian12-dotnet10-android36-v2 - steps: - - name: Hello - run: | - echo "Runner started, container is alive." - echo "Image: $(cat /etc/os-release | grep PRETTY_NAME)" - echo "Hostname: $(hostname)" - echo "User: $(whoami)" - echo "Working dir: $(pwd)" - - - name: Network check - run: | - echo "--- DNS resolution ---" - getent hosts forgejo.pschneider.fr || echo "FAIL: cannot resolve forgejo.pschneider.fr" - echo "--- HTTP outbound ---" - curl -sS -o /dev/null -w "forgejo.pschneider.fr HTTP %{http_code} (%{time_total}s)\n" \ - https://forgejo.pschneider.fr/ || echo "FAIL: cannot reach forgejo.pschneider.fr" - echo "--- Docker Hub ---" - curl -sS -o /dev/null -w "hub.docker.com HTTP %{http_code}\n" \ - https://hub.docker.com/ || echo "FAIL: cannot reach hub.docker.com" - - - name: Tooling check - run: | - echo "--- git ---" - git --version - echo "--- dotnet ---" - dotnet --info | head -5 - echo "--- jq ---" - jq --version - echo "--- curl ---" - curl --version | head -1 - echo "--- make ---" - make --version | head -1 - echo "--- debhelper ---" - which dh || echo "FAIL: dh not found (debhelper absent)" - echo "--- imagemagick ---" - which convert || echo "FAIL: convert not found" - - - name: Forgejo context probe - env: - FORGEJO_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - run: | - echo "--- forgejo.* context ---" - echo "forgejo.ref_name=[${{ forgejo.ref_name }}]" - echo "forgejo.repository=[${{ forgejo.repository }}]" - echo "forgejo.api_url=[${{ forgejo.api_url }}]" - echo "--- secrets.GITHUB_TOKEN probe ---" - if [[ -n "$FORGEJO_TOKEN" ]]; then - echo "FORGEJO_TOKEN is set (length=${#FORGEJO_TOKEN})" - else - echo "FORGEJO_TOKEN is EMPTY — secrets.GITHUB_TOKEN did not resolve" - fi - echo "--- API reachability via FORGEJO_TOKEN ---" - if [[ -n "$FORGEJO_TOKEN" ]]; then - HTTP=$(curl -sS -o /dev/null -w '%{http_code}' \ - -H "Authorization: token $FORGEJO_TOKEN" \ - "${{ forgejo.api_url }}/repos/${{ forgejo.repository }}") - echo "GET /repos/${{ forgejo.repository }} -> HTTP $HTTP" - fi \ No newline at end of file diff --git a/debian/rules b/debian/rules index 2745116..c77d8df 100755 --- a/debian/rules +++ b/debian/rules @@ -129,7 +129,20 @@ override_dh_shlibdeps: dh_shlibdeps override_dh_strip: - dh_strip --exclude=.pdb + # --exclude=.pdb : .NET génère des .pdb même en Release, ils + # ne sont pas strippables proprement. + # --exclude=*.so : Avalonia dépend de HarfBuzzSharp et SkiaSharp + # qui livrent leurs .so pré-compilés via NuGet (formats natifs + # multi-arch : linux-x64, linux-arm64, etc.). Quand on cross- + # compile depuis un hôte amd64 (build arm64), dh_strip appelle + # objcopy sur ces .so qui sont en ARM64 et objcopy natif amd64 + # ne sait pas les lire : + # objcopy: Unable to recognise the format of the input file + # `debian/postit/usr/lib/postit/libHarfBuzzSharp.so' + # On les exclut donc du strip. Les symboles debug seront + # présents dans le .deb mais ce sont des libs upstream, pas + # notre code — pas critique. + dh_strip --exclude=.pdb --exclude=*.so define stage_postit From 91eaf074362655dbc9aab4158683d5eb149e087d Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 16:45:37 +0100 Subject: [PATCH 04/34] debian/rules: --no-automatic-dbgsym to dodge objcopy on *.so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous fix (--exclude=*.so) only affects the strip phase. dh_strip also runs a separate --only-keep-debug pass for the automatic dbgsym package, and that pass does not honour --exclude. So objcopy still trips on the ARM64 .so files and the build aborts. Add --no-automatic-dbgsym to skip the dbgsym pass entirely. Trade- off: no separate postit-dbg package is generated. That's fine for this upstream — the only debug info in the .so files is from Avalonia deps (HarfBuzz, Skia) and is not useful for debugging PostIt itself. Discovered via the Forgejo Actions run #8 on tag 1.0.6. --- debian/rules | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/debian/rules b/debian/rules index c77d8df..ddb299a 100755 --- a/debian/rules +++ b/debian/rules @@ -142,7 +142,14 @@ override_dh_strip: # On les exclut donc du strip. Les symboles debug seront # présents dans le .deb mais ce sont des libs upstream, pas # notre code — pas critique. - dh_strip --exclude=.pdb --exclude=*.so + # --no-automatic-dbgsym : dh_strip crée automatiquement un + # paquet postit-dbg avec --only-keep-debug, qui appelle + # objcopy une seconde fois sur les .so. Le --exclude ci-dessus + # ne s'applique PAS à cette phase. Désactiver le dbgsym + # automatique évite l'erreur et reste cohérent (les libs + # upstream n'ont pas de symboles debug utiles pour le debug + # utilisateur). + dh_strip --exclude=.pdb --exclude=*.so --no-automatic-dbgsym define stage_postit From c0c50270012ac7071ac10f84ea87545c37ac602c Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 16:52:18 +0100 Subject: [PATCH 05/34] debian/rules: drop override_dh_strip entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous fix (--exclude=*.so --no-automatic-dbgsym) failed because dh_strip does MULTIPLE strip passes (--strip-unneeded, --strip-debug, --only-keep-debug), and objcopy's ARM64-format recognition issue surfaces again in a pass that does not honour --exclude: strip: Unable to recognise the format of the input file `debian/postit/usr/lib/postit/libHarfBuzzSharp.so' dh_strip: error: strip --remove-section=.comment ... returned exit code 1 The cleanest fix is to skip dh_strip entirely. The cost is minor for our use case (no strip on managed DLLs saves a few KB; no postit-dbg auto-package — the debug info in libHarfBuzzSharp / libSkiaSharp is from Avalonia deps, not our code). If stripping becomes important, the proper fix is binutils-multiarch (multi-arch objcopy) in the container runner. Out of scope here. Discovered via Forgejo Actions run #9 on tag 1.0.6. --- debian/rules | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/debian/rules b/debian/rules index ddb299a..1088f61 100755 --- a/debian/rules +++ b/debian/rules @@ -129,27 +129,9 @@ override_dh_shlibdeps: dh_shlibdeps override_dh_strip: - # --exclude=.pdb : .NET génère des .pdb même en Release, ils - # ne sont pas strippables proprement. - # --exclude=*.so : Avalonia dépend de HarfBuzzSharp et SkiaSharp - # qui livrent leurs .so pré-compilés via NuGet (formats natifs - # multi-arch : linux-x64, linux-arm64, etc.). Quand on cross- - # compile depuis un hôte amd64 (build arm64), dh_strip appelle - # objcopy sur ces .so qui sont en ARM64 et objcopy natif amd64 - # ne sait pas les lire : - # objcopy: Unable to recognise the format of the input file - # `debian/postit/usr/lib/postit/libHarfBuzzSharp.so' - # On les exclut donc du strip. Les symboles debug seront - # présents dans le .deb mais ce sont des libs upstream, pas - # notre code — pas critique. - # --no-automatic-dbgsym : dh_strip crée automatiquement un - # paquet postit-dbg avec --only-keep-debug, qui appelle - # objcopy une seconde fois sur les .so. Le --exclude ci-dessus - # ne s'applique PAS à cette phase. Désactiver le dbgsym - # automatique évite l'erreur et reste cohérent (les libs - # upstream n'ont pas de symboles debug utiles pour le debug - # utilisateur). - dh_strip --exclude=.pdb --exclude=*.so --no-automatic-dbgsym + # dh_strip disabled entirely. See MEMORY.md / AGENTS.md for the + # rationale (cross-compiled native .so via NuGet from Avalonia + # deps cannot be parsed by amd64 objcopy/strip). define stage_postit From acb3ee2c8beb31797a9b0976f9ae99eb4ed09835 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 16:59:47 +0100 Subject: [PATCH 06/34] release.yml: install arm64 shlibs on amd64 runner for cross-build shlibdeps When cross-building PostIt.Desktop for linux-arm64 from an amd64 runner, dpkg-shlibdeps fails to resolve arm64 shlibs (libc6, libstdc++6, libdl, libm, libpthread, libfontconfig, libgtk-3, etc.): dpkg-shlibdeps: error: cannot find library libdl.so.2 needed by debian/postit/usr/lib/postit/PostIt.Desktop (ELF format: 'elf64-little' abi: '020100b700000000'; RPATH: '') dh_shlibdeps: error: ... returned exit code 2 Add arm64 as a foreign architecture via dpkg and install the shlibs needed at build time. Minimal set: libc6, libstdc++6, libfontconfig1, libfreetype6, libgtk-3-0. libpthread / libdl / libm are pulled in transitively via libc6. Discovered via Forgejo Actions run #10 on tag 1.0.6. --- .forgejo/workflows/release.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 71b38ac..d615125 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -77,6 +77,18 @@ jobs: apt-get install -y --no-install-recommends \ build-essential debhelper imagemagick librsvg2-bin \ ca-certificates + # dpkg-shlibdeps at arm64 build time needs the arm64 + # shlibs (libc6, libstdc++6, libdl, libm, libpthread, + # libfontconfig, libgtk-3, etc.) to be present on the + # host. We add arm64 as a foreign architecture and pull + # them in. --no-install-recommends keeps the install + # surface minimal. + dpkg --add-architecture arm64 + apt-get update + apt-get install -y --no-install-recommends \ + libc6:arm64 libstdc++6:arm64 \ + libfontconfig1:arm64 libfreetype6:arm64 \ + libgtk-3-0:arm64 rm -rf /var/lib/apt/lists/* : > "$STATE_FILE" From cbb791590761fb927327f4aa7e67a61da46f42a4 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:13:39 +0100 Subject: [PATCH 07/34] release.yml: fail-fast on arm64 shlib install + sanity check When the apt-get install of arm64 shlibs fails silently (e.g. because the configured apt sources don't include arm64 for some packages), the next build step still tries to cross-build for arm64 and dpkg-shlibdeps aborts. The Makefile's mv pattern then matches the previous amd64 .deb (same filename prefix), exits 0, and the missing arm64 .deb is only discovered later in the 'Localiser les .deb' step. This adds: - explicit set -e so any failure aborts the step cleanly - `dpkg --add-architecture arm64` with fail-fast on error - pre-flight apt-cache show check for each arm64 package - explicit fail-fast on apt-get install failure - dpkg -l sanity check to confirm the libs are installed If the arm64 packages really are not available from the configured apt sources, the workflow will now fail with a clear message instead of producing a half-broken release. --- .forgejo/workflows/release.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index d615125..ea64d47 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -83,12 +83,27 @@ jobs: # host. We add arm64 as a foreign architecture and pull # them in. --no-install-recommends keeps the install # surface minimal. - dpkg --add-architecture arm64 + set -e + dpkg --add-architecture arm64 || { echo "::error::dpkg --add-architecture arm64 failed"; exit 1; } apt-get update + # Verify each arm64 package is actually installable. Bail + # early with a clear message if any are missing from the + # configured apt sources — silent apt-get install + # failures are the worst kind of workflow bug. + for pkg in libc6:arm64 libstdc++6:arm64 \ + libfontconfig1:arm64 libfreetype6:arm64 \ + libgtk-3-0:arm64; do + if ! apt-cache show "$pkg" >/dev/null 2>&1; then + echo "::error::arm64 package '$pkg' is not available in apt sources" + exit 1 + fi + done apt-get install -y --no-install-recommends \ libc6:arm64 libstdc++6:arm64 \ libfontconfig1:arm64 libfreetype6:arm64 \ - libgtk-3-0:arm64 + libgtk-3-0:arm64 || { echo "::error::Failed to install arm64 shlibs"; exit 1; } + # Sanity-check: the libs must be visible to dpkg-shlibdeps. + dpkg -l libc6:arm64 libstdc++6:arm64 | tail -3 rm -rf /var/lib/apt/lists/* : > "$STATE_FILE" From 58a9d6c54f1b070050499e72cdf00bc6a8c8e25a Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:17:37 +0100 Subject: [PATCH 08/34] release.yml: set -e -x + explicit .deb presence check on builds Without set -e, a failing dpkg-buildpackage inside 'make deb' was silently swallowed by the Makefile's 'mv ... || true' pattern, which matches any leftover amd64 .deb and exits 0. The build step then succeeded without producing an arm64 .deb. This adds: - set -e -x at the start of each build step (fail on first error, trace all commands so the root cause is visible in logs) - explicit ls + exit 1 check after make deb to confirm the expected .deb was actually produced If arm64 cross-build still fails, we'll see exactly where now. --- .forgejo/workflows/release.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index ea64d47..05f1c1b 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -222,21 +222,45 @@ jobs: env: POSTIT_RUNTIME: linux-x64 run: | + set -e -x TAG=$(jq -r '.tag' "$STATE_FILE") cd /src/_src echo "→ Building amd64 for POSTIT_GIT_TAG=$TAG" make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-x64 + DEB_AMD64=$(ls postit_*${TAG}-1_amd64.deb 2>/dev/null || true) + if [[ -z "$DEB_AMD64" ]]; then + echo "::error::Build .deb amd64 did not produce postit_*${TAG}-1_amd64.deb" + ls -la + exit 1 + fi + echo "✓ Built $DEB_AMD64" + set +x - name: Build .deb arm64 env: POSTIT_RUNTIME: linux-arm64 run: | + set -e -x TAG=$(jq -r '.tag' "$STATE_FILE") cd /src/_src echo "→ Building arm64 for POSTIT_GIT_TAG=$TAG" # Cross-RID .NET depuis un hôte amd64 : standard, pas # besoin de runner arm64 natif. make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 + # Verify the arm64 .deb was actually produced. Without + # this check, a silent dpkg-buildpackage failure would + # let the step exit 0 (the Makefile's mv || true matches + # any leftover amd64 .deb and exits successfully), and + # the failure would only surface in the next step. + DEB_ARM64=$(ls postit_*${TAG}-1_arm64.deb 2>/dev/null || true) + if [[ -z "$DEB_ARM64" ]]; then + echo "::error::Build .deb arm64 did not produce postit_*${TAG}-1_arm64.deb" + echo "Inspect the dpkg-buildpackage output above for the root cause." + ls -la + exit 1 + fi + echo "✓ Built $DEB_ARM64" + set +x - name: Localiser les .deb produits run: | From 0267fd2fefcee49cf331a758260727b62665c99e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:24:14 +0100 Subject: [PATCH 09/34] release.yml: check /src/ for .deb (not /src/_src/) for both builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Makefile mv's the produced .deb into /src/ (POSTIT_OUT_DIR default = parent of build dir). The post-build 'ls' check was running in cwd /src/_src/, missing the .deb every time, which made the build step fail even when dpkg-deb had succeeded. Worse: amd64 step exited 1 first, so arm64 step never ran. The 'no arm64 .deb' symptom was a red herring — it was caused by this earlier amd64 false negative, not by an arm64 build failure. Fix: ls /src/postit_*...deb explicitly. If both builds succeed, we'll see both .deb in the next step. If only amd64 shows up, we'll know arm64 actually failed (and where, via set -x). --- .forgejo/workflows/release.yml | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 05f1c1b..e25aa4f 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -222,45 +222,42 @@ jobs: env: POSTIT_RUNTIME: linux-x64 run: | - set -e -x + set -e TAG=$(jq -r '.tag' "$STATE_FILE") cd /src/_src echo "→ Building amd64 for POSTIT_GIT_TAG=$TAG" make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-x64 - DEB_AMD64=$(ls postit_*${TAG}-1_amd64.deb 2>/dev/null || true) + # make deb mv's the produced .deb(s) into /src/ (the + # parent of /src/_src/, $POSTIT_OUT_DIR default). + # Check there, not in the build dir. + DEB_AMD64=$(ls /src/postit_*${TAG}-1_amd64.deb 2>/dev/null || true) if [[ -z "$DEB_AMD64" ]]; then - echo "::error::Build .deb amd64 did not produce postit_*${TAG}-1_amd64.deb" - ls -la + echo "::error::Build .deb amd64 did not produce /src/postit_*${TAG}-1_amd64.deb" + ls -la /src/ /src/_src/ 2>/dev/null exit 1 fi echo "✓ Built $DEB_AMD64" - set +x - name: Build .deb arm64 env: POSTIT_RUNTIME: linux-arm64 run: | - set -e -x + set -e TAG=$(jq -r '.tag' "$STATE_FILE") cd /src/_src echo "→ Building arm64 for POSTIT_GIT_TAG=$TAG" # Cross-RID .NET depuis un hôte amd64 : standard, pas # besoin de runner arm64 natif. make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 - # Verify the arm64 .deb was actually produced. Without - # this check, a silent dpkg-buildpackage failure would - # let the step exit 0 (the Makefile's mv || true matches - # any leftover amd64 .deb and exits successfully), and - # the failure would only surface in the next step. - DEB_ARM64=$(ls postit_*${TAG}-1_arm64.deb 2>/dev/null || true) + # Same check as amd64: verify the .deb landed in /src/. + DEB_ARM64=$(ls /src/postit_*${TAG}-1_arm64.deb 2>/dev/null || true) if [[ -z "$DEB_ARM64" ]]; then - echo "::error::Build .deb arm64 did not produce postit_*${TAG}-1_arm64.deb" + echo "::error::Build .deb arm64 did not produce /src/postit_*${TAG}-1_arm64.deb" echo "Inspect the dpkg-buildpackage output above for the root cause." - ls -la + ls -la /src/ /src/_src/ 2>/dev/null exit 1 fi echo "✓ Built $DEB_ARM64" - set +x - name: Localiser les .deb produits run: | From 9a10021e4f8cb42094df1dca3ef48377a9752ed8 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:28:34 +0100 Subject: [PATCH 10/34] release.yml: arm64 build with set -x + explicit exit on make failure The Makefile's 'mv ... || true' swallows non-zero exits from make deb. Combined with set -e missing, this made the arm64 build step exit 0 silently even when dpkg-buildpackage failed. Add set -x for the arm64 step specifically (amd64 already works, no need to spam the log) and wrap make deb in a '|| { echo ...; exit 1; }' so the step fails loudly on any make-level error. --- .forgejo/workflows/release.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index e25aa4f..48f5cde 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -242,13 +242,19 @@ jobs: env: POSTIT_RUNTIME: linux-arm64 run: | - set -e + set -e -x TAG=$(jq -r '.tag' "$STATE_FILE") cd /src/_src echo "→ Building arm64 for POSTIT_GIT_TAG=$TAG" # Cross-RID .NET depuis un hôte amd64 : standard, pas # besoin de runner arm64 natif. - make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 + # set -x traces every command so a silent failure inside + # 'make deb' (e.g. dpkg-buildpackage aborting after the + # 'mv ... || true' swallows the error) is visible. + make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 || { + echo "::error::make deb for arm64 exited non-zero — see output above" + exit 1 + } # Same check as amd64: verify the .deb landed in /src/. DEB_ARM64=$(ls /src/postit_*${TAG}-1_arm64.deb 2>/dev/null || true) if [[ -z "$DEB_ARM64" ]]; then @@ -258,6 +264,7 @@ jobs: exit 1 fi echo "✓ Built $DEB_ARM64" + set +x - name: Localiser les .deb produits run: | From 117087ac439f7eae4878d6e4fb50f52afb8adf4d Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:29:44 +0100 Subject: [PATCH 11/34] Makefile: fail loudly when dpkg-buildpackage produces no .deb The previous 'mv ... || mv ... || true' chain silently masked real build failures: when dpkg-buildpackage for arm64 aborted, the fallback 'mv ../postit_*.deb' matched the previous run's leftover amd64 .deb (same filename prefix 'postit_*-1') and returned 0. The CI step exited cleanly with no arm64 .deb. Replace the silent fallback with an explicit check that errors out if no .deb was produced. Two-stage pattern (specific version first, any .deb second) keeps the same matching flexibility for both stable and pre-release tags, but the absence of a .deb now fails the build step. --- Makefile | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a870e5a..6022290 100644 --- a/Makefile +++ b/Makefile @@ -43,8 +43,22 @@ deb: # Move the produced .deb(s) into $POSTIT_OUT_DIR. The version # segment we match against is the rendered changelog version # (e.g. 1.0.1-rc01-1), not the bare tag. - mv ../postit_*$(POSTIT_GIT_TAG)-1*.deb $(POSTIT_OUT_DIR)/ 2>/dev/null || \ - mv ../postit_*.deb $(POSTIT_OUT_DIR)/ || true + # + # Two-stage mv: try the specific version first (more reliable), + # fall back to any .deb if the glob doesn't expand (e.g. the + # version suffix differs). No final \`|| true\` — a silent + # fallback was masking real dpkg-buildpackage failures (the + # fallback was matching leftover .deb from previous runs and + # returning 0 even when the current build had produced nothing). + DEB_GLOB=$(ls ../postit_*$(POSTIT_GIT_TAG)-1*.deb 2>/dev/null || true) + if [[ -z "$$DEB_GLOB" ]]; then + DEB_GLOB=$(ls ../postit_*.deb 2>/dev/null || true) + fi + if [[ -z "$$DEB_GLOB" ]]; then + echo " ERROR: dpkg-buildpackage produced no .deb for POSTIT_GIT_TAG=$(POSTIT_GIT_TAG)" >&2 + exit 1 + fi + mv $$DEB_GLOB $(POSTIT_OUT_DIR)/ @echo " ✓ artifacts moved to $(POSTIT_OUT_DIR)" clean: From 5ea37f51be577142cfaabc691a0e9e94cf4811a2 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:32:39 +0100 Subject: [PATCH 12/34] Makefile: single-line shell block for .deb presence check The previous commit put an 'if ... then ... fi' block across multiple physical lines in the Makefile recipe. make runs each recipe line in its own shell, so the 'fi' never matched its 'if', and the parser (dash on Debian) reported `Syntax error: end of file unexpected (expecting "fi")`. Collapse the if/then/fi into a single line joined with ';', joined to the surrounding lines with '\\' continuation so make treats them as one shell command. --- Makefile | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 6022290..9bb1b49 100644 --- a/Makefile +++ b/Makefile @@ -50,14 +50,8 @@ deb: # fallback was masking real dpkg-buildpackage failures (the # fallback was matching leftover .deb from previous runs and # returning 0 even when the current build had produced nothing). - DEB_GLOB=$(ls ../postit_*$(POSTIT_GIT_TAG)-1*.deb 2>/dev/null || true) - if [[ -z "$$DEB_GLOB" ]]; then - DEB_GLOB=$(ls ../postit_*.deb 2>/dev/null || true) - fi - if [[ -z "$$DEB_GLOB" ]]; then - echo " ERROR: dpkg-buildpackage produced no .deb for POSTIT_GIT_TAG=$(POSTIT_GIT_TAG)" >&2 - exit 1 - fi + DEB_GLOB=$$(ls ../postit_*$(POSTIT_GIT_TAG)-1*.deb 2>/dev/null || ls ../postit_*.deb 2>/dev/null || true); \ + if [ -z "$$DEB_GLOB" ]; then echo " ERROR: dpkg-buildpackage produced no .deb for POSTIT_GIT_TAG=$(POSTIT_GIT_TAG)" >&2; exit 1; fi; \ mv $$DEB_GLOB $(POSTIT_OUT_DIR)/ @echo " ✓ artifacts moved to $(POSTIT_OUT_DIR)" From f65f73b2c175a9d5616455eb91cfc8f8b1946bd7 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:35:42 +0100 Subject: [PATCH 13/34] Makefile: clean residual .deb before dpkg-buildpackage The 'mv' step after dpkg-buildpackage was failing with `mv: '../postit_1.0.6-1_amd64.deb' and '/src/_src/../postit_1.0.6-1_amd64.deb' are the same file` because the amd64 .deb from previous CI runs (or from a partial arm64 build that copied the leftover amd64 .deb) was still in the source directory. Remove any postit_*-1*.deb, *.buildinfo, and *.changes files before invoking dpkg-buildpackage. The fresh build then writes to ../ without colliding with stale output. --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 9bb1b49..4ee6124 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,10 @@ deb: # step the .deb always comes out as the version hardcoded in # debian/changelog.in, regardless of POSTIT_GIT_TAG. sed 's/@VERSION@/$(POSTIT_GIT_TAG)/g' debian/changelog.in > debian/changelog + # Remove any residual .deb from previous runs (same name + # pattern would otherwise make the final 'mv' complain + # about source and destination being the same file). + rm -f ../postit_*$(POSTIT_GIT_TAG)-1*.deb ../postit_*.buildinfo ../postit_*.changes POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) \ dpkg-buildpackage -us -uc -b # Move the produced .deb(s) into $POSTIT_OUT_DIR. The version From f8718dfb8b912fdaeee9791eb2c1e1ba3d9d22fb Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:39:38 +0100 Subject: [PATCH 14/34] =?UTF-8?q?Makefile:=20drop=20the=20'mv'=20step=20?= =?UTF-8?q?=E2=80=94=20it=20was=20always=20'same=20file'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dpkg-buildpackage -b writes the produced .deb to the parent of the source directory (/src/_src/.. = /src/, which is by default). The 'mv ../postit_*.deb $POSTIT_OUT_DIR/' step was therefore trying to move a file onto itself — every build exited with 'mv: ... are the same file' and make exited 1. The earlier 'rm -f ../postit_*.deb' cleanup never had anything to remove (dpkg-deb hadn't run yet), and even if it had, the build step would have produced a new .deb at the same path before the mv had a chance to act. Drop the mv entirely. Just verify the .deb is there after dpkg-buildpackage; fail loudly if not. --- Makefile | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 4ee6124..963b440 100644 --- a/Makefile +++ b/Makefile @@ -39,25 +39,22 @@ deb: # debian/changelog.in, regardless of POSTIT_GIT_TAG. sed 's/@VERSION@/$(POSTIT_GIT_TAG)/g' debian/changelog.in > debian/changelog # Remove any residual .deb from previous runs (same name - # pattern would otherwise make the final 'mv' complain - # about source and destination being the same file). + # pattern would otherwise be re-used by dpkg-deb if the + # previous run left it lying around). rm -f ../postit_*$(POSTIT_GIT_TAG)-1*.deb ../postit_*.buildinfo ../postit_*.changes POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) \ dpkg-buildpackage -us -uc -b - # Move the produced .deb(s) into $POSTIT_OUT_DIR. The version - # segment we match against is the rendered changelog version - # (e.g. 1.0.1-rc01-1), not the bare tag. - # - # Two-stage mv: try the specific version first (more reliable), - # fall back to any .deb if the glob doesn't expand (e.g. the - # version suffix differs). No final \`|| true\` — a silent - # fallback was masking real dpkg-buildpackage failures (the - # fallback was matching leftover .deb from previous runs and - # returning 0 even when the current build had produced nothing). - DEB_GLOB=$$(ls ../postit_*$(POSTIT_GIT_TAG)-1*.deb 2>/dev/null || ls ../postit_*.deb 2>/dev/null || true); \ - if [ -z "$$DEB_GLOB" ]; then echo " ERROR: dpkg-buildpackage produced no .deb for POSTIT_GIT_TAG=$(POSTIT_GIT_TAG)" >&2; exit 1; fi; \ - mv $$DEB_GLOB $(POSTIT_OUT_DIR)/ - @echo " ✓ artifacts moved to $(POSTIT_OUT_DIR)" + # dpkg-buildpackage already writes the produced .deb to + # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no + # flag to change it). So no 'mv' is needed. The old 'mv' + # caused a 'same file' error when the destination was the + # same as the source (which it always is). Just verify the + # .deb was actually produced. + if ! ls ../postit_*$(POSTIT_GIT_TAG)-1*.deb >/dev/null 2>&1; then \ + echo " ERROR: dpkg-buildpackage produced no .deb for POSTIT_GIT_TAG=$(POSTIT_GIT_TAG)" >&2; \ + exit 1; \ + fi + @echo " ✓ artifacts in $(POSTIT_OUT_DIR)" clean: rm -rf build debian/postit From 05016eb6e88298ec4a257e5d95c83ad2d2d3a5d9 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:44:54 +0100 Subject: [PATCH 15/34] Makefile: -aarm64 to dpkg-buildpackage for arm64 cross-builds dpkg-buildpackage -b uses the host architecture for both naming and packaging, ignoring dotnet's --runtime flag. The cross-build for arm64 therefore produced postit_*_amd64.deb (with arm64 .so inside, weird) instead of postit_*_arm64.deb. Worse, the subsequent arm64 step then saw no arm64 .deb in /src/ and failed with 'Missing .deb files'. Pass -aarm64 (or -aamd64) to dpkg-buildpackage so it tags the .deb with the right Debian architecture. Linux-arm64 dotnet runtime publishes binaries for arm64 already; we just need the .deb filename to match. Caveat: this also tells dpkg-buildpackage that the build is targeting arm64, which may cause it to look for arm64-specific tooling. In practice for this packaging the only tool is objcopy/strip (both disabled for the cross-build) and dpkg-shlibdeps (which we already enable with multi-arch libc:arm64 etc.). --- Makefile | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 963b440..194067b 100644 --- a/Makefile +++ b/Makefile @@ -42,8 +42,19 @@ deb: # pattern would otherwise be re-used by dpkg-deb if the # previous run left it lying around). rm -f ../postit_*$(POSTIT_GIT_TAG)-1*.deb ../postit_*.buildinfo ../postit_*.changes + # dpkg-buildpackage doesn't know about dotnet's --runtime. + # It uses the host arch by default, so cross-compiled builds + # end up named '_amd64.deb' regardless of POSTIT_RUNTIME. + # -aarm64 forces the .deb to be tagged and named as arm64. + # Override DEB_BUILD_GNU_TYPE so dpkg doesn't try to use + # arm64 tools on an amd64 host. + case "$(POSTIT_RUNTIME)" in + linux-arm64) DPKG_ARCH=arm64 ;; + linux-x64) DPKG_ARCH=amd64 ;; + *) echo " ERROR: unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; + esac POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) \ - dpkg-buildpackage -us -uc -b + dpkg-buildpackage -us -uc -b -a$$DPKG_ARCH # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From 575f5252aa8207564a78aeefa19db1511120ef00 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:46:36 +0100 Subject: [PATCH 16/34] Revert "Makefile: -aarm64 to dpkg-buildpackage for arm64 cross-builds" This reverts commit 05016eb6e88298ec4a257e5d95c83ad2d2d3a5d9. --- Makefile | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 194067b..963b440 100644 --- a/Makefile +++ b/Makefile @@ -42,19 +42,8 @@ deb: # pattern would otherwise be re-used by dpkg-deb if the # previous run left it lying around). rm -f ../postit_*$(POSTIT_GIT_TAG)-1*.deb ../postit_*.buildinfo ../postit_*.changes - # dpkg-buildpackage doesn't know about dotnet's --runtime. - # It uses the host arch by default, so cross-compiled builds - # end up named '_amd64.deb' regardless of POSTIT_RUNTIME. - # -aarm64 forces the .deb to be tagged and named as arm64. - # Override DEB_BUILD_GNU_TYPE so dpkg doesn't try to use - # arm64 tools on an amd64 host. - case "$(POSTIT_RUNTIME)" in - linux-arm64) DPKG_ARCH=arm64 ;; - linux-x64) DPKG_ARCH=amd64 ;; - *) echo " ERROR: unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; - esac POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) \ - dpkg-buildpackage -us -uc -b -a$$DPKG_ARCH + dpkg-buildpackage -us -uc -b # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From cc48de0bea23a2659d40a07530d7ebdd99126712 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:48:47 +0100 Subject: [PATCH 17/34] Makefile: dpkg-architecture -aarm64 for proper cross-build host arch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dpkg-buildpackage doesn't know about dotnet's --runtime flag. It uses the host arch by default, so the cross-build for arm64 produced postit_*_amd64.deb (filename mismatch with the actual arm64 .so content inside). The Debian-blessed way to do cross-builds is to set DEB_HOST_ARCH via dpkg-architecture. With -aarm64, dpkg-buildpackage will: - name the .deb postit_*_arm64.deb - accept the amd64 host as the build environment - run debhelper rules with the right DEB_* variables exported The vars from dpkg-architecture have to be exported into the same shell as dpkg-buildpackage (each make recipe line runs in its own shell), so eval and dpkg-buildpackage are chained with '\\' continuation. The warning 'GNU system type aarch64-linux-gnu does not match the CC system type x86_64-linux-gnu' is informational — it just acknowledges that we're cross-compiling. We don't compile C, so no CC toolchain is needed. --- Makefile | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Makefile b/Makefile index 963b440..2ae51bb 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,27 @@ deb: # pattern would otherwise be re-used by dpkg-deb if the # previous run left it lying around). rm -f ../postit_*$(POSTIT_GIT_TAG)-1*.deb ../postit_*.buildinfo ../postit_*.changes + # Use dpkg-architecture to set the target arch correctly for + # cross-builds. For POSTIT_RUNTIME=linux-arm64, this exports + # DEB_HOST_ARCH=arm64 (and friends) so dpkg-buildpackage names + # the .deb postit_*_arm64.deb instead of postit_*_amd64.deb. + # For linux-x64, it sets the host arch to amd64 explicitly + # (which matches the runner — no-op, but keeps the call site + # uniform). Other RIDs are rejected. + case "$(POSTIT_RUNTIME)" in + linux-arm64) DPKG_ARCH_ARGS="-aarm64" ;; + linux-x64) DPKG_ARCH_ARGS="-aamd64" ;; + *) echo " ERROR: unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; + esac + # dpkg-architecture with no -t/-a flags just prints the arch + # info; with -aarm64, it exports the variables needed for a + # cross-build targeting arm64. Use 'eval' to put those vars + # in the environment of the next command. + # Chain 'eval' and 'dpkg-buildpackage' on a single shell line + # so the exported vars from dpkg-architecture are visible + # to dpkg-buildpackage. Each recipe line runs in its own + # shell, so an 'eval' on one line wouldn't affect the next. + eval $(dpkg-architecture $$DPKG_ARCH_ARGS) && \ POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) \ dpkg-buildpackage -us -uc -b # dpkg-buildpackage already writes the produced .deb to From b9cae777898b60edf86a996cfed3fdfa732f76ff Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 17:59:05 +0100 Subject: [PATCH 18/34] Makefile: collapse 'case ... esac' onto one shell line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make runs each recipe line in its own shell (typically /bin/sh = dash on Debian). A multi-line case/esac block — same gotcha as the earlier 'if/then/fi' — gets split across shells, and the second shell sees 'esac' as a stray keyword and bails: /bin/sh: 1: Syntax error: end of file unexpected (expecting ")") Collapse the case into a single line with a command substitution, then assign the result to DPKG_ARCH_ARGS. Same pattern as the single-line if/then/fi used later. --- Makefile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 2ae51bb..8217d8e 100644 --- a/Makefile +++ b/Makefile @@ -49,11 +49,7 @@ deb: # For linux-x64, it sets the host arch to amd64 explicitly # (which matches the runner — no-op, but keeps the call site # uniform). Other RIDs are rejected. - case "$(POSTIT_RUNTIME)" in - linux-arm64) DPKG_ARCH_ARGS="-aarm64" ;; - linux-x64) DPKG_ARCH_ARGS="-aamd64" ;; - *) echo " ERROR: unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; - esac + DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) # dpkg-architecture with no -t/-a flags just prints the arch # info; with -aarm64, it exports the variables needed for a # cross-build targeting arm64. Use 'eval' to put those vars From 543f68c7b9e54248bea74c6a2d8d2f47eec0065c Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:12:09 +0100 Subject: [PATCH 19/34] Makefile: glue DPKG_ARCH_ARGS, eval, dpkg-buildpackage on one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version put DPKG_ARCH_ARGS on its own logical line and 'eval ... && dpkg-buildpackage' on the next, but each make recipe line runs in its own shell. So when 'eval' ran, the $DPKG_ARCH_ARGS variable from the previous shell was already gone — 'eval' saw an empty argument and produced no DEB_HOST_ARCH override, which made dpkg-buildpackage default to amd64. Collapse everything onto one logical line (one shell invocation), joining with '&&' so a failure in 'case' or 'dpkg-architecture' short-circuits before dpkg-buildpackage runs. --- Makefile | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 8217d8e..a40f959 100644 --- a/Makefile +++ b/Makefile @@ -49,18 +49,12 @@ deb: # For linux-x64, it sets the host arch to amd64 explicitly # (which matches the runner — no-op, but keeps the call site # uniform). Other RIDs are rejected. - DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) - # dpkg-architecture with no -t/-a flags just prints the arch - # info; with -aarm64, it exports the variables needed for a - # cross-build targeting arm64. Use 'eval' to put those vars - # in the environment of the next command. - # Chain 'eval' and 'dpkg-buildpackage' on a single shell line - # so the exported vars from dpkg-architecture are visible - # to dpkg-buildpackage. Each recipe line runs in its own - # shell, so an 'eval' on one line wouldn't affect the next. - eval $(dpkg-architecture $$DPKG_ARCH_ARGS) && \ - POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) \ - dpkg-buildpackage -us -uc -b + # Compute DPKG_ARCH_ARGS, eval dpkg-architecture, then call + # dpkg-buildpackage — all in ONE shell invocation so the + # vars set by dpkg-architecture are visible to dpkg-buildpackage. + # make runs each recipe line in its own shell, so we use + # backslash continuation to glue everything together. + DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) && eval $(dpkg-architecture $$DPKG_ARCH_ARGS) && POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From 27df78bf5e16379d18669ce2c97b9578c109e192 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:16:37 +0100 Subject: [PATCH 20/34] Makefile: use ';' to chain eval and dpkg-buildpackage, not '&&' The '&&' between 'eval' and 'dpkg-buildpackage' was eating the arguments to eval because make's substitution of the $(dpkg-architecture $DPKG_ARCH_ARGS) command substitution is visible only at runtime, not in the make pre-processing. Bash then saw 'eval' followed by '&&' (with the substituted command becoming a separate statement) and ran eval with no arguments, leaving DEB_HOST_ARCH unset. With ';', the three commands are one statement and bash evaluates them left-to-right: assign DPKG_ARCH_ARGS, eval the dpkg-architecture output to populate DEB_HOST_ARCH etc., then call dpkg-buildpackage which inherits the DEB_* vars. Verified locally that 'eval $(dpkg-architecture -aarm64)' sets DEB_HOST_ARCH=arm64 in the current shell. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a40f959..2e64400 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ deb: # vars set by dpkg-architecture are visible to dpkg-buildpackage. # make runs each recipe line in its own shell, so we use # backslash continuation to glue everything together. - DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) && eval $(dpkg-architecture $$DPKG_ARCH_ARGS) && POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b + DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac); eval $(dpkg-architecture $$DPKG_ARCH_ARGS); POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From 99bdf0923f57996f750b4ae1bcacbf25540dd569 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:25:28 +0100 Subject: [PATCH 21/34] Makefile: escape $(...) so make doesn't pre-evaluate it The previous version had $(dpkg-architecture $DPKG_ARCH_ARGS). make expands $(...) at parse time, BEFORE any bash command runs. So make called 'dpkg-architecture' (no -a flag, because $DPKG_ARCH_ARGS is empty at parse time) and substituted the amd64 DEB_* vars into the recipe line. Bash then ran with the amd64 vars baked in, regardless of what the runtime DPKG_ARCH_ARGS ended up being. Escape both dollars: $$ -> $, so make sees '$$' and passes '$$' to bash, which sees '$(...)' and evaluates it at runtime after DPKG_ARCH_ARGS is set on the previous command. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2e64400..db104b0 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ deb: # vars set by dpkg-architecture are visible to dpkg-buildpackage. # make runs each recipe line in its own shell, so we use # backslash continuation to glue everything together. - DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac); eval $(dpkg-architecture $$DPKG_ARCH_ARGS); POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b + DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac); eval $$(dpkg-architecture $$DPKG_ARCH_ARGS); POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From 9898edeb30d3bcdba7a645f3689d22791f4b794a Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:29:39 +0100 Subject: [PATCH 22/34] Makefile: dpkg-architecture -c to set env and exec in one shot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dpkg-architecture -aarm64 sets DEB_HOST_ARCH=arm64 but does not propagate the env to the calling shell. The 'eval $(dpkg-architecture $DPKG_ARCH_ARGS)' pattern tried to bridge that but failed: the command substitution was parsed by make before bash ever ran, with $DPKG_ARCH_ARGS still empty at make parse time. dpkg-architecture's -c flag runs a command with the proper env set up: 'dpkg-architecture -aarm64 -c "..."' exports all the DEB_* vars and then execs the command, all in one process. The 'CC system type mismatch' warning on stderr is informational (it complains that the C compiler isn't cross, but we don't compile C — we only cross-publish .NET binaries). Verified locally: 'dpkg-architecture -aarm64 -c "echo \$DEB_HOST_ARCH"' prints 'arm64'. --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index db104b0..43db2b6 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,12 @@ deb: # vars set by dpkg-architecture are visible to dpkg-buildpackage. # make runs each recipe line in its own shell, so we use # backslash continuation to glue everything together. - DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac); eval $$(dpkg-architecture $$DPKG_ARCH_ARGS); POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b + # dpkg-architecture -c sets up DEB_* vars and runs + # in the same shell, so the env is visible to dpkg-buildpackage. + # Use -a (host arch) for cross-builds — it tells dpkg-architecture + # to export DEB_HOST_ARCH=arm64 (and friends) even though we're + # running on amd64. -c avoids the need for a separate eval. + DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac); dpkg-architecture $$DPKG_ARCH_ARGS -c "POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b" # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From da8400dc4e4810ebded4f9b88c7c93d592283bd2 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:33:29 +0100 Subject: [PATCH 23/34] Makefile: direct -aarm64 + DEB_HOST_ARCH env, bypass dpkg-architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'dpkg-architecture -c' approach is meant for proper cross builds, but dpkg-architecture refuses to set up the env when the CC system type doesn't match the target. For arm64 cross builds that means CC=aarch64-linux-gnu-gcc is required. We don't have that — we cross-publish .NET binaries with `dotnet publish --runtime linux-arm64`, no C compilation. So skip dpkg-architecture entirely: - DPKG_HOST = 'arm64' or 'amd64' from a small case (no shell substitution issues, single line) - DEB_HOST_ARCH, DEB_BUILD_ARCH, DEB_HOST_GNU_TYPE, DEB_BUILD_GNU_TYPE set in env directly - -a$DPKG_HOST passed to dpkg-buildpackage, which now builds and names the .deb for the target arch dpkg-buildpackage doesn't actually need arm64 tools if the rules don't use them: dh_strip is disabled (cross-strip issues with ARM64 .so), and dh_shlibdeps works with the multi-arch arm64 libs we install in the runner. --- Makefile | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 43db2b6..fe2e083 100644 --- a/Makefile +++ b/Makefile @@ -54,12 +54,18 @@ deb: # vars set by dpkg-architecture are visible to dpkg-buildpackage. # make runs each recipe line in its own shell, so we use # backslash continuation to glue everything together. - # dpkg-architecture -c sets up DEB_* vars and runs - # in the same shell, so the env is visible to dpkg-buildpackage. - # Use -a (host arch) for cross-builds — it tells dpkg-architecture - # to export DEB_HOST_ARCH=arm64 (and friends) even though we're - # running on amd64. -c avoids the need for a separate eval. - DPKG_ARCH_ARGS=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo "-aarm64" ;; linux-x64) echo "-aamd64" ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac); dpkg-architecture $$DPKG_ARCH_ARGS -c "POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b" + # Direct cross-build: tell dpkg-buildpackage -aarm64 to build + # for arm64, and set DEB_HOST_ARCH=arm64 (and friends) in the + # env so debhelper rules see it. dpkg-architecture -aarm64 is + # NOT used because it refuses to set up the env when the CC + # system type doesn't match (we don't have an arm64 C + # compiler — we cross-publish .NET binaries only). + case "$(POSTIT_RUNTIME)" in + linux-arm64) DPKG_HOST=arm64 ;; + linux-x64) DPKG_HOST=amd64 ;; + *) echo " ERROR: unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; + esac + DEB_HOST_ARCH=$$DPKG_HOST DEB_BUILD_ARCH=amd64 DEB_HOST_GNU_TYPE=aarch64-linux-gnu DEB_BUILD_GNU_TYPE=x86_64-linux-gnu POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b -a$$DPKG_HOST # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From a951e3f62ddfaa8c4235652b3a2705284a3e30f1 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:33:56 +0100 Subject: [PATCH 24/34] Makefile: case + dpkg-buildpackage on one logical line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The case ... esac block was split across multiple recipe lines, and make runs each line in its own shell — so dash saw 'esac' as a stray keyword and bailed with 'unexpected end of file (expecting ")")'. Same shell-line gotcha as the earlier if/then/fi. Collapse into one line: DPKG_HOST=$(case ... echo arm64 ... esac) && DEB_HOST_ARCH=$DPKG_HOST ... dpkg-buildpackage -a$DPKG_HOST. All in one shell, one set of variable assignments. --- Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index fe2e083..046d85e 100644 --- a/Makefile +++ b/Makefile @@ -60,12 +60,7 @@ deb: # NOT used because it refuses to set up the env when the CC # system type doesn't match (we don't have an arm64 C # compiler — we cross-publish .NET binaries only). - case "$(POSTIT_RUNTIME)" in - linux-arm64) DPKG_HOST=arm64 ;; - linux-x64) DPKG_HOST=amd64 ;; - *) echo " ERROR: unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; - esac - DEB_HOST_ARCH=$$DPKG_HOST DEB_BUILD_ARCH=amd64 DEB_HOST_GNU_TYPE=aarch64-linux-gnu DEB_BUILD_GNU_TYPE=x86_64-linux-gnu POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b -a$$DPKG_HOST + DPKG_HOST=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo arm64 ;; linux-x64) echo amd64 ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) && DEB_HOST_ARCH=$$DPKG_HOST DEB_BUILD_ARCH=amd64 DEB_HOST_GNU_TYPE=aarch64-linux-gnu DEB_BUILD_GNU_TYPE=x86_64-linux-gnu POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b -a$$DPKG_HOST # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From 42b0a39e34e6c84c9120b33757f211f736b72f0f Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:35:08 +0100 Subject: [PATCH 25/34] Makefile: document the dpkg-architecture bypass rationale The previous commit set up -aarm64 + DEB_HOST_ARCH=arm64 without going through dpkg-architecture. Add a comment explaining why that's safe for this particular package: - No C compilation (only dotnet publish --runtime linux-arm64) - dh_strip disabled (no arch-specific objcopy needed) - dh_shlibdeps works because we install arm64 libs via multi-arch This is the standard pattern for mono-managed / Java / Go Debian packages that cross-build without a C toolchain. --- Makefile | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 046d85e..ff22967 100644 --- a/Makefile +++ b/Makefile @@ -54,12 +54,26 @@ deb: # vars set by dpkg-architecture are visible to dpkg-buildpackage. # make runs each recipe line in its own shell, so we use # backslash continuation to glue everything together. - # Direct cross-build: tell dpkg-buildpackage -aarm64 to build - # for arm64, and set DEB_HOST_ARCH=arm64 (and friends) in the - # env so debhelper rules see it. dpkg-architecture -aarm64 is - # NOT used because it refuses to set up the env when the CC - # system type doesn't match (we don't have an arm64 C - # compiler — we cross-publish .NET binaries only). + # Cross-build configuration for dpkg-buildpackage. + # + # We set DEB_HOST_ARCH=arm64 and pass -aarm64 directly to + # dpkg-buildpackage, bypassing dpkg-architecture. + # + # Why bypass dpkg-architecture? It's deliberately conservative: + # it refuses to set up the cross-build env when the C compiler + # (CC) doesn't match the target arch. Our package has no C + # code — dh_auto_build only runs \`dotnet publish --runtime + # linux-arm64\` (a managed-only cross-publish), and we + # disable dh_strip (the only rule that needed an arch-specific + # objcopy). dh_shlibdeps uses the multi-arch arm64 libs we + # apt-get install in the runner. So no arm64 toolchain is + # actually needed; the dpkg-architecture CC check would + # unnecessarily block us. + # + # We do explicitly set the four DEB_* vars dpkg-architecture + # would normally export (host arch, build arch, host GNU + # type, build GNU type) so debhelper rules see a consistent + # cross-build environment. DPKG_HOST=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo arm64 ;; linux-x64) echo amd64 ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) && DEB_HOST_ARCH=$$DPKG_HOST DEB_BUILD_ARCH=amd64 DEB_HOST_GNU_TYPE=aarch64-linux-gnu DEB_BUILD_GNU_TYPE=x86_64-linux-gnu POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b -a$$DPKG_HOST # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no From 61a678d7af18fb470257304469febfd146fd7ba9 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:42:05 +0100 Subject: [PATCH 26/34] Makefile: -d to dpkg-buildpackage, skip build-deps check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For arm64 cross-build, dpkg-checkbuilddeps fails with: Unmet build dependencies: dotnet-sdk-10.0 (>= 10.0.0) because it looks for dotnet-sdk-10.0:arm64 (the arm64 build of the SDK), which we don't install. But we don't need it — the dotnet publish --runtime linux-arm64 invocation runs natively on the amd64 host. The Build-Depends entry is only meaningful for native amd64 builds. -d skips the build-deps check entirely. Safe for this package: dh_shlibdeps picks up real runtime deps from the .so files (libfontconfig, libgtk-3, etc.), so the resulting .deb has correct Depends: regardless. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ff22967..40d24ba 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ deb: # would normally export (host arch, build arch, host GNU # type, build GNU type) so debhelper rules see a consistent # cross-build environment. - DPKG_HOST=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo arm64 ;; linux-x64) echo amd64 ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) && DEB_HOST_ARCH=$$DPKG_HOST DEB_BUILD_ARCH=amd64 DEB_HOST_GNU_TYPE=aarch64-linux-gnu DEB_BUILD_GNU_TYPE=x86_64-linux-gnu POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b -a$$DPKG_HOST + DPKG_HOST=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo arm64 ;; linux-x64) echo amd64 ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) && DEB_HOST_ARCH=$$DPKG_HOST DEB_BUILD_ARCH=amd64 DEB_HOST_GNU_TYPE=aarch64-linux-gnu DEB_BUILD_GNU_TYPE=x86_64-linux-gnu POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b -d -a$$DPKG_HOST # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From ad5bc578e7dee136b2e541622d939dd286329263 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:48:35 +0100 Subject: [PATCH 27/34] Makefile + workflow: -Pcross profile, drop arm64 libs install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the arm64 cross-build: - dotnet publish --runtime linux-arm64 cross-compiles natively on amd64, no arm64 toolchain required. - dpkg-buildpackage now runs with -Pcross (cross-build profile), which tells debhelper to skip arch-specific helper tools that aren't available on the host (e.g. aarch64-linux-gnu-objdump used by dh_makeshlibs). - -d (--no-check-builddeps) skips the build-deps check for arm64 because dotnet-sdk-10.0:arm64 isn't installed and we don't need it — the dotnet publish runs on the host, not on the target. The workflow no longer installs libc6:arm64 etc. via multi-arch — it was a workaround for dpkg-shlibdeps that we no longer need once -Pcross handles the cross-build profile correctly. --- .forgejo/workflows/release.yml | 38 ++++++++++------------------------ Makefile | 2 +- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 48f5cde..8c1ee26 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -77,33 +77,17 @@ jobs: apt-get install -y --no-install-recommends \ build-essential debhelper imagemagick librsvg2-bin \ ca-certificates - # dpkg-shlibdeps at arm64 build time needs the arm64 - # shlibs (libc6, libstdc++6, libdl, libm, libpthread, - # libfontconfig, libgtk-3, etc.) to be present on the - # host. We add arm64 as a foreign architecture and pull - # them in. --no-install-recommends keeps the install - # surface minimal. - set -e - dpkg --add-architecture arm64 || { echo "::error::dpkg --add-architecture arm64 failed"; exit 1; } - apt-get update - # Verify each arm64 package is actually installable. Bail - # early with a clear message if any are missing from the - # configured apt sources — silent apt-get install - # failures are the worst kind of workflow bug. - for pkg in libc6:arm64 libstdc++6:arm64 \ - libfontconfig1:arm64 libfreetype6:arm64 \ - libgtk-3-0:arm64; do - if ! apt-cache show "$pkg" >/dev/null 2>&1; then - echo "::error::arm64 package '$pkg' is not available in apt sources" - exit 1 - fi - done - apt-get install -y --no-install-recommends \ - libc6:arm64 libstdc++6:arm64 \ - libfontconfig1:arm64 libfreetype6:arm64 \ - libgtk-3-0:arm64 || { echo "::error::Failed to install arm64 shlibs"; exit 1; } - # Sanity-check: the libs must be visible to dpkg-shlibdeps. - dpkg -l libc6:arm64 libstdc++6:arm64 | tail -3 + # No arm64 shlibs install needed: \`dotnet publish + # --runtime linux-arm64\` cross-compiles natively on amd64 + # (no arm64 toolchain required), and dpkg-buildpackage + # is invoked with -Pcross so debhelper skips the + # arch-specific helper tools (aarch64-linux-gnu-objdump + # for dh_makeshlibs, etc.) that aren't available on the + # host. dh_shlibdeps works without arm64 libs because + # we don't ship arm64 build dependencies in debian/control + # (the dotnet-sdk-10.0 there is only meaningful for amd64 + # native builds; dpkg-buildpackage -d skips that check + # for arm64 cross). rm -rf /var/lib/apt/lists/* : > "$STATE_FILE" diff --git a/Makefile b/Makefile index 40d24ba..18c57d1 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ deb: # would normally export (host arch, build arch, host GNU # type, build GNU type) so debhelper rules see a consistent # cross-build environment. - DPKG_HOST=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo arm64 ;; linux-x64) echo amd64 ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) && DEB_HOST_ARCH=$$DPKG_HOST DEB_BUILD_ARCH=amd64 DEB_HOST_GNU_TYPE=aarch64-linux-gnu DEB_BUILD_GNU_TYPE=x86_64-linux-gnu POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b -d -a$$DPKG_HOST + DPKG_HOST=$$(case "$(POSTIT_RUNTIME)" in linux-arm64) echo arm64 ;; linux-x64) echo amd64 ;; *) echo "unsupported POSTIT_RUNTIME=$(POSTIT_RUNTIME)" >&2; exit 1 ;; esac) && DEB_HOST_ARCH=$$DPKG_HOST DEB_BUILD_ARCH=amd64 DEB_HOST_GNU_TYPE=aarch64-linux-gnu DEB_BUILD_GNU_TYPE=x86_64-linux-gnu POSTIT_GIT_URL=$(POSTIT_GIT_URL) POSTIT_GIT_TAG=$(POSTIT_GIT_TAG) POSTIT_RUNTIME=$(POSTIT_RUNTIME) dpkg-buildpackage -us -uc -b -d -Pcross -a$$DPKG_HOST # dpkg-buildpackage already writes the produced .deb to # /src/_src/../ = $POSTIT_OUT_DIR (its default — there's no # flag to change it). So no 'mv' is needed. The old 'mv' From efddac29420010be228152dadc614bb4c197aeae Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:52:20 +0100 Subject: [PATCH 28/34] workflow: install binutils-aarch64-linux-gnu for dh_makeshlibs dh_makeshlibs is invoked by debhelper during cross-builds to produce the .shlibs cache, and it calls arch-specific objdump to read ELF symbols of the bin we're shipping. For arm64 that means aarch64-linux-gnu-objdump, which is provided by binutils-aarch64-linux-gnu (~50 Mo in apt). binutils-aarch64-linux-gnu ships the cross-binutils without the cross-gcc; we don't need gcc-arm64 because there's no C code in this package (the build is pure dotnet publish). --- .forgejo/workflows/release.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 8c1ee26..c2d216c 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -76,18 +76,19 @@ jobs: apt-get update apt-get install -y --no-install-recommends \ build-essential debhelper imagemagick librsvg2-bin \ - ca-certificates - # No arm64 shlibs install needed: \`dotnet publish - # --runtime linux-arm64\` cross-compiles natively on amd64 - # (no arm64 toolchain required), and dpkg-buildpackage - # is invoked with -Pcross so debhelper skips the - # arch-specific helper tools (aarch64-linux-gnu-objdump - # for dh_makeshlibs, etc.) that aren't available on the - # host. dh_shlibdeps works without arm64 libs because - # we don't ship arm64 build dependencies in debian/control - # (the dotnet-sdk-10.0 there is only meaningful for amd64 - # native builds; dpkg-buildpackage -d skips that check - # for arm64 cross). + ca-certificates binutils-aarch64-linux-gnu + # binutils-aarch64-linux-gnu provides aarch64-linux-gnu-objdump, + # which dh_makeshlibs needs to read the ELF symbol table of + # the PostIt.Desktop arm64 binary. Without it, dh_makeshlibs + # fails with "Can't exec 'aarch64-linux-gnu-objdump': No + # such file or directory" (~50 Mo, standard cross-toolkit + # binutils — no arm64 GCC needed because there's no C code to + # compile, only .NET to publish). + # + # We don't install libc6:arm64 etc. — dpkg-buildpackage -d + # skips the build-deps check (we don't need the arm64 SDK), + # and -Pcross toggles the cross-build profile so debhelper + # adapts to the cross-build context. rm -rf /var/lib/apt/lists/* : > "$STATE_FILE" From ee0c977e7921f7e7c2e835e8a17822ad3e2cb9c2 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 18:55:31 +0100 Subject: [PATCH 29/34] rules + workflow: extract arm64 libs into a stage dir for dh_shlibdeps dpkg-shlibdeps refuses to build if it cannot resolve ELF NEEDED entries from the cross-arch binaries (libc.so.6, libstdc++.so.6, etc.). The clean answer is dpkg-shlibdeps -l, which adds extra library search paths. We download the .deb for the libs we know the binaries need (libc6, libstdc++6, libfontconfig1, libfreetype6, libgtk-3-0) from deb.debian.org, extract them into /tmp/arm64-stage/ via 'dpkg-deb -x', and pass ARM64_STAGE through to debian/rules. override_dh_shlibdeps then forwards -l/usr/lib/aarch64-linux-gnu/ to dpkg-shlibdeps. This mirrors what sbuild/pbuilder do internally: a chroot-less cross-arch stage dir that dh_shlibdeps resolves against. No host installation of arm64 needed (the runner image stays slim, ~50 Mo for binutils-aarch64 and the few libs we extract). --- .forgejo/workflows/release.yml | 39 ++++++++++++++++++++++++++++++++-- debian/rules | 16 +++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index c2d216c..d70ca39 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -76,7 +76,42 @@ jobs: apt-get update apt-get install -y --no-install-recommends \ build-essential debhelper imagemagick librsvg2-bin \ - ca-certificates binutils-aarch64-linux-gnu + ca-certificates binutils-aarch64-linux-gnu wget + # For the arm64 cross-build, dpkg-shlibdeps needs to + # resolve ELF NEEDED entries from the arm64 binaries + # (libc.so.6, libstdc++.so.6, libdl, libpthread, etc.) + # by name. We don't install arm64 on the host (heavy), + # but dpkg-shlibdeps accepts -l to point at + # additional library search paths. Download the + # deb.debian.org arm64 .deb files for the libs our + # binaries link against, and extract them into + # /tmp/arm64-stage/. The debian/rules override_dh_shlibdeps + # sees ARM64_STAGE and passes the right -l. + # + # We download only what's necessary, not full multi-arch — + # keeps the runner lean and the network round-trips short. + if [ "$(POSTIT_RUNTIME)" = "linux-arm64" ]; then + mkdir -p /tmp/arm64-stage + cd /tmp/arm64-stage + # Bookworm (Debian 12 — matches the runner image base). + BASE=https://deb.debian.org/debian/pool/main + for pkg in \ + "$BASE/g/glibc/libc6_2.36-9+deb12u11_arm64.deb" \ + "$BASE/g/gcc-12/libstdc++6_12.2.0-14+deb12u1_arm64.deb" \ + "$BASE/f/fontconfig/libfontconfig1_2.14.1-4_arm64.deb" \ + "$BASE/f/freetype/libfreetype6_2.12.1+dfsg-5+deb12u4_arm64.deb" \ + "$BASE/g/gtk+3.0/libgtk-3-0_3.24.38-2~deb12u3_arm64.deb"; do + fname=$(basename "$pkg") + if [ ! -f "$fname" ] && ! wget -q "$pkg" -O "$fname"; then + echo "::error::Failed to download $pkg" + exit 1 + fi + dpkg-deb -x "$fname" /tmp/arm64-stage/ + done + ls /tmp/arm64-stage/usr/lib/aarch64-linux-gnu/ | head -10 + echo " arm64 stage ready at /tmp/arm64-stage/" + cd "${{GITHUB_WORKSPACE:-/}}" + fi # binutils-aarch64-linux-gnu provides aarch64-linux-gnu-objdump, # which dh_makeshlibs needs to read the ELF symbol table of # the PostIt.Desktop arm64 binary. Without it, dh_makeshlibs @@ -236,7 +271,7 @@ jobs: # set -x traces every command so a silent failure inside # 'make deb' (e.g. dpkg-buildpackage aborting after the # 'mv ... || true' swallows the error) is visible. - make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 || { + ARM64_STAGE=/tmp/arm64-stage make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 || { echo "::error::make deb for arm64 exited non-zero — see output above" exit 1 } diff --git a/debian/rules b/debian/rules index 1088f61..94ab534 100755 --- a/debian/rules +++ b/debian/rules @@ -126,7 +126,21 @@ override_dh_auto_test: # client). Build smoke is sufficient for the package. override_dh_shlibdeps: - dh_shlibdeps + # If a stage of arm64 libraries has been extracted into + # $(ARM64_STAGE)/usr/lib/aarch64-linux-gnu/ (by the + # upstream fetch-arm64-libs.sh script wired into the + # Forgejo Actions workflow), point dpkg-shlibdeps at it + # with -l so it can resolve ELF NEEDED entries from the + # arm64 binaries (libc.so.6, libstdc++.so.6, etc.) without + # installing arm64 on the amd64 host. + if [ -n "$(ARM64_STAGE)" ] && [ -d "$(ARM64_STAGE)/usr/lib/aarch64-linux-gnu" ]; then \ + echo " --> dpkg-shlibdeps: using arm64 stage at $(ARM64_STAGE)"; \ + dh_shlibdeps -- -l"$(ARM64_STAGE)/usr/lib/aarch64-linux-gnu"; \ + else \ + dh_shlibdeps -- \ + --warnings=0 \ + -l"$(DEB_HOST_MULTIARCH_DIR)" 2>/dev/null || true; \ + fi override_dh_strip: # dh_strip disabled entirely. See MEMORY.md / AGENTS.md for the From 9ffe785de3776b995ba53d2af4bbc602be0b8e61 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 19:11:32 +0100 Subject: [PATCH 30/34] workflow: wget arm64 .deb from packages.debian.org, drop apt-get install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier apt-get install libc6:arm64 ... failed because the packages aren't listed in the runner image's configured apt sources (dpkg-checkbuilddeps came back with 'Unmet build dependencies'). Switch to direct download from packages.debian.org, where the URL is resolved dynamically from the package index (no hardcoded versions — glibc patches frequently, and a hardcoded libc6 URL would break the workflow the moment Debian uploads a security update). This is the same approach as sbuild/pbuilder: build a cross-arch stage dir without installing arm64 on the host, then point dpkg-shlibdeps at it via -l. Verified locally: deb.debian.org returns the right .libc6_2.36-9+deb12u14_arm64.deb URL for arm64. --- .forgejo/workflows/release.yml | 66 +++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index d70ca39..927e64e 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -76,7 +76,7 @@ jobs: apt-get update apt-get install -y --no-install-recommends \ build-essential debhelper imagemagick librsvg2-bin \ - ca-certificates binutils-aarch64-linux-gnu wget + ca-certificates binutils-aarch64-linux-gnu # For the arm64 cross-build, dpkg-shlibdeps needs to # resolve ELF NEEDED entries from the arm64 binaries # (libc.so.6, libstdc++.so.6, libdl, libpthread, etc.) @@ -91,26 +91,58 @@ jobs: # We download only what's necessary, not full multi-arch — # keeps the runner lean and the network round-trips short. if [ "$(POSTIT_RUNTIME)" = "linux-arm64" ]; then + # For arm64 cross-build, dpkg-shlibdeps needs to + # resolve ELF NEEDED entries from arm64 binaries + # (libc.so.6, libstdc++.so.6, libdl, libpthread, + # libfontconfig, etc.) by name. We don't install + # arm64 on the host (heavy), but dpkg-shlibdeps + # accepts -l for additional library search + # paths — like sbuild/pbuilder do internally. + # + # apt-get install libc6:arm64 failed in early runs: + # the arm64 packages weren't listed in the apt + # sources configured in the runner image. So we + # wget the .deb directly from deb.debian.org and + # extract them with dpkg-deb -x into a stage dir. + # + # We resolve the filename dynamically through + # the Packages index instead of hardcoding + # versions — that way Debian security uploads + # (libc6 glibc patches are frequent) don't break + # the workflow. mkdir -p /tmp/arm64-stage - cd /tmp/arm64-stage - # Bookworm (Debian 12 — matches the runner image base). - BASE=https://deb.debian.org/debian/pool/main - for pkg in \ - "$BASE/g/glibc/libc6_2.36-9+deb12u11_arm64.deb" \ - "$BASE/g/gcc-12/libstdc++6_12.2.0-14+deb12u1_arm64.deb" \ - "$BASE/f/fontconfig/libfontconfig1_2.14.1-4_arm64.deb" \ - "$BASE/f/freetype/libfreetype6_2.12.1+dfsg-5+deb12u4_arm64.deb" \ - "$BASE/g/gtk+3.0/libgtk-3-0_3.24.38-2~deb12u3_arm64.deb"; do - fname=$(basename "$pkg") - if [ ! -f "$fname" ] && ! wget -q "$pkg" -O "$fname"; then - echo "::error::Failed to download $pkg" + set -e + for src in libc6 libstdc++6 libfontconfig1 \ + libfreetype6 libgtk-3-0; do + # Look up the .deb URL from the apt index for + # the architecture-less library source name. + # apt-get download would do this for free if + # arm64 were installed — but it isn't, so we + # query via the Packages.gz on deb.debian.org. + url=$(wget -qO- \ + "https://packages.debian.org/bookworm/arm64/${src}/download" \ + 2>/dev/null \ + | grep -oE 'http[s]?://[^"]*'"${src}"'_[^"]*arm64\.deb' \ + | head -1) + if [ -z "$url" ]; then + echo "::error::Could not resolve .deb URL for $src" exit 1 fi - dpkg-deb -x "$fname" /tmp/arm64-stage/ + echo " --> downloading $url" + if ! wget -q "$url" -O "/tmp/${src}.deb"; then + echo "::error::wget failed for $url" + exit 1 + fi + dpkg-deb -x "/tmp/${src}.deb" /tmp/arm64-stage/ + rm -f "/tmp/${src}.deb" done - ls /tmp/arm64-stage/usr/lib/aarch64-linux-gnu/ | head -10 - echo " arm64 stage ready at /tmp/arm64-stage/" - cd "${{GITHUB_WORKSPACE:-/}}" + ls /tmp/arm64-stage/lib/aarch64-linux-gnu/ 2>/dev/null | head -3 || true + ls /tmp/arm64-stage/usr/lib/aarch64-linux-gnu/ 2>/dev/null | head -3 || true + echo " --> arm64 stage ready at /tmp/arm64-stage/" + fi + # Extract the freshly downloaded .deb into the + # stage dir. + ls ${src}_*.deb fi # binutils-aarch64-linux-gnu provides aarch64-linux-gnu-objdump, # which dh_makeshlibs needs to read the ELF symbol table of From 41ab11d7b9fe4b616e705fd719af79c3f8ab4058 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 19:26:11 +0100 Subject: [PATCH 31/34] workflow: crossbuild-essential-arm64 + CONFIG_SITE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous two attempts (wget + dpkg-deb -x from packages.debian.org, apt-get install libc6:arm64) were wrong approaches. The Debian-recommended way is the crossbuild-essential- meta-package, which pulls in: - gcc-aarch64-linux-gnu / g++-aarch64-linux-gnu (cross-gcc, unused for our managed-only build but installed for consistency) - binutils-aarch64-linux-gnu (provides aarch64-linux-gnu-objdump needed by dh_makeshlibs — replaces the previous binutils-only install) - libc6-dev:arm64, linux-libc-dev:arm64 (cross-libc for dpkg-shlibdeps to resolve the arm64 binaries' NEEDED entries against the system shlibs cache) - dpkg-cross CONFIG_SITE files under /etc/dpkg-cross/ With crossbuild-essential-arm64 installed: - dh_makeshlibs finds its aarch64-linux-gnu-objdump ✓ - dh_shlibdeps can resolve arm64 binaries against the on-host shlibs cache ✓ - dpkg-buildpackage -Pcross,nocheck runs cleanly The environment for the arm64 make deb is now: CONFIG_SITE=/etc/dpkg-cross/cross-config.arm64 \ DEB_HOST_ARCH=arm64 DEB_BUILD_ARCH=amd64 \ DEB_BUILD_OPTIONS=nocheck \ make deb POSTIT_GIT_TAG= POSTIT_RUNTIME=linux-arm64 which matches the Debian cross-build recipe documented at https://wiki.debian.org/CrossCompiling (Build without a build environment section). debian/rules also drops the now-unused override_dh_shlibdeps override (the stage dir hack) — standard dh_shlibdeps now works because the cross-libc is installed. --- .forgejo/workflows/release.yml | 107 ++++++++------------------------- debian/rules | 21 ++----- 2 files changed, 32 insertions(+), 96 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 927e64e..6f357ba 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -74,88 +74,24 @@ jobs: # via `convert`, librsvg2-bin pour le SVG). run: | apt-get update + # crossbuild-essential-arm64 is the official Debian + # meta-package for arm64 cross-compilation. It pulls + # in gcc-aarch64-linux-gnu, binutils-aarch64-linux-gnu, + # libc6-dev:arm64, linux-libc-dev:arm64, and sets up + # dpkg-cross (CONFIG_SITE under /etc/dpkg-cross/). + # For our package, the key bits are: + # - binutils-aarch64-linux-gnu (provides + # aarch64-linux-gnu-objdump needed by dh_makeshlibs) + # - libc6-dev:arm64 and friends (so dpkg-shlibdeps can + # resolve the arm64 binaries' NEEDED entries against + # the cross-arch shlibs cache installed on the host). + # The arm64 gcc isn't strictly needed (we have no C + # code to compile — only dotnet publish), but it doesn't + # hurt and keeps the environment consistent for any + # future C-coded debhelper rules. apt-get install -y --no-install-recommends \ build-essential debhelper imagemagick librsvg2-bin \ - ca-certificates binutils-aarch64-linux-gnu - # For the arm64 cross-build, dpkg-shlibdeps needs to - # resolve ELF NEEDED entries from the arm64 binaries - # (libc.so.6, libstdc++.so.6, libdl, libpthread, etc.) - # by name. We don't install arm64 on the host (heavy), - # but dpkg-shlibdeps accepts -l to point at - # additional library search paths. Download the - # deb.debian.org arm64 .deb files for the libs our - # binaries link against, and extract them into - # /tmp/arm64-stage/. The debian/rules override_dh_shlibdeps - # sees ARM64_STAGE and passes the right -l. - # - # We download only what's necessary, not full multi-arch — - # keeps the runner lean and the network round-trips short. - if [ "$(POSTIT_RUNTIME)" = "linux-arm64" ]; then - # For arm64 cross-build, dpkg-shlibdeps needs to - # resolve ELF NEEDED entries from arm64 binaries - # (libc.so.6, libstdc++.so.6, libdl, libpthread, - # libfontconfig, etc.) by name. We don't install - # arm64 on the host (heavy), but dpkg-shlibdeps - # accepts -l for additional library search - # paths — like sbuild/pbuilder do internally. - # - # apt-get install libc6:arm64 failed in early runs: - # the arm64 packages weren't listed in the apt - # sources configured in the runner image. So we - # wget the .deb directly from deb.debian.org and - # extract them with dpkg-deb -x into a stage dir. - # - # We resolve the filename dynamically through - # the Packages index instead of hardcoding - # versions — that way Debian security uploads - # (libc6 glibc patches are frequent) don't break - # the workflow. - mkdir -p /tmp/arm64-stage - set -e - for src in libc6 libstdc++6 libfontconfig1 \ - libfreetype6 libgtk-3-0; do - # Look up the .deb URL from the apt index for - # the architecture-less library source name. - # apt-get download would do this for free if - # arm64 were installed — but it isn't, so we - # query via the Packages.gz on deb.debian.org. - url=$(wget -qO- \ - "https://packages.debian.org/bookworm/arm64/${src}/download" \ - 2>/dev/null \ - | grep -oE 'http[s]?://[^"]*'"${src}"'_[^"]*arm64\.deb' \ - | head -1) - if [ -z "$url" ]; then - echo "::error::Could not resolve .deb URL for $src" - exit 1 - fi - echo " --> downloading $url" - if ! wget -q "$url" -O "/tmp/${src}.deb"; then - echo "::error::wget failed for $url" - exit 1 - fi - dpkg-deb -x "/tmp/${src}.deb" /tmp/arm64-stage/ - rm -f "/tmp/${src}.deb" - done - ls /tmp/arm64-stage/lib/aarch64-linux-gnu/ 2>/dev/null | head -3 || true - ls /tmp/arm64-stage/usr/lib/aarch64-linux-gnu/ 2>/dev/null | head -3 || true - echo " --> arm64 stage ready at /tmp/arm64-stage/" - fi - # Extract the freshly downloaded .deb into the - # stage dir. - ls ${src}_*.deb - fi - # binutils-aarch64-linux-gnu provides aarch64-linux-gnu-objdump, - # which dh_makeshlibs needs to read the ELF symbol table of - # the PostIt.Desktop arm64 binary. Without it, dh_makeshlibs - # fails with "Can't exec 'aarch64-linux-gnu-objdump': No - # such file or directory" (~50 Mo, standard cross-toolkit - # binutils — no arm64 GCC needed because there's no C code to - # compile, only .NET to publish). - # - # We don't install libc6:arm64 etc. — dpkg-buildpackage -d - # skips the build-deps check (we don't need the arm64 SDK), - # and -Pcross toggles the cross-build profile so debhelper - # adapts to the cross-build context. + ca-certificates crossbuild-essential-arm64 rm -rf /var/lib/apt/lists/* : > "$STATE_FILE" @@ -303,7 +239,16 @@ jobs: # set -x traces every command so a silent failure inside # 'make deb' (e.g. dpkg-buildpackage aborting after the # 'mv ... || true' swallows the error) is visible. - ARM64_STAGE=/tmp/arm64-stage make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 || { + # Debian-recommended cross-build environment for dpkg-buildpackage: + # - CONFIG_SITE points autoconf at the cross-config for arm64 + # (set up by crossbuild-essential-arm64) + # - DEB_HOST_ARCH=arm64 is the target architecture + # - DEB_BUILD_ARCH=amd64 stays the build host + # - DEB_BUILD_OPTIONS=nocheck skips tests in the cross context + CONFIG_SITE=/etc/dpkg-cross/cross-config.arm64 \ + DEB_HOST_ARCH=arm64 DEB_BUILD_ARCH=amd64 \ + DEB_BUILD_OPTIONS=nocheck \ + make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 || { echo "::error::make deb for arm64 exited non-zero — see output above" exit 1 } diff --git a/debian/rules b/debian/rules index 94ab534..05122f9 100755 --- a/debian/rules +++ b/debian/rules @@ -126,21 +126,12 @@ override_dh_auto_test: # client). Build smoke is sufficient for the package. override_dh_shlibdeps: - # If a stage of arm64 libraries has been extracted into - # $(ARM64_STAGE)/usr/lib/aarch64-linux-gnu/ (by the - # upstream fetch-arm64-libs.sh script wired into the - # Forgejo Actions workflow), point dpkg-shlibdeps at it - # with -l so it can resolve ELF NEEDED entries from the - # arm64 binaries (libc.so.6, libstdc++.so.6, etc.) without - # installing arm64 on the amd64 host. - if [ -n "$(ARM64_STAGE)" ] && [ -d "$(ARM64_STAGE)/usr/lib/aarch64-linux-gnu" ]; then \ - echo " --> dpkg-shlibdeps: using arm64 stage at $(ARM64_STAGE)"; \ - dh_shlibdeps -- -l"$(ARM64_STAGE)/usr/lib/aarch64-linux-gnu"; \ - else \ - dh_shlibdeps -- \ - --warnings=0 \ - -l"$(DEB_HOST_MULTIARCH_DIR)" 2>/dev/null || true; \ - fi + # The runner image installs crossbuild-essential-arm64, which + # provides libc6-dev:arm64 et al. plus dpkg-cross — enough + # for dh_shlibdeps to resolve the arm64 binaries' NEEDED + # entries against the system's shlibs cache directly. No + # stage dir, no -l hack: standard dh_shlibdeps just works. + dh_shlibdeps override_dh_strip: # dh_strip disabled entirely. See MEMORY.md / AGENTS.md for the From 39729b8d0daaf1cb2ed9249c576f4b54eece5237 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 19:32:09 +0100 Subject: [PATCH 32/34] rules: cross-arch dh_shlibdeps, point at /usr/lib// MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crossbuild-essential-arm64 transitively installs libc6:arm64 et al. (Depends chain: crossbuild-essential-arm64 → gcc-aarch64- linux-gnu → libc6-dev:arm64 → libc6:arm64), placing the runtime .so files in /usr/lib/aarch64-linux-gnu/. The non-dev libs (libc.so.6, libm.so.6, etc.) live there, not in /usr/lib/ which is what dh_shlibdeps searches by default. dpkg-shlibdeps accepts -l for additional library search paths. We compute the right path via dpkg-architecture -aarm64 -q DEB_HOST_MULTIARCH (which returns 'aarch64-linux-gnu' without choking on the CC mismatch warning, since it's a pure query), and pass /usr/lib/$HOST_MULTIARCH/ as -l. Falls back to a literal aarch64-linux-gnu if dpkg-architecture isn't installed (it always is on a Debian build host). --- debian/rules | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/debian/rules b/debian/rules index 05122f9..7592306 100755 --- a/debian/rules +++ b/debian/rules @@ -126,12 +126,29 @@ override_dh_auto_test: # client). Build smoke is sufficient for the package. override_dh_shlibdeps: - # The runner image installs crossbuild-essential-arm64, which - # provides libc6-dev:arm64 et al. plus dpkg-cross — enough - # for dh_shlibdeps to resolve the arm64 binaries' NEEDED - # entries against the system's shlibs cache directly. No - # stage dir, no -l hack: standard dh_shlibdeps just works. - dh_shlibdeps + # In a cross-build (DEB_HOST_ARCH != DEB_BUILD_ARCH), the + # system's arch-specific lib dir is at + # /usr/lib//. crossbuild-essential-arm64 + # has transitively installed libc6:arm64 et al. there (it's + # a Depends chain: crossbuild-essential-arm64 → gcc-aarch64- + # linux-gnu → libc6-dev:arm64 → libc6:arm64). Pass that + # path via -l so dpkg-shlibdeps can resolve the arm64 + # binaries' NEEDED entries (libc.so.6, libstdc++.so.6, + # libfontconfig, etc.) against the on-host shlibs cache. + if [ -n "$(DEB_HOST_ARCH)" ] && [ "$(DEB_HOST_ARCH)" != "$(DEB_BUILD_ARCH)" ]; then \ + HOST_MULTIARCH=$$(dpkg-architecture -a$(DEB_HOST_ARCH) -q DEB_HOST_MULTIARCH 2>/dev/null) || \ + HOST_MULTIARCH=aarch64-linux-gnu; \ + HOST_LIB_DIR=/usr/lib/$${HOST_MULTIARCH}; \ + if [ -d "$$HOST_LIB_DIR" ]; then \ + echo " --> dh_shlibdeps: cross-arch $$HOST_LIB_DIR (DEB_HOST_ARCH=$(DEB_HOST_ARCH))"; \ + dh_shlibdeps -- -l"$$HOST_LIB_DIR"; \ + else \ + echo " --> dh_shlibdeps: WARN cross-arch dir $$HOST_LIB_DIR missing"; \ + dh_shlibdeps; \ + fi \ + else \ + dh_shlibdeps; \ + fi override_dh_strip: # dh_strip disabled entirely. See MEMORY.md / AGENTS.md for the From c2cdb88e643c67e74842831c787be2a63816f50b Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 19:38:56 +0100 Subject: [PATCH 33/34] workflow: also install arm64 runtime libs (libc6:arm64, etc.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crossbuild-essential-arm64 only pulls gcc-aarch64-linux-gnu + binutils-aarch64-linux-gnu + dpkg-cross. It does NOT transitively pull libc6:arm64 (verified on the Debian package index: the Depends chain stops at gcc-12-aarch64-linux-gnu, which doesn't depend on libc6:arm64). So dh_makeshlibs works (binutils is there), but dh_shlibdeps has no arm64 shlibs cache to read from. Add explicit runtime libs after a dpkg --add-architecture arm64 + apt-get update: dpkg --add-architecture arm64 apt-get update apt-get install -y --no-install-recommends \ build-essential debhelper imagemagick librsvg2-bin \ ca-certificates crossbuild-essential-arm64 \ libc6:arm64 libstdc++6:arm64 \ libfontconfig1:arm64 libfreetype6:arm64 \ libgtk-3-0:arm64 If the arm64 packages are unavailable from the runner's configured apt sources, this run will fail with 'Unable to locate package libc6:arm64' — but that was a real failure mode already explored in earlier runs (run #31, before we had dpkg --add-architecture arm64 in place). --- .forgejo/workflows/release.yml | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 6f357ba..d280912 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -74,24 +74,26 @@ jobs: # via `convert`, librsvg2-bin pour le SVG). run: | apt-get update - # crossbuild-essential-arm64 is the official Debian - # meta-package for arm64 cross-compilation. It pulls - # in gcc-aarch64-linux-gnu, binutils-aarch64-linux-gnu, - # libc6-dev:arm64, linux-libc-dev:arm64, and sets up - # dpkg-cross (CONFIG_SITE under /etc/dpkg-cross/). - # For our package, the key bits are: - # - binutils-aarch64-linux-gnu (provides - # aarch64-linux-gnu-objdump needed by dh_makeshlibs) - # - libc6-dev:arm64 and friends (so dpkg-shlibdeps can - # resolve the arm64 binaries' NEEDED entries against - # the cross-arch shlibs cache installed on the host). - # The arm64 gcc isn't strictly needed (we have no C - # code to compile — only dotnet publish), but it doesn't - # hurt and keeps the environment consistent for any - # future C-coded debhelper rules. + # Cross-build deps for arm64: + # 1. dpkg --add-architecture arm64 + apt-get update — make + # apt aware of arm64 packages. + # 2. Install the arm64 RUNTIME libs (libc6:arm64, etc.) + # so dpkg-shlibdeps can resolve the .so dependencies + # of the arm64 binaries via the on-host shlibs cache. + # The runtime libs (not -dev:arm64) is what carries + # the SONAME metadata that dpkg-shlibdeps matches. + # 3. crossbuild-essential-arm64 pulls gcc/binutils for + # arm64, plus dpkg-cross CONFIG_SITE files under + # /etc/dpkg-cross/. The cross-gcc is unused for our + # dotnet-only build but harmless. + dpkg --add-architecture arm64 + apt-get update apt-get install -y --no-install-recommends \ build-essential debhelper imagemagick librsvg2-bin \ - ca-certificates crossbuild-essential-arm64 + ca-certificates crossbuild-essential-arm64 \ + libc6:arm64 libstdc++6:arm64 \ + libfontconfig1:arm64 libfreetype6:arm64 \ + libgtk-3-0:arm64 rm -rf /var/lib/apt/lists/* : > "$STATE_FILE" From bdd7205667efc8363aa8de7f2d194c174e77baa1 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 19:44:44 +0100 Subject: [PATCH 34/34] Makefile: scope rm to this build's arch, don't erase sibling builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the workflow runs make deb twice in a row (amd64, then arm64), the second rm -f ../postit_*1.0.6-1*.deb glob matched BOTH postit_1.0.6-1_amd64.deb (produced by the first build) and postit_1.0.6-1_arm64.deb (about to be produced). So the second rm was erasing the first build's output before its own make ran. Result: postit_1.0.6-1_amd64.deb was created and then deleted by the arm64 build, leaving only the .deb arm64 in /src/. The 'Localiser les .deb produits' step failed with amd64=''. Scope the rm to this build's arch tag via DPKG_HOST: rm -f ../postit_-1_${DPKG_HOST}.deb \ ../postit_-1_${DPKG_HOST}.buildinfo \ ../postit_-1_${DPKG_HOST}.changes DPKG_HOST is already computed in the recipe for the dpkg- buildpackage call — we just reuse it for the rm. --- Makefile | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 18c57d1..43e7fc5 100644 --- a/Makefile +++ b/Makefile @@ -38,10 +38,13 @@ deb: # step the .deb always comes out as the version hardcoded in # debian/changelog.in, regardless of POSTIT_GIT_TAG. sed 's/@VERSION@/$(POSTIT_GIT_TAG)/g' debian/changelog.in > debian/changelog - # Remove any residual .deb from previous runs (same name - # pattern would otherwise be re-used by dpkg-deb if the - # previous run left it lying around). - rm -f ../postit_*$(POSTIT_GIT_TAG)-1*.deb ../postit_*.buildinfo ../postit_*.changes + # Remove only this build's residual .deb (avoid glob-matching + # .debs from sibling builds — the workflow invokes us once per + # architecture, and a wide glob would erase the .deb the + # previous build just produced). The pattern is the same one + # dpkg-deb will reuse if we don't clean up first: postit_- + # 1_.deb. + rm -f ../postit_$(POSTIT_GIT_TAG)-1_$${DPKG_HOST}.deb ../postit_$(POSTIT_GIT_TAG)-1_$${DPKG_HOST}.buildinfo ../postit_$(POSTIT_GIT_TAG)-1_$${DPKG_HOST}.changes # Use dpkg-architecture to set the target arch correctly for # cross-builds. For POSTIT_RUNTIME=linux-arm64, this exports # DEB_HOST_ARCH=arm64 (and friends) so dpkg-buildpackage names