From c8bd641e27febbf84b475ac2f098125f7e992ba7 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:11:13 -0400 Subject: [PATCH] release: Linux .deb and .rpm packages (#125) cargo-deb and cargo-generate-rpm (pinned) package the same stripped binaries and files as the tar.gz under /usr. The release job compares the file lists, runs lintian, and installs both packages in Ubuntu 22.04 and Fedora containers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- .github/workflows/release.yml | 47 +++++++++++++++++--- ATTRIBUTION.md | 1 + apps/fieldcraft/Cargo.toml | 82 ++++++++++++++++++++++++++++++++++- docs/releasing.md | 20 +++++++-- packaging/linux/package.sh | 33 +++++++++++--- 5 files changed, 169 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2964d8e..7a88e44 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ # and pushes to develop or tags never start it. # # macOS universal (arm64 + x86_64) .dmg with FieldCraft.app, the CLI as a .zip packaging/macos -# Linux x86_64 (glibc 2.35+) .tar.gz and .AppImage packaging/linux +# Linux x86_64 (glibc 2.35+) .tar.gz, .AppImage, .deb and .rpm packaging/linux # Windows x64 .zip with fieldcraft.exe and fieldcraft-cli.exe packaging/windows # # macOS: signed with a Developer ID and notarized when the APPLE_* secrets exist, ad-hoc @@ -119,33 +119,69 @@ jobs: timeout-minutes: 90 env: FIELDCRAFT_VERSION: ${{ needs.version.outputs.version }} + # The .deb and .rpm builders (both MIT), pinned. + CARGO_DEB_VERSION: 3.8.0 + CARGO_GENERATE_RPM_VERSION: 0.21.0 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install packaging tools run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends desktop-file-utils + sudo apt-get install -y --no-install-recommends desktop-file-utils lintian rpm - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-linux-x86_64 # Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's. save-if: ${{ github.ref == 'refs/heads/main' }} - - name: Build and package (tar.gz, AppImage) + - name: Install cargo-deb and cargo-generate-rpm + run: | + cargo install --locked --version "$CARGO_DEB_VERSION" cargo-deb + cargo install --locked --version "$CARGO_GENERATE_RPM_VERSION" cargo-generate-rpm + - name: Build and package (tar.gz, AppImage, deb, rpm) run: packaging/linux/package.sh - name: Check the packages run: | . packaging/env.sh - list="$(tar -tzf dist/release/*.tar.gz)" + base="fieldcraft-$VERSION-linux-x86_64" + deb="dist/release/fieldcraft_${VERSION}_amd64.deb" + rpm="dist/release/fieldcraft-$VERSION.x86_64.rpm" + list="$(tar -tzf "dist/release/$base.tar.gz")" for f in bin/fieldcraft bin/fieldcraft-cli "share/applications/$APP_ID.desktop" \ share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt; do - grep -Fx -- "fieldcraft-$VERSION-linux-x86_64/$f" <<<"$list" + grep -Fx -- "$base/$f" <<<"$list" done + # The .deb and .rpm carry the tar.gz's files under /usr (the .deb adds its copyright + # file); their lists live in apps/fieldcraft/Cargo.toml, so a drift fails here. + files() { grep -v '/$' | sed -E "s#^(\./|/|$base/)##; s#^usr/##" | grep -vx 'share/doc/fieldcraft/copyright' | sort; } + diff <(files <<<"$list") <(dpkg-deb --fsys-tarfile "$deb" | tar -t | files) + diff <(files <<<"$list") <(rpm -qpl "$rpm" | files) + dpkg-deb --info "$deb" + rpm -qpi "$rpm" + echo "rpm requires:"; rpm -qpR "$rpm" + lintian --info --fail-on error "$deb" ldd target/release/fieldcraft for appimage in dist/release/*.AppImage; do chmod +x "$appimage" APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version done + - name: Install the .deb and .rpm in containers + run: | + # The oldest supported Ubuntu (the build's glibc) and a current Fedora. Recommended and + # weak dependencies (GPU drivers) are skipped to keep it quick; the hard ones must resolve. + docker run --rm -v "$PWD/dist/release:/dist:ro" ubuntu:22.04 bash -euxc ' + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends /dist/*.deb + fieldcraft-cli --version + fieldcraft --version + test -f /usr/share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt' + docker run --rm -v "$PWD/dist/release:/dist:ro" fedora:44 bash -euxc ' + dnf install -y -q --setopt=install_weak_deps=False /dist/*.rpm + fieldcraft-cli --version + fieldcraft --version + test -f /usr/share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt + dnf install -y -q rpmlint + rpmlint /dist/*.rpm || true' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-linux-x86_64 @@ -201,6 +237,7 @@ jobs: run: | for f in "fieldcraft-$VERSION-macos-universal.dmg" "fieldcraft-cli-$VERSION-macos-universal.zip" \ "fieldcraft-$VERSION-linux-x86_64.tar.gz" "fieldcraft-$VERSION-linux-x86_64.AppImage" \ + "fieldcraft_${VERSION}_amd64.deb" "fieldcraft-$VERSION.x86_64.rpm" \ "fieldcraft-$VERSION-windows-x64.zip"; do test -s "artifacts/$f" || { echo "::error::missing $f"; exit 1; } done diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index caa8962..024b6fb 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -180,6 +180,7 @@ is committed and listed here with its exports. | crates/mcp/src/ (JSON-RPC framing, MCP lifecycle, tool schema checks, the control-channel backend, base64) | SolidCraft `crates/mcp/src/` | MIT OR Apache-2.0 | | crates/io/src/roots.rs (the rules of read and write roots) | VectorCraft's automation roots (docs/mcp.md there) | MIT OR Apache-2.0 | | packaging/env.sh, packaging/.shellcheckrc, packaging/macos/package.sh, packaging/macos/Info.plist.in, packaging/linux/package.sh, packaging/linux/AppRun, packaging/windows/package.ps1 | SolidCraft `packaging/` and VectorCraft `packaging/` (signing and notarization in macos/package.sh) | MIT OR Apache-2.0 | +| apps/fieldcraft/Cargo.toml (the run-time dependency lists of `[package.metadata.deb]` and `[package.metadata.generate-rpm]`) | SolidCraft `packaging/linux/nfpm.yaml` | MIT OR Apache-2.0 | | packaging/macos/import-cert.sh | VectorCraft `packaging/macos/import-cert.sh` | MIT OR Apache-2.0 | | packaging/windows/check-exe.ps1 | SolidCraft `packaging/windows/check-icon.ps1` | MIT OR Apache-2.0 | | apps/fieldcraft-web/ (the web runner, browser file services and downloads, Trunk configuration, index.html) | VectorCraft `apps/vectorcraft-web/` and SolidCraft `apps/solidcraft-web/` | MIT OR Apache-2.0 | diff --git a/apps/fieldcraft/Cargo.toml b/apps/fieldcraft/Cargo.toml index edc30f6..43a7edc 100644 --- a/apps/fieldcraft/Cargo.toml +++ b/apps/fieldcraft/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fieldcraft" -description = "FieldCraft desktop app" +description = "Finite element modeling: meshing, model setup, solver decks and a linear solver" version.workspace = true edition.workspace = true license.workspace = true @@ -22,3 +22,83 @@ log = { workspace = true } [build-dependencies] # The Windows executable's icon resource (build.rs). embed-resource = "3.0.12" + +# Linux .deb and .rpm packages (cargo-deb, cargo-generate-rpm), built by packaging/linux/package.sh +# from the release binaries it has already built and stripped; it sets the version and the file +# names. The file list matches the tar.gz's (package.sh's staged tree; the release workflow +# compares them). +# The binaries link only glibc and libgcc_s. Windowing (Wayland, X11, xkbcommon), the GPU (Vulkan +# or EGL) and the file dialogs (the XDG desktop portal) are opened at run time, so they are no ELF +# NEEDED entries and the tools cannot find them: they are listed here. +[package.metadata.deb] +name = "fieldcraft" +maintainer = "FieldCraft contributors" +copyright = "FieldCraft contributors" +section = "science" +priority = "optional" +extended-description = """\ +FieldCraft turns geometry into analysis-ready finite element models: volume \ +meshes, materials, sections, boundary conditions, loads and steps. It writes \ +Abaqus input, Nastran bulk data and LS-DYNA keyword decks, and solves linear \ +static and modal problems with its own solver. Includes fieldcraft-cli, a \ +headless command runner and MCP server.""" +depends = "$auto, libxkbcommon0, libxkbcommon-x11-0, libwayland-client0, libx11-6, libx11-xcb1, libxcb1, libxcursor1, libxi6, libvulkan1 | libegl1" +recommends = "mesa-vulkan-drivers, libegl-mesa0, xdg-desktop-portal" +assets = [ + ["target/release/fieldcraft", "usr/bin/", "755"], + ["target/release/fieldcraft-cli", "usr/bin/", "755"], + ["../../packaging/linux/io.github.jyviko.fieldcraft.desktop", "usr/share/applications/", "644"], + ["../../assets/app-icon/icon.iconset/icon_16x16.png", "usr/share/icons/hicolor/16x16/apps/io.github.jyviko.fieldcraft.png", "644"], + ["../../assets/app-icon/icon.iconset/icon_32x32.png", "usr/share/icons/hicolor/32x32/apps/io.github.jyviko.fieldcraft.png", "644"], + ["../../assets/app-icon/icon.iconset/icon_32x32@2x.png", "usr/share/icons/hicolor/64x64/apps/io.github.jyviko.fieldcraft.png", "644"], + ["../../assets/app-icon/icon.iconset/icon_128x128.png", "usr/share/icons/hicolor/128x128/apps/io.github.jyviko.fieldcraft.png", "644"], + ["../../assets/app-icon/icon.iconset/icon_256x256.png", "usr/share/icons/hicolor/256x256/apps/io.github.jyviko.fieldcraft.png", "644"], + ["../../assets/app-icon/icon.iconset/icon_512x512.png", "usr/share/icons/hicolor/512x512/apps/io.github.jyviko.fieldcraft.png", "644"], + ["../../assets/app-icon/icon.iconset/icon_512x512@2x.png", "usr/share/icons/hicolor/1024x1024/apps/io.github.jyviko.fieldcraft.png", "644"], + ["../../README.md", "usr/share/doc/fieldcraft/", "644"], + ["../../LICENSE-MIT", "usr/share/doc/fieldcraft/", "644"], + ["../../LICENSE-APACHE", "usr/share/doc/fieldcraft/", "644"], + ["../../NOTICE", "usr/share/doc/fieldcraft/", "644"], + ["../../THIRD-PARTY-LICENSES.txt", "usr/share/doc/fieldcraft/", "644"], +] + +[package.metadata.generate-rpm] +name = "fieldcraft" +license = "MIT OR Apache-2.0" +url = "https://github.com/jyviko/fieldcraft" +assets = [ + { source = "target/release/fieldcraft", dest = "/usr/bin/fieldcraft", mode = "755" }, + { source = "target/release/fieldcraft-cli", dest = "/usr/bin/fieldcraft-cli", mode = "755" }, + { source = "../../packaging/linux/io.github.jyviko.fieldcraft.desktop", dest = "/usr/share/applications/io.github.jyviko.fieldcraft.desktop", mode = "644" }, + { source = "../../assets/app-icon/icon.iconset/icon_16x16.png", dest = "/usr/share/icons/hicolor/16x16/apps/io.github.jyviko.fieldcraft.png", mode = "644" }, + { source = "../../assets/app-icon/icon.iconset/icon_32x32.png", dest = "/usr/share/icons/hicolor/32x32/apps/io.github.jyviko.fieldcraft.png", mode = "644" }, + { source = "../../assets/app-icon/icon.iconset/icon_32x32@2x.png", dest = "/usr/share/icons/hicolor/64x64/apps/io.github.jyviko.fieldcraft.png", mode = "644" }, + { source = "../../assets/app-icon/icon.iconset/icon_128x128.png", dest = "/usr/share/icons/hicolor/128x128/apps/io.github.jyviko.fieldcraft.png", mode = "644" }, + { source = "../../assets/app-icon/icon.iconset/icon_256x256.png", dest = "/usr/share/icons/hicolor/256x256/apps/io.github.jyviko.fieldcraft.png", mode = "644" }, + { source = "../../assets/app-icon/icon.iconset/icon_512x512.png", dest = "/usr/share/icons/hicolor/512x512/apps/io.github.jyviko.fieldcraft.png", mode = "644" }, + { source = "../../assets/app-icon/icon.iconset/icon_512x512@2x.png", dest = "/usr/share/icons/hicolor/1024x1024/apps/io.github.jyviko.fieldcraft.png", mode = "644" }, + { source = "../../README.md", dest = "/usr/share/doc/fieldcraft/README.md", mode = "644", doc = true }, + { source = "../../LICENSE-MIT", dest = "/usr/share/doc/fieldcraft/LICENSE-MIT", mode = "644", doc = true }, + { source = "../../LICENSE-APACHE", dest = "/usr/share/doc/fieldcraft/LICENSE-APACHE", mode = "644", doc = true }, + { source = "../../NOTICE", dest = "/usr/share/doc/fieldcraft/NOTICE", mode = "644", doc = true }, + { source = "../../THIRD-PARTY-LICENSES.txt", dest = "/usr/share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt", mode = "644", doc = true }, +] +# The binaries' ELF dependencies (glibc, libgcc_s) are found automatically; these are opened at run +# time. Soname provides rather than package names, so the same requirements resolve on Fedora/RHEL +# and openSUSE. +require-sh = false + +[package.metadata.generate-rpm.requires] +"libxkbcommon.so.0()(64bit)" = "*" +"libxkbcommon-x11.so.0()(64bit)" = "*" +"libwayland-client.so.0()(64bit)" = "*" +"libX11.so.6()(64bit)" = "*" +"libX11-xcb.so.1()(64bit)" = "*" +"libxcb.so.1()(64bit)" = "*" +"libXcursor.so.1()(64bit)" = "*" +"libXi.so.6()(64bit)" = "*" +"(libvulkan.so.1()(64bit) or libEGL.so.1()(64bit))" = "" + +[package.metadata.generate-rpm.recommends] +mesa-vulkan-drivers = "*" +xdg-desktop-portal = "*" diff --git a/docs/releasing.md b/docs/releasing.md index 0523f31..d110180 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -11,7 +11,7 @@ are in `packaging/` and run the same way on a developer machine. | Platform | Files | Script | |---|---|---| | macOS 11+, universal (arm64 + x86_64) | `fieldcraft--macos-universal.dmg` (FieldCraft.app on a drag-to-Applications image), `fieldcraft-cli--macos-universal.zip` | `packaging/macos/package.sh` | -| Linux x86_64, glibc 2.35+ | `fieldcraft--linux-x86_64.tar.gz` (`bin/`, `share/`), `fieldcraft--linux-x86_64.AppImage` | `packaging/linux/package.sh` | +| Linux x86_64, glibc 2.35+ | `fieldcraft--linux-x86_64.tar.gz` (`bin/`, `share/`), `fieldcraft--linux-x86_64.AppImage`, `fieldcraft__amd64.deb` (Debian, Ubuntu 22.04+), `fieldcraft-.x86_64.rpm` (Fedora, RHEL, openSUSE) | `packaging/linux/package.sh` | | Windows x64 | `fieldcraft--windows-x64.zip` (`fieldcraft.exe`, `fieldcraft-cli.exe`) | `packaging/windows/package.ps1` | Plus `SHA256SUMS.txt` and the notes from `.github/release-notes.md` followed by GitHub's @@ -23,6 +23,17 @@ entry and icon name, and the window's Wayland app_id / X11 class. The icons come `icon.ico` embedded in `fieldcraft.exe` (`apps/fieldcraft/build.rs`), the iconset PNGs as the Linux hicolor icons. +The `.deb` and `.rpm` are built by `cargo-deb` and `cargo-generate-rpm` (versions pinned in +`release.yml`) from the same stripped release binaries as the tar.gz, with the same files under +`/usr` (`/usr/bin`, the desktop entry, the hicolor icons, the licences in +`/usr/share/doc/fieldcraft`); their metadata and file lists are in `apps/fieldcraft/Cargo.toml`. +The binaries link only glibc; the windowing, GPU and file-dialog libraries they open at run time +are listed as dependencies by hand (Debian package names, RPM soname capabilities). A +pre-release `0.2.0-rc.1` is versioned `0.2.0~rc.1` inside the packages so it sorts before +`0.2.0`; the file names keep the release version. The Linux job compares both file lists with +the tar.gz, runs lintian, and installs the packages in Ubuntu 22.04 and Fedora containers to run +`fieldcraft-cli --version`. + ## Signing **macOS.** When the repository has the secrets below, the app, the dmg and the CLI are signed @@ -85,7 +96,8 @@ Publishing is a maintainer's call; an agent preparing a release stops at the dra run `shasum -a 256 -c SHA256SUMS.txt`, and check every platform's files are there. 3. Smoke-test what you can run: open the dmg, drag the app to Applications, launch it with `--sample cantilever`; run `fieldcraft-cli --version` from the zip or tar.gz; on Linux run the - AppImage; on Windows unzip and start `fieldcraft.exe`. Look at the run's logs for the + AppImage, or install the `.deb` (`sudo apt install ./fieldcraft__amd64.deb`) or `.rpm` + (`sudo dnf install ./fieldcraft-.x86_64.rpm`); on Windows unzip and start `fieldcraft.exe`. Look at the run's logs for the platforms you could not try (each job checks its own package). 4. Edit the notes: what is new, fixes, breaking changes, known limitations. Keep the install and signing paragraphs; never claim signing the notes don't state. @@ -99,4 +111,6 @@ Publishing is a maintainer's call; an agent preparing a release stops at the dra uploads them with the notes as the `release-assets` artifact of the run, without creating a release. `-f draft=true` creates or updates the draft, and is accepted on `main` only. Locally, `packaging/macos/package.sh --arch aarch64` (or `x86_64`, `universal`) and -`packaging/linux/package.sh` write to `dist/release/`. +`packaging/linux/package.sh` write to `dist/release/` (the `.deb` and `.rpm` need +`cargo install --locked cargo-deb cargo-generate-rpm` at the versions in `release.yml`; +`--formats "tar appimage"` skips them). diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index c29b819..fc218fe 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -4,12 +4,17 @@ # $DIST/fieldcraft--linux-x86_64.tar.gz bin/fieldcraft, bin/fieldcraft-cli, the # .desktop entry, icons and licences # $DIST/fieldcraft--linux-x86_64.AppImage the app as one file (chmod +x, run) +# $DIST/fieldcraft__amd64.deb Debian/Ubuntu package (cargo-deb) +# $DIST/fieldcraft-.x86_64.rpm Fedora/RHEL/openSUSE package (cargo-generate-rpm) # -# Usage: packaging/linux/package.sh [--skip-build] [--formats "tar appimage"] +# Usage: packaging/linux/package.sh [--skip-build] [--formats "tar appimage deb rpm"] # # Build on an old distribution (CI: Ubuntu 22.04, glibc 2.35) so the binaries run on newer ones. # The AppImage uses appimagetool and the type 2 runtime at pinned releases, checked against their -# SHA-256 (downloaded into $CARGO_TARGET_DIR). Optional: desktop-file-validate. +# SHA-256 (downloaded into $CARGO_TARGET_DIR). The .deb and .rpm need cargo-deb and +# cargo-generate-rpm (the versions release.yml installs); their metadata and file lists are in +# apps/fieldcraft/Cargo.toml. FIELDCRAFT_MAINTAINER sets the .deb's Maintainer field. +# Optional: desktop-file-validate. # Adapted from SolidCraft's packaging/linux/package.sh (MIT OR Apache-2.0; see ATTRIBUTION.md). set -euo pipefail # shellcheck source=../env.sh @@ -18,12 +23,12 @@ HERE="$ROOT/packaging/linux" ICONSET="$ROOT/assets/app-icon/icon.iconset" SKIP_BUILD=0 -FORMATS="tar appimage" +FORMATS="tar appimage deb rpm" while [ $# -gt 0 ]; do case "$1" in --skip-build) SKIP_BUILD=1; shift ;; --formats) FORMATS="$2"; shift 2 ;; - -h | --help) sed -n '2,13p' "$0"; exit 0 ;; + -h | --help) sed -n '2,18p' "$0"; exit 0 ;; *) echo "unknown argument: $1" >&2; exit 2 ;; esac done @@ -50,6 +55,8 @@ if [ "$SKIP_BUILD" = 0 ]; then (cd "$ROOT" && cargo build --release --locked -p fieldcraft -p fieldcraft-cli) fi BIN="$CARGO_TARGET_DIR/release" +# Every format ships stripped binaries; cargo-generate-rpm packages target/release as it is. +strip "$BIN/fieldcraft" "$BIN/fieldcraft-cli" WORK="$CARGO_TARGET_DIR/linux-package" STAGE="$WORK/root/usr" rm -rf "$WORK" @@ -57,7 +64,6 @@ rm -rf "$WORK" # ---- an FHS tree, shared by both formats -------------------------------------------------------- install -Dm755 "$BIN/fieldcraft" "$STAGE/bin/fieldcraft" install -Dm755 "$BIN/fieldcraft-cli" "$STAGE/bin/fieldcraft-cli" -strip "$STAGE/bin/fieldcraft" "$STAGE/bin/fieldcraft-cli" install -Dm644 "$HERE/$APP_ID.desktop" "$STAGE/share/applications/$APP_ID.desktop" # hicolor icons: . while read -r size file; do @@ -85,6 +91,23 @@ if has tar; then echo "wrote $DIST/$BASENAME.tar.gz" fi +# ---- .deb and .rpm ------------------------------------------------------------------------------ +# From the binaries built and stripped above (--no-build). Semver pre-releases (0.2.0-rc.1) become +# 0.2.0~rc.1 inside the packages so they sort before the release; the file names keep $VERSION. +PKG_VERSION="$(printf '%s' "$VERSION" | sed 's/-/~/')" +if has deb; then + case "$ARCH" in x86_64) DEB_ARCH=amd64 ;; *) DEB_ARCH="$ARCH" ;; esac + OUT="$DIST/fieldcraft_${VERSION}_$DEB_ARCH.deb" + (cd "$ROOT" && cargo deb --locked -p fieldcraft --no-build --deb-version "$PKG_VERSION" \ + --maintainer "${FIELDCRAFT_MAINTAINER:-FieldCraft contributors}" -o "$OUT") + echo "wrote $OUT" +fi +if has rpm; then + OUT="$DIST/fieldcraft-$VERSION.$ARCH.rpm" + (cd "$ROOT" && cargo generate-rpm -p apps/fieldcraft -s "version = \"$PKG_VERSION\"" -o "$OUT") + echo "wrote $OUT" +fi + # ---- AppImage ----------------------------------------------------------------------------------- # Usage: fetch ; downloads once and checks the hash every time. fetch() {