feat(release): add x86_64 and aarch64 musl Linux targets

Extend the release matrix to two statically-linked musl variants so
Alpine and other musl-based Linux hosts can install without glibc.
Homebrew and the Docker image remain glibc-only.

- release.yml: add x86_64-unknown-linux-musl (ubuntu-24.04) and
  aarch64-unknown-linux-musl (ubuntu-24.04-arm) matrix rows with
  musl-tools, CC_*_musl, CARGO_TARGET_*_LINKER, and LIBZ_SYS_STATIC
- Cargo.toml: enable git2 vendored-libgit2 so libgit2 compiles from
  source for every target (needed because musl cannot link against
  Ubuntu's glibc-built libgit2-dev)
- install.sh: check `ldd --version` for "musl" and rewrite the target
  from -gnu to -musl so Alpine users get the right tarball
- upgrade.rs: add detect_linux_libc() / parse_ldd_libc() helper and
  route detect_target() Linux arms through it, with unit tests
  covering glibc, musl, empty, and unknown output
- tests/it: extend target regex in the dry-run snapshot filter

Ubuntu 24.04 is required for the musl runner: 22.04 ships musl 1.2.2
which SIGSEGVs statically-linked x86_64 test binaries at startup.
Confirmed against graphviz-sys CI before landing here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-18 01:00:51 -04:00
parent 5335f8bcb4
commit 828d686a6f
No known key found for this signature in database
5 changed files with 101 additions and 6 deletions

View file

@ -42,11 +42,27 @@ jobs:
runner: ubuntu-latest
- target: aarch64-unknown-linux-gnu
runner: ubuntu-22.04-arm
- target: x86_64-unknown-linux-musl
runner: ubuntu-24.04
musl: true
- target: aarch64-unknown-linux-musl
runner: ubuntu-24.04-arm
musl: true
env:
CC_x86_64_unknown_linux_musl: musl-gcc
CC_aarch64_unknown_linux_musl: musl-gcc
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: musl-gcc
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: musl-gcc
LIBZ_SYS_STATIC: "1"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install musl toolchain
if: matrix.musl
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Set up Rust
run: |
rustup toolchain install stable --profile minimal --no-self-update

View file

@ -37,7 +37,7 @@ tar = "0.4"
cli-table = { version = "0.5", default-features = false }
console = "0.15"
dialoguer = "0.12"
git2 = { version = "0.20", default-features = false }
git2 = { version = "0.20", default-features = false, features = ["vendored-libgit2"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
tracing-appender = "0.2"

View file

@ -57,6 +57,9 @@ case "$OS" in
aarch64) TARGET="aarch64-unknown-linux-gnu" ;;
*) error "Unsupported Linux architecture: $ARCH. Supported: x86_64, aarch64" ;;
esac
if ldd --version 2>&1 | grep -qi musl; then
TARGET="${TARGET%-gnu}-musl"
fi
;;
*)
error "Unsupported OS: $OS. Supported platforms: macOS (Apple Silicon), Linux (x86_64, aarch64)"

View file

@ -277,12 +277,54 @@ async fn select_backend() -> Backend {
fn detect_target() -> Result<&'static str> {
match (std::env::consts::OS, std::env::consts::ARCH) {
("macos", "aarch64") => Ok("aarch64-apple-darwin"),
("linux", "x86_64") => Ok("x86_64-unknown-linux-gnu"),
("linux", "aarch64") => Ok("aarch64-unknown-linux-gnu"),
("linux", "x86_64") => Ok(match detect_linux_libc() {
Libc::Musl => "x86_64-unknown-linux-musl",
Libc::Gnu => "x86_64-unknown-linux-gnu",
}),
("linux", "aarch64") => Ok(match detect_linux_libc() {
Libc::Musl => "aarch64-unknown-linux-musl",
Libc::Gnu => "aarch64-unknown-linux-gnu",
}),
(os, arch) => bail!("unsupported platform: {os}/{arch}"),
}
}
#[derive(Debug, PartialEq, Eq)]
enum Libc {
Gnu,
Musl,
}
/// Parse `ldd --version` output to determine the host libc flavor.
///
/// glibc's ldd writes to stdout ("ldd (Ubuntu GLIBC 2.35...)");
/// musl's ldd writes to stderr ("musl libc (x86_64)\nVersion 1.2.4").
/// Callers concatenate both streams before passing in.
fn parse_ldd_libc(output: &str) -> Libc {
if output.to_ascii_lowercase().contains("musl") {
Libc::Musl
} else {
Libc::Gnu
}
}
fn detect_linux_libc() -> Libc {
#[expect(
clippy::disallowed_methods,
reason = "one-shot libc detection at upgrade startup; async overhead is unwarranted"
)]
let result = std::process::Command::new("ldd").arg("--version").output();
let Ok(output) = result else {
return Libc::Gnu;
};
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
parse_ldd_libc(&combined)
}
// ── Version helpers ────────────────────────────────────────────────────────
fn parse_version_from_tag(tag: &str) -> Result<Version> {
@ -690,15 +732,49 @@ mod tests {
let result = detect_target();
// We can only assert it succeeds on known CI platforms
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
assert_eq!(result.unwrap(), "x86_64-unknown-linux-gnu");
let got = result.unwrap();
assert!(
got == "x86_64-unknown-linux-gnu" || got == "x86_64-unknown-linux-musl",
"got {got}"
);
} else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
assert_eq!(result.unwrap(), "aarch64-apple-darwin");
} else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") {
assert_eq!(result.unwrap(), "aarch64-unknown-linux-gnu");
let got = result.unwrap();
assert!(
got == "aarch64-unknown-linux-gnu" || got == "aarch64-unknown-linux-musl",
"got {got}"
);
}
// On other platforms it would return an error, which is fine
}
// -- Libc parsing --
#[test]
fn parse_ldd_libc_detects_glibc() {
assert_eq!(
parse_ldd_libc(
"ldd (Ubuntu GLIBC 2.35-0ubuntu3.4) 2.35\nCopyright (C) 2022 Free Software Foundation, Inc."
),
Libc::Gnu,
);
}
#[test]
fn parse_ldd_libc_detects_musl() {
assert_eq!(
parse_ldd_libc("musl libc (x86_64)\nVersion 1.2.4\nDynamic Program Loader"),
Libc::Musl,
);
}
#[test]
fn parse_ldd_libc_defaults_to_gnu_on_unknown_output() {
assert_eq!(parse_ldd_libc(""), Libc::Gnu);
assert_eq!(parse_ldd_libc("some unrelated output"), Libc::Gnu);
}
// -- Version parsing --
#[test]

View file

@ -168,7 +168,7 @@ esac
"[VERSION]".to_string(),
));
filters.push((
"(aarch64-apple-darwin|x86_64-unknown-linux-gnu|aarch64-unknown-linux-gnu)".to_string(),
"(aarch64-apple-darwin|x86_64-unknown-linux-gnu|aarch64-unknown-linux-gnu|x86_64-unknown-linux-musl|aarch64-unknown-linux-musl)".to_string(),
"[TARGET]".to_string(),
));