diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5413849..8dd49a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,7 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ inputs.version }} + prepared_ref: ${{ steps.prepared-ref.outputs.ref }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -44,7 +45,7 @@ jobs: git config user.email "christian.visintin@veeso.dev" - name: Install git-cliff - uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 + uses: taiki-e/install-action@e67fa11c4b9316fa714ddf0abed07a0c3143b95b # v2.87.4 with: tool: git-cliff @@ -66,6 +67,9 @@ jobs: VERSION: ${{ inputs.version }} run: git-cliff --unreleased --tag "v$VERSION" --strip header -o RELEASE_NOTES.md + - name: Verify Cargo.lock is unchanged + run: git diff --exit-code -- Cargo.lock + - name: Upload release notes uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -84,10 +88,22 @@ jobs: VERSION: ${{ inputs.version }} run: | rm -f RELEASE_NOTES.md - git add -A + git add -A -- . ':!Cargo.lock' + git diff --cached --exit-code -- Cargo.lock git commit -m "chore: release v$VERSION" git push origin HEAD:main + - name: Export prepared ref + id: prepared-ref + env: + DRY_RUN: ${{ inputs.dry_run }} + run: | + if [ "$DRY_RUN" = "true" ]; then + echo "ref=$GITHUB_SHA" >> "$GITHUB_OUTPUT" + else + echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + fi + build: needs: prepare name: build-${{ matrix.target }} @@ -95,14 +111,12 @@ jobs: fail-fast: false matrix: include: - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest + - target: x86_64-unknown-linux-musl + os: ubuntu-24.04 kind: linux - deb_suffix: amd64 - - target: aarch64-unknown-linux-gnu + - target: aarch64-unknown-linux-musl os: ubuntu-24.04-arm kind: linux - deb_suffix: arm64 - target: aarch64-apple-darwin os: macos-latest kind: macos @@ -125,58 +139,44 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.dry_run && github.sha || 'main' }} + ref: ${{ needs.prepare.outputs.prepared_ref }} persist-credentials: false fetch-depth: 0 fetch-tags: true + - name: Prepare release version + shell: bash + run: | + dist/release/bump_version.sh "$VERSION" "$(date +%F)" + cargo update --package termscp --precise "$VERSION" + cargo metadata --locked --no-deps --format-version 1 > /dev/null - name: Install just uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 - name: Install Rust target - if: matrix.kind != 'windows' + if: matrix.kind == 'macos' run: rustup target add "$TARGET" - name: Install Rust target if: matrix.kind == 'windows' run: rustup target add "$env:TARGET" - # ---- Linux: native per-arch build (x86_64 on ubuntu-latest, aarch64 on ubuntu-24.04-arm) ---- - - name: Install dependencies (Linux) + # ---- Linux: static musl build in a pinned Alpine container ---- + - name: Install cargo-deb (Linux) if: matrix.kind == 'linux' - run: | - sudo apt-get update - sudo apt-get install -y \ - make \ - libgit2-dev \ - build-essential \ - pkg-config \ - libbsd-dev \ - libcap-dev \ - libcups2-dev \ - libgnutls28-dev \ - libicu-dev \ - libjansson-dev \ - libkeyutils-dev \ - libldap2-dev \ - zlib1g-dev \ - libpam0g-dev \ - libacl1-dev \ - libarchive-dev \ - flex \ - bison \ - libntirpc-dev \ - libtracker-sparql-3.0-dev \ - libglib2.0-dev \ - libdbus-1-dev \ - libsasl2-dev \ - libunistring-dev \ - cpanminus - sudo cpanm Parse::Yapp::Driver - cargo install cargo-deb + run: cargo install cargo-deb --locked - name: Build (Linux) if: matrix.kind == 'linux' - run: just build_release "$TARGET" "--features smb-vendored" + run: just build_musl "$TARGET" - name: Build deb (Linux) if: matrix.kind == 'linux' - run: just package_deb "$TARGET" + run: cargo deb --locked --no-build --target "$TARGET" --features smb-vendored + - name: Verify deb declares no runtime dependencies (Linux) + if: matrix.kind == 'linux' + run: | + deb=$(ls target/"$TARGET"/debian/*.deb) + depends=$(dpkg-deb -f "$deb" Depends) + if [ -n "$depends" ]; then + echo "static deb must have no Depends, got: $depends" >&2 + exit 1 + fi # ---- macOS ---- - name: Install deps (macOS) @@ -229,7 +229,7 @@ jobs: if-no-files-found: error publish-homebrew: - needs: [prepare, build] + needs: [prepare, build, release] runs-on: ubuntu-latest env: VERSION: ${{ needs.prepare.outputs.version }} @@ -255,8 +255,8 @@ jobs: cd "$GITHUB_WORKSPACE" SHA_MAC_ARM=$(cat dl/aarch64-apple-darwin.sha256) SHA_MAC_X64=$(cat dl/x86_64-apple-darwin.sha256) - SHA_LIN_ARM=$(cat dl/aarch64-unknown-linux-gnu.sha256) - SHA_LIN_X64=$(cat dl/x86_64-unknown-linux-gnu.sha256) + SHA_LIN_ARM=$(cat dl/aarch64-unknown-linux-musl.sha256) + SHA_LIN_X64=$(cat dl/x86_64-unknown-linux-musl.sha256) BASE="https://github.com/veeso/termscp/releases/latest/download" cat > tap/Formula/termscp.rb < /dev/null + - name: Download build artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -399,10 +403,15 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: main + ref: ${{ needs.prepare.outputs.prepared_ref }} persist-credentials: false fetch-depth: 0 fetch-tags: true + - name: Prepare release version + run: | + dist/release/bump_version.sh "$VERSION" "$(date +%F)" + cargo update --package termscp --precise "$VERSION" + cargo metadata --locked --no-deps --format-version 1 > /dev/null - name: Install just uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 diff --git a/Cargo.toml b/Cargo.toml index 287cdc3..c89cc3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ termscp = { path = "/usr/bin/termscp" } [package.metadata.deb] maintainer = "Christian Visintin " copyright = "2025, Christian Visintin " +depends = "" extended-description-file = "docs/misc/README.deb.txt" [features] diff --git a/README.md b/README.md index 0d1577a..881d2c3 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,12 @@ For more information or other platforms, please visit [termscp.rs](https://terms ### Requirements ❗ +The official Linux binaries and `.deb` packages are statically linked against +musl and have **no runtime requirements**: they run on any distribution and +any glibc version. + +These are only required to build termscp from source: + - **Linux** users: - libdbus-1 - pkg-config diff --git a/dist/release/build_musl.sh b/dist/release/build_musl.sh new file mode 100755 index 0000000..ed2188a --- /dev/null +++ b/dist/release/build_musl.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env sh +# Builds a static musl termscp release binary for the given target triple +# inside a pinned Alpine container. +# +# Usage: dist/release/build_musl.sh +set -eu + +IMAGE="rust:1.98-alpine3.22" + +TARGET="${1:-}" +if [ -z "$TARGET" ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +case "$TARGET" in + x86_64-unknown-linux-musl) PLATFORM="linux/amd64" ;; + aarch64-unknown-linux-musl) PLATFORM="linux/arm64" ;; + *) + echo "unsupported target: $TARGET" >&2 + exit 2 + ;; +esac + +WORKSPACE="$(CDPATH='' cd -- "$(dirname -- "$0")/../.." && pwd)" + +# The container appends a [patch.crates-io] section to Cargo.toml and updates +# Cargo.lock; keep byte-exact copies so the workspace is clean afterwards. +BACKUP_DIR="$(mktemp -d)" +cp -p "$WORKSPACE/Cargo.toml" "$BACKUP_DIR/Cargo.toml" +cp -p "$WORKSPACE/Cargo.lock" "$BACKUP_DIR/Cargo.lock" + +restore_manifests() { + cp -p "$BACKUP_DIR/Cargo.toml" "$WORKSPACE/Cargo.toml" + cp -p "$BACKUP_DIR/Cargo.lock" "$WORKSPACE/Cargo.lock" + rm -rf "$BACKUP_DIR" +} +trap restore_manifests EXIT + +HOST_UID="$(id -u)" +HOST_GID="$(id -g)" +export TARGET HOST_UID HOST_GID + +docker run --rm \ + --platform "$PLATFORM" \ + --env TARGET \ + --env HOST_UID \ + --env HOST_GID \ + --volume "$WORKSPACE:/work" \ + --workdir /work \ + "$IMAGE" \ + sh /work/dist/release/build_musl_container.sh diff --git a/dist/release/build_musl_container.sh b/dist/release/build_musl_container.sh new file mode 100755 index 0000000..9074690 --- /dev/null +++ b/dist/release/build_musl_container.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env sh +# Builds a static musl termscp binary. Runs INSIDE the Alpine container +# started by dist/release/build_musl.sh; /work is the mounted workspace. +# +# Required environment: TARGET, HOST_UID, HOST_GID. +set -eux + +NETTLE_VERSION="3.10.1" +GNUTLS_VERSION="3.8.13" +PAVAO_SRC_VERSION="4.24.6" + +cleanup() { + chown -R "$HOST_UID:$HOST_GID" /work +} +trap cleanup EXIT + +apk add --no-cache \ + bison \ + binutils \ + build-base \ + file \ + flex \ + git \ + gnutls-dev \ + libgit2-dev \ + libgit2-static \ + libunistring-dev \ + libunistring-static \ + linux-headers \ + openssl-dev \ + openssl-libs-static \ + perl \ + perl-parse-yapp \ + pkgconf \ + python3 \ + wget \ + xz \ + zlib-dev \ + zlib-static + +rustup target add "$TARGET" +cargo fetch --locked + +NATIVE_CFLAGS="-O2 -fPIC" +if [ "$TARGET" = "aarch64-unknown-linux-musl" ]; then + NATIVE_CFLAGS="$NATIVE_CFLAGS -mno-outline-atomics" +fi +export CFLAGS="$NATIVE_CFLAGS" + +# -- static nettle (GnuTLS crypto backend); mini-gmp avoids a GMP dependency +mkdir -p /tmp/native +wget -q "https://ftp.gnu.org/gnu/nettle/nettle-$NETTLE_VERSION.tar.gz" \ + -O /tmp/native/nettle.tar.gz +tar -xzf /tmp/native/nettle.tar.gz -C /tmp/native +cd "/tmp/native/nettle-$NETTLE_VERSION" +./configure \ + --prefix=/tmp/native/nettle \ + --disable-shared \ + --enable-static \ + --disable-documentation \ + --enable-mini-gmp +make -j"$(getconf _NPROCESSORS_ONLN)" +make install + +# -- static GnuTLS; every optional backend is disabled so nothing links +# against a shared library +wget -q "https://www.gnupg.org/ftp/gcrypt/gnutls/v3.8/gnutls-$GNUTLS_VERSION.tar.xz" \ + -O /tmp/native/gnutls.tar.xz +tar -xf /tmp/native/gnutls.tar.xz -C /tmp/native +cd "/tmp/native/gnutls-$GNUTLS_VERSION" +PKG_CONFIG_PATH=/tmp/native/nettle/lib/pkgconfig \ + ./configure \ + --prefix=/tmp/native/gnutls \ + --disable-shared \ + --enable-static \ + --disable-doc \ + --disable-tests \ + --disable-nls \ + --disable-hardware-acceleration \ + --with-nettle-mini \ + --with-included-libtasn1 \ + --with-included-unistring \ + --without-idn \ + --without-p11-kit \ + --without-brotli \ + --without-zstd \ + --without-zlib +make -j"$(getconf _NPROCESSORS_ONLN)" +make install + +# -- flatten gnutls.pc: pkg-config must hand the linker the static archives +# directly, with no Requires.private chain to resolve +mkdir -p /tmp/native/pkgconfig +sed \ + -e "s#^Libs:.*#Libs: -L/tmp/native/gnutls/lib -lgnutls -latomic -L/tmp/native/nettle/lib -lhogweed -lnettle#" \ + -e "/^Requires.private:/d" \ + -e "s#^Cflags:.*#Cflags: -I/tmp/native/gnutls/include -I/tmp/native/nettle/include#" \ + /tmp/native/gnutls/lib/pkgconfig/gnutls.pc \ + > /tmp/native/pkgconfig/gnutls.pc + +# -- pavao-src: Samba's replacement library omits two sources that musl needs +cd /work +PAVAO_SRC=$(find "${CARGO_HOME:-/usr/local/cargo}/registry/src" \ + -type d -name "pavao-src-$PAVAO_SRC_VERSION" -print -quit) +test -n "$PAVAO_SRC" +cp -R "$PAVAO_SRC" /tmp/pavao-src +perl -0pi -e "s#( \\\"lib/replace/replace\\.c\\\",\\n)#\$1 \\\"lib/replace/closefrom.c\\\",\\n \\\"lib/replace/strptime.c\\\",\\n#" \ + /tmp/pavao-src/src/lib.rs + +cat >> Cargo.toml < /tmp/program-headers.txt; then + cat /tmp/program-headers.txt +else + status=$? + cat /tmp/program-headers.txt + exit "$status" +fi +if readelf -d "target/$TARGET/release/termscp" > /tmp/dynamic-section.txt; then + cat /tmp/dynamic-section.txt +else + status=$? + cat /tmp/dynamic-section.txt + exit "$status" +fi +if grep -q INTERP /tmp/program-headers.txt; then + exit 1 +fi +if grep -q NEEDED /tmp/dynamic-section.txt; then + exit 1 +fi diff --git a/docs/en-US/getting-started/installation.md b/docs/en-US/getting-started/installation.md index 9264b69..9d870e7 100644 --- a/docs/en-US/getting-started/installation.md +++ b/docs/en-US/getting-started/installation.md @@ -44,9 +44,28 @@ Install termscp from the official repositories: pacman -S termscp ``` +## Official binaries + +Official release binaries are published for these targets: + +- GNU/Linux: + - `x86_64-unknown-linux-musl` + - `aarch64-unknown-linux-musl` +- macOS: + - `x86_64-apple-darwin` + - `aarch64-apple-darwin` +- Windows: + - `x86_64-pc-windows-msvc` + - `aarch64-pc-windows-msvc` + +The Linux binaries, and the `.deb` package built from them, are statically +linked against musl. They have no runtime dependencies: they run on any Linux +distribution and any glibc version, with no system packages to install. + ## Requirements -The following system dependencies are required to run termscp. +The official binaries do not require these dependencies. They are needed only +to build termscp from source, for example with `cargo install termscp`: - Linux users: - libdbus-1 diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 3404e7f..beab57b 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -117,6 +117,10 @@ pacman -S termscp ### 依赖 ❗ +官方 Linux 二进制文件和 `.deb` 包静态链接了 musl,**没有任何运行时依赖**:可在任意发行版、任意 glibc 版本上运行。 + +以下依赖仅在从源码构建 termscp 时需要: + - **Linux** 用户: - libdbus-1 - pkg-config diff --git a/docs/zh-CN/getting-started/installation.md b/docs/zh-CN/getting-started/installation.md index 0432949..81be7d5 100644 --- a/docs/zh-CN/getting-started/installation.md +++ b/docs/zh-CN/getting-started/installation.md @@ -42,9 +42,25 @@ pkgin install termscp pacman -S termscp ``` +## 官方二进制文件 + +官方发布的二进制文件支持以下目标: + +- GNU/Linux: + - `x86_64-unknown-linux-musl` + - `aarch64-unknown-linux-musl` +- macOS: + - `x86_64-apple-darwin` + - `aarch64-apple-darwin` +- Windows: + - `x86_64-pc-windows-msvc` + - `aarch64-pc-windows-msvc` + +Linux 二进制文件以及由其构建的 `.deb` 包都静态链接了 musl。它们没有任何运行时依赖:可在任意 Linux 发行版、任意 glibc 版本上运行,无需安装任何系统软件包。 + ## 系统要求 -运行 termscp 需要以下系统依赖。 +官方二进制文件不需要以下依赖。它们仅在从源码构建 termscp 时才需要,例如使用 `cargo install termscp`: - Linux 用户: - libdbus-1 diff --git a/just/build.just b/just/build.just index 4983419..f9b1f26 100644 --- a/just/build.just +++ b/just/build.just @@ -17,6 +17,11 @@ build_crates_release: build_release target features="": cargo build --locked --release --target {{ target }} {{ features }} +# Build a static musl Linux release binary in a pinned Alpine container +[group('build')] +build_musl target: + dist/release/build_musl.sh {{ target }} + # Package an already-built Linux release as a Debian package [group('build')] package_deb target: diff --git a/just/code_check.just b/just/code_check.just index 961f64e..0b5c1e6 100644 --- a/just/code_check.just +++ b/just/code_check.just @@ -35,7 +35,9 @@ setup_githooks: [group('code_check')] check_install_scripts: sh -n install.sh - shellcheck install.sh + sh -n dist/release/build_musl.sh + sh -n dist/release/build_musl_container.sh + shellcheck install.sh dist/release/build_musl.sh dist/release/build_musl_container.sh @if command -v pwsh >/dev/null 2>&1; then \ pwsh -NoProfile -Command '$t = $null; $e = $null; $null = [System.Management.Automation.Language.Parser]::ParseFile("install.ps1", [ref]$t, [ref]$e); if ($e) { $e; exit 1 }'; \ else \ diff --git a/site/src/pages/install.astro b/site/src/pages/install.astro index 621f76a..87bdff9 100644 --- a/site/src/pages/install.astro +++ b/site/src/pages/install.astro @@ -72,9 +72,12 @@ const methods = [ Update anytime with termscp --update.

- Linux build deps: libdbus-1, musl: no runtime dependencies, any distribution, any glibc version. Building + from source still needs libdbus-1, pkg-config, libsmbclient. More details in the and libsmbclient. More details in the docs. diff --git a/src/system/auto_update.rs b/src/system/auto_update.rs index 5b567b6..0ec96b1 100644 --- a/src/system/auto_update.rs +++ b/src/system/auto_update.rs @@ -50,6 +50,20 @@ impl Update { self } + /// Maps a build target triple onto the target triple used to name the + /// official release assets. + /// + /// Official Linux binaries are statically linked against musl, so a + /// termscp built against glibc (for example via `cargo install`) must + /// still download the `-musl` asset. + fn map_release_target(target: &str) -> String { + if target.contains("-linux-") { + target.replace("-gnu", "-musl") + } else { + target.to_string() + } + } + /// Installs the latest available release using the configured update options. pub fn upgrade(self) -> Result { info!("Updating termscp..."); @@ -58,6 +72,7 @@ impl Update { .repo_owner("veeso") .repo_name("termscp") .bin_name("termscp") + .target(&Self::map_release_target(self_update::get_target())) .current_version(cargo_crate_version!()) .no_confirm(!self.ask_confirm) .show_download_progress(self.progress) @@ -180,6 +195,38 @@ mod test { assert_eq!(upd.progress, true); } + #[test] + fn should_map_linux_release_target_to_musl() { + assert_eq!( + Update::map_release_target("x86_64-unknown-linux-gnu"), + "x86_64-unknown-linux-musl".to_string() + ); + assert_eq!( + Update::map_release_target("aarch64-unknown-linux-gnu"), + "aarch64-unknown-linux-musl".to_string() + ); + } + + #[test] + fn should_leave_non_gnu_linux_release_target_unchanged() { + assert_eq!( + Update::map_release_target("x86_64-unknown-linux-musl"), + "x86_64-unknown-linux-musl".to_string() + ); + } + + #[test] + fn should_leave_other_platform_release_targets_unchanged() { + assert_eq!( + Update::map_release_target("aarch64-apple-darwin"), + "aarch64-apple-darwin".to_string() + ); + assert_eq!( + Update::map_release_target("x86_64-pc-windows-msvc"), + "x86_64-pc-windows-msvc".to_string() + ); + } + #[test] #[cfg(all( not(all(