From 76a692224dbad106047988b6df82b8bf98b3d4c3 Mon Sep 17 00:00:00 2001 From: Jiahao XU Date: Thu, 7 Sep 2023 00:17:43 +1000 Subject: [PATCH] Fix `detect-targets` on Linux and add CI testing (#1344) * Testing: Add `detect-targets/src/main.rs` Signed-off-by: Jiahao XU * Fix `detect-targets` linux: `guess_host_triple` could return wrong libc info so it has to check it manually instead of simply providing alternatives like other OSes. Signed-off-by: Jiahao XU * Fix `get_ld_flavor` for Alpine's gcompat glibc Its output is different from regular glibc, so we need to hardcode that particular cases. Signed-off-by: Jiahao XU * Fix detection of alpine specific musl target Signed-off-by: Jiahao XU * Add ci testing for Alpine Signed-off-by: Jiahao XU * Add CI test for detect-targets on ubuntu Signed-off-by: Jiahao XU * Refactor `get_ld_flavor` Signed-off-by: Jiahao XU * Fix shellcheck Signed-off-by: Jiahao XU * Add more CI test for ubuntu and fixed typo in it Signed-off-by: Jiahao XU * Rm distro specific target as it breaks `cargo-install` fallback Signed-off-by: Jiahao XU * Make sure all binaries are built in CI Signed-off-by: Jiahao XU * Add `package.metadata.binstall` for `detect-targets` Signed-off-by: Jiahao XU * Fix justfile Signed-off-by: Jiahao XU * Fix `detect-targets-{alpine, ubuntu}-test` Signed-off-by: Jiahao XU * Fix `detect-targets-ubuntu-test` Signed-off-by: Jiahao XU * Fix `debug-targets-ubuntu-test` Signed-off-by: Jiahao XU * `set -exuo pipefail` in `detect-targets-ubuntu-test` Signed-off-by: Jiahao XU * Simplify `detect-targets-*-test`: Use `Swatinem/rust-cache@v2` directly instead of using `just-setup` Signed-off-by: Jiahao XU * Rm dup steps in `detect-targets-ubuntu-test` Signed-off-by: Jiahao XU * Add `ls` to detect-targets-alpine-test for debugging Signed-off-by: Jiahao XU * FIx `detect-targets-alpine-test` Signed-off-by: Jiahao XU * Fix `get_ld_flavor` Signed-off-by: Jiahao XU * Fix `linux::detect_targets` on ubuntu & glibc system Signed-off-by: Jiahao XU * FIx `linux::detect_targets` glibc checking Check dynlib suffix Signed-off-by: Jiahao XU --------- Signed-off-by: Jiahao XU --- .github/workflows/ci.yml | 35 ++++++ crates/detect-targets/Cargo.toml | 3 + crates/detect-targets/src/detect.rs | 12 +- crates/detect-targets/src/detect/linux.rs | 142 ++++++++++------------ crates/detect-targets/src/main.rs | 17 +++ justfile | 15 +++ test-detect-targets-musl.sh | 20 +++ 7 files changed, 159 insertions(+), 85 deletions(-) create mode 100644 crates/detect-targets/src/main.rs create mode 100755 test-detect-targets-musl.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0238e24a..442be421 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,39 @@ jobs: CARGO_PROFILE_RELEASE_LTO: no CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 4 + detect-targets-alpine-test: + runs-on: ubuntu-latest + env: + CARGO_BUILD_TARGET: x86_64-unknown-linux-musl + TARGET: x86_64-unknown-linux-musl + steps: + - uses: actions/checkout@v3 + - name: Install x86_64-unknown-linux-musl target + run: rustup target add $TARGET + - uses: Swatinem/rust-cache@v2 + - name: Build detect-targets + run: | + pip3 install -r zigbuild-requirements.txt + cd crates/detect-targets && cargo zigbuild --target $TARGET + - name: Run test in alpine + run: | + docker run --rm \ + --mount src="$PWD/target/$TARGET/debug/detect-targets",dst=/usr/local/bin/detect-targets,type=bind \ + --mount src="$PWD/test-detect-targets-musl.sh",dst=/usr/local/bin/test.sh,type=bind \ + alpine test.sh "$TARGET" + + detect-targets-ubuntu-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + - name: Build detect-targets + run: cargo build --bin detect-targets + - name: Run test in ubuntu + run: | + set -exuo pipefail + [ "$(./target/debug/detect-targets)" = "$(printf '%s\n%s' x86_64-unknown-linux-gnu x86_64-unknown-linux-musl)" ] + # Dummy job to have a stable name for the "all tests pass" requirement tests-pass: name: Tests pass @@ -139,6 +172,8 @@ jobs: - cross-check - lint - release-builds + - detect-targets-alpine-test + - detect-targets-ubuntu-test if: always() # always run even if dependencies fail runs-on: ubuntu-latest steps: diff --git a/crates/detect-targets/Cargo.toml b/crates/detect-targets/Cargo.toml index 38a6bfce..37b5049d 100644 --- a/crates/detect-targets/Cargo.toml +++ b/crates/detect-targets/Cargo.toml @@ -20,3 +20,6 @@ windows-dll = { version = "0.4.1", features = ["windows"], default-features = fa [dev-dependencies] tokio = { version = "1.28.2", features = ["macros"], default-features = false } + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/cargo-binstall-{ target }.full.{ archive-format }" diff --git a/crates/detect-targets/src/detect.rs b/crates/detect-targets/src/detect.rs index 1850be0a..d9fb9df6 100644 --- a/crates/detect-targets/src/detect.rs +++ b/crates/detect-targets/src/detect.rs @@ -38,19 +38,21 @@ pub async fn detect_targets() -> Vec { .to_string() }); - let mut targets = vec![target]; - cfg_if! { if #[cfg(target_os = "macos")] { + let mut targets = vec![target]; targets.extend(macos::detect_alternative_targets(&targets[0]).await); + targets } else if #[cfg(target_os = "windows")] { + let mut targets = vec![target]; targets.extend(windows::detect_alternative_targets(&targets[0])); + targets } else if #[cfg(target_os = "linux")] { - targets.extend(linux::detect_alternative_targets(&targets[0]).await); + // Linux is a bit special, since the result from `guess_host_triple` + // might be wrong about whether glibc or musl is used. + linux::detect_targets(target).await } } - - targets } /// Figure out what the host target is using `rustc`. diff --git a/crates/detect-targets/src/detect/linux.rs b/crates/detect-targets/src/detect/linux.rs index ea7f4bc0..b2c2756f 100644 --- a/crates/detect-targets/src/detect/linux.rs +++ b/crates/detect-targets/src/detect/linux.rs @@ -1,12 +1,11 @@ use std::{ - fs, - path::Path, process::{Output, Stdio}, + str, }; use tokio::{process::Command, task}; -pub(super) async fn detect_alternative_targets(target: &str) -> impl Iterator { +pub(super) async fn detect_targets(target: String) -> Vec { let (prefix, postfix) = target .rsplit_once('-') .expect("unwrap: target always has a -"); @@ -37,71 +36,81 @@ pub(super) async fn detect_alternative_targets(target: &str) -> impl Iterator = [ + format!("/lib/ld-linux-{cpu_arch_suffix}.so.2"), + format!("/lib/{cpu_arch}-linux-gnu/ld-linux-{cpu_arch_suffix}.so.2"), + format!("/usr/lib/{cpu_arch}-linux-gnu/ld-linux-{cpu_arch_suffix}.so.2"), + ] + .into_iter() + .map(|p| AutoAbortHandle(tokio::spawn(is_gnu_ld(p)))) + .collect(); + + let has_glibc = async move { + for mut handle in handles { + if let Ok(true) = (&mut handle.0).await { + return true; + } } - }); - let distro_if_has_musl_dynlib = if get_ld_flavor(&format!( - "/lib/ld-musl-{cpu_arch}.so.1" - )) - .await - == Some(Libc::Musl) - { - get_distro_name().await - } else { - None - }; + false + } + .await; [ - has_glibc - .await - .unwrap_or(false) - .then(|| format!("{cpu_arch}-unknown-linux-gnu{abi}")), - distro_if_has_non_std_glibc - .await - .ok() - .flatten() - .map(|distro_name| format!("{cpu_arch}-{distro_name}-linux-gnu{abi}")), - // Fallback for Linux flavors like Alpine, which has a musl dyn libc - distro_if_has_musl_dynlib - .map(|distro_name| format!("{cpu_arch}-{distro_name}-linux-musl{abi}")), + has_glibc.then(|| format!("{cpu_arch}-unknown-linux-gnu{abi}")), Some(musl_fallback_target()), ] } - Libc::Android | Libc::Unknown => [ - Some(target.to_string()), - Some(musl_fallback_target()), - None, - None, - ], + Libc::Android | Libc::Unknown => [Some(target.clone()), Some(musl_fallback_target())], } .into_iter() .flatten() + .collect() } -async fn is_gnu_ld(cmd: &str) -> bool { - get_ld_flavor(cmd).await == Some(Libc::Gnu) +async fn is_gnu_ld(cmd: String) -> bool { + get_ld_flavor(&cmd).await == Some(Libc::Gnu) } async fn get_ld_flavor(cmd: &str) -> Option { - Command::new(cmd) + let Output { + status, + stdout, + stderr, + } = Command::new(cmd) .arg("--version") .stdin(Stdio::null()) .output() .await - .ok() - .and_then(|Output { stdout, stderr, .. }| { - Libc::parse(&stdout).or_else(|| Libc::parse(&stderr)) - }) + .ok()?; + + const ALPINE_GCOMPAT: &str = r#"This is the gcompat ELF interpreter stub. +You are not meant to run this directly. +"#; + + if status.success() { + // Executing glibc ldd or /lib/ld-linux-{cpu_arch}.so.1 will always + // succeeds. + String::from_utf8_lossy(&stdout) + .contains("GLIBC") + .then_some(Libc::Gnu) + } else if status.code() == Some(1) { + // On Alpine, executing both the gcompat glibc and the ldd and + // /lib/ld-musl-{cpu_arch}.so.1 will fail with exit status 1. + if str::from_utf8(&stdout).as_deref() == Ok(ALPINE_GCOMPAT) { + // Alpine's gcompat package will output ALPINE_GCOMPAT to stdout + Some(Libc::Gnu) + } else if String::from_utf8_lossy(&stderr).contains("musl libc") { + // Alpine/s ldd and musl dynlib will output to stderr + Some(Libc::Musl) + } else { + None + } + } else { + None + } } #[derive(Eq, PartialEq)] @@ -112,37 +121,10 @@ enum Libc { Unknown, } -impl Libc { - fn parse(output: &[u8]) -> Option { - let s = String::from_utf8_lossy(output); - if s.contains("musl libc") { - Some(Self::Musl) - } else if s.contains("GLIBC") { - Some(Self::Gnu) - } else { - None - } - } -} - -async fn get_distro_name() -> Option { - task::spawn_blocking(get_distro_name_blocking) - .await - .ok() - .flatten() -} - -fn get_distro_name_blocking() -> Option { - match fs::read_to_string("/etc/os-release") { - Ok(os_release) => os_release - .lines() - .find_map(|line| line.strip_prefix("ID=\"")?.strip_suffix('"')) - .map(ToString::to_string), - Err(_) => (Path::new("/etc/nix/nix.conf").is_file() - && ["/nix/store", "/nix/var/profiles"] - .into_iter() - .map(Path::new) - .all(Path::is_dir)) - .then_some("nixos".to_string()), +struct AutoAbortHandle(task::JoinHandle); + +impl Drop for AutoAbortHandle { + fn drop(&mut self) { + self.0.abort(); } } diff --git a/crates/detect-targets/src/main.rs b/crates/detect-targets/src/main.rs new file mode 100644 index 00000000..fbaa187f --- /dev/null +++ b/crates/detect-targets/src/main.rs @@ -0,0 +1,17 @@ +use std::io; + +use detect_targets::detect_targets; +use tokio::runtime; + +fn main() -> io::Result<()> { + let targets = runtime::Builder::new_current_thread() + .enable_all() + .build()? + .block_on(detect_targets()); + + for target in targets { + println!("{target}"); + } + + Ok(()) +} diff --git a/justfile b/justfile index 11b1d1f7..e493a0c7 100644 --- a/justfile +++ b/justfile @@ -297,6 +297,9 @@ package-prepare: build package-dir just get-output detect-wasi{{output-ext}} packages/prep -just get-output detect-wasi.dSYM packages/prep + just get-output detect-targets{{output-ext}} packages/prep + -just get-output detect-targets.dSYM packages/prep + # when https://github.com/rust-lang/cargo/pull/11384 lands, we can use # -just get-output cargo_binstall.dwp packages/prep # underscored dwp name needs to remain for debuggers to find the file properly @@ -308,6 +311,9 @@ package-prepare: build package-dir just get-output detect-wasi packages/prep -cp {{output-folder}}/deps/detect_wasi-*.dwp packages/prep/detect_wasi.dwp + just get-output detect-targets packages/prep + -cp {{output-folder}}/deps/detect_target-*.dwp packages/prep/detect_target.dwp + # underscored pdb name needs to remain for debuggers to find the file properly # read from deps because sometimes cargo doesn't copy the pdb to the output folder [windows] @@ -318,6 +324,9 @@ package-prepare: build package-dir just get-output detect-wasi.exe packages/prep -just get-output deps/detect_wasi.pdb packages/prep + just get-output detect-targets.exe packages/prep + -just get-output deps/detect_target.pdb packages/prep + # we don't get dSYM bundles for universal binaries; unsure if it's even a thing [macos] lipo-prepare: package-dir @@ -335,6 +344,11 @@ lipo-prepare: package-dir just target=x86_64h-apple-darwin get-output detect-wasi{{output-ext}} packages/prep/x64h lipo -create -output packages/prep/detect-wasi{{output-ext}} packages/prep/{arm64,x64,x64h}/detect-wasi{{output-ext}} + just target=aarch64-apple-darwin get-output detect-targets{{output-ext}} packages/prep/arm64 + just target=x86_64-apple-darwin get-output detect-targets{{output-ext}} packages/prep/x64 + just target=x86_64h-apple-darwin get-output detect-targets{{output-ext}} packages/prep/x64h + lipo -create -output packages/prep/detect-targets{{output-ext}} packages/prep/{arm64,x64,x64h}/detect-targets{{output-ext}} + rm -rf packages/prep/{arm64,x64,x64h} @@ -370,6 +384,7 @@ repackage-lipo: package-dir lipo -create -output packages/prep/{{output-filename}} packages/prep/{arm64,x64,x64h}/{{output-filename}} lipo -create -output packages/prep/detect-wasi packages/prep/{arm64,x64,x64h}/detect-wasi + lipo -create -output packages/prep/detect-targets packages/prep/{arm64,x64,x64h}/detect-targets ./packages/prep/{{output-filename}} -vV diff --git a/test-detect-targets-musl.sh b/test-detect-targets-musl.sh new file mode 100755 index 00000000..9613120f --- /dev/null +++ b/test-detect-targets-musl.sh @@ -0,0 +1,20 @@ +#!/bin/ash + +# shellcheck shell=dash + +set -exuo pipefail + +TARGET=${1?} + +[ "$(detect-targets)" = "$TARGET" ] + +apk update +apk add gcompat + +ls -lsha /lib + +GNU_TARGET=$(echo "$TARGET" | sed 's/musl/gnu/') + +[ "$(detect-targets)" = "$(printf '%s\n%s' "$GNU_TARGET" "$TARGET")" ] + +echo