diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 88a4e4f..d280912 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, @@ -71,10 +73,27 @@ jobs: # Debian (debhelper, imagemagick pour les icônes .png # via `convert`, librsvg2-bin pour le SVG). run: | + apt-get update + # 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 + 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" @@ -100,11 +119,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,37 +198,76 @@ 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" + 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 + # 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 /src/postit_*${TAG}-1_amd64.deb" + ls -la /src/ /src/_src/ 2>/dev/null + exit 1 + fi + echo "✓ Built $DEB_AMD64" - name: Build .deb arm64 env: POSTIT_RUNTIME: linux-arm64 run: | - source "$STATE_FILE" + 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. + # 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 + } + # 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 /src/postit_*${TAG}-1_arm64.deb" + echo "Inspect the dpkg-buildpackage output above for the root cause." + ls -la /src/ /src/_src/ 2>/dev/null + exit 1 + fi + echo "✓ Built $DEB_ARM64" + set +x - 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 +278,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 +290,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 diff --git a/Makefile b/Makefile index a870e5a..43e7fc5 100644 --- a/Makefile +++ b/Makefile @@ -38,14 +38,57 @@ 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 - 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. - mv ../postit_*$(POSTIT_GIT_TAG)-1*.deb $(POSTIT_OUT_DIR)/ 2>/dev/null || \ - mv ../postit_*.deb $(POSTIT_OUT_DIR)/ || true - @echo " ✓ artifacts moved to $(POSTIT_OUT_DIR)" + # 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 + # 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. + # 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. + # 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 -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' + # 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 diff --git a/debian/rules b/debian/rules index 2745116..7592306 100755 --- a/debian/rules +++ b/debian/rules @@ -126,10 +126,34 @@ override_dh_auto_test: # client). Build smoke is sufficient for the package. override_dh_shlibdeps: - 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 --exclude=.pdb + # 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