coreutils/tests/by-util/test_stdbuf.rs
Etienne Cordonnier 35634b46a0 stdbuf: fix cross-compilation
Summary:

Partial fix for https://github.com/uutils/coreutils/issues/6591

The current code declare libstdbuf as a build-dependency of stdbuf as a
workaround to enforce that libstdbuf is compiled before stdbuf. This breaks
cross-compilation, because build-dependencies were compiled for the host
architecture, and not for the target architecture.

The reason this workaround is necessary is that bindeps is available only in nightly at the moment:
https://rust-lang.github.io/rfcs/3028-cargo-binary-dependencies.html

This commit replaces the "build-dependency" workaround with another workaround:
calling cargo manually to build libstdbuf in the build.rs of stdbuf, in order to ensure that libstdbuf is built before stdbuf.

Changes:

- Removed cpp/cpp_build dependencies:

The cpp, cpp_build, and related dependencies were removed because they made cross-compilation in a build.rs file very complex, since you need
to pass proper CXX env variables for cross-compilation, whereas cross-compiling rust code using cargo is quite simple.
Provided Rust implementations for getting stdin, stdout, and stderr pointers.
Switched from C++/cpp macro-based initialization to using the Rust ctor crate for library initialization.

- Remove "feat_require_crate_cpp" which is not needed any more, since stdbuf was the only utility using the cpp crate.

Tests:

This commit fixes e.g. this test:
cross test --target aarch64-unknown-linux-gnu --features stdbuf test_stdbuf::test_libstdbuf_preload -- --nocapture

- The "i686" build of stdbuf was also broken (stdbuf 32 bits, but libstdbuf 64 bits) and test_stdbuf::test_libstdbuf_preload of the i686 builds in github CI serves as regression
test for this issue, no need to add a cross-rs test for aarch64.
- The x86_64 musl build of stdbuf was also broken and was passing tests in CI only because it was compiled with the wrong libc (glibc instead of musl)

Signed-off-by: Etienne Cordonnier <ecordonnier@snap.com>
2025-06-02 10:55:57 +02:00

218 lines
8 KiB
Rust

// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore dyld dylib setvbuf
use uutests::new_ucmd;
#[cfg(not(target_os = "windows"))]
use uutests::util::TestScenario;
use uutests::util_name;
#[test]
fn invalid_input() {
new_ucmd!().arg("-/").fails_with_code(125);
}
#[test]
fn test_permission() {
new_ucmd!()
.arg("-o1")
.arg(".")
.fails_with_code(126)
.stderr_contains("Permission denied");
}
#[test]
fn test_no_such() {
new_ucmd!()
.arg("-o1")
.arg("no_such")
.fails_with_code(127)
.stderr_contains("No such file or directory");
}
// Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target
// does not provide musl-compiled system utilities (like head), leading to dynamic linker errors
// when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD.
#[cfg(all(
not(target_os = "windows"),
not(target_os = "freebsd"),
not(target_os = "openbsd"),
not(all(target_arch = "x86_64", target_env = "musl"))
))]
#[test]
fn test_stdbuf_unbuffered_stdout() {
// This is a basic smoke test
// Note: This test only verifies that stdbuf does not crash and that output is passed through as expected
// for simple, short-lived commands. It does not guarantee that buffering is actually modified or that
// libstdbuf is loaded and functioning correctly.
new_ucmd!()
.args(&["-o0", "head"])
.pipe_in("The quick brown fox jumps over the lazy dog.")
.succeeds()
.stdout_is("The quick brown fox jumps over the lazy dog.");
}
// Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target
// does not provide musl-compiled system utilities (like head), leading to dynamic linker errors
// when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD.
#[cfg(all(
not(target_os = "windows"),
not(target_os = "freebsd"),
not(target_os = "openbsd"),
not(all(target_arch = "x86_64", target_env = "musl"))
))]
#[test]
fn test_stdbuf_line_buffered_stdout() {
// Note: This test only verifies that stdbuf does not crash and that output is passed through as expected
// for simple, short-lived commands. It does not guarantee that buffering is actually modified or that
// libstdbuf is loaded and functioning correctly.
new_ucmd!()
.args(&["-oL", "head"])
.pipe_in("The quick brown fox jumps over the lazy dog.")
.succeeds()
.stdout_is("The quick brown fox jumps over the lazy dog.");
}
#[cfg(not(target_os = "windows"))]
#[test]
fn test_stdbuf_no_buffer_option_fails() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.args(&["head"])
.fails()
.stderr_contains("the following required arguments were not provided:");
}
// Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target
// does not provide musl-compiled system utilities (like tail), leading to dynamic linker errors
// when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD.
#[cfg(all(
not(target_os = "windows"),
not(target_os = "freebsd"),
not(target_os = "openbsd"),
not(all(target_arch = "x86_64", target_env = "musl"))
))]
#[test]
fn test_stdbuf_trailing_var_arg() {
new_ucmd!()
.args(&["-i", "1024", "tail", "-1"])
.pipe_in("The quick brown fox\njumps over the lazy dog.")
.succeeds()
.stdout_is("jumps over the lazy dog.");
}
#[cfg(not(target_os = "windows"))]
#[test]
fn test_stdbuf_line_buffering_stdin_fails() {
new_ucmd!()
.args(&["-i", "L", "head"])
.fails()
.usage_error("line buffering stdin is meaningless");
}
#[cfg(not(target_os = "windows"))]
#[test]
fn test_stdbuf_invalid_mode_fails() {
let options = ["--input", "--output", "--error"];
for option in &options {
new_ucmd!()
.args(&[*option, "1024R", "head"])
.fails_with_code(125)
.usage_error("invalid mode '1024R': Value too large for defined data type");
new_ucmd!()
.args(&[*option, "1Y", "head"])
.fails_with_code(125)
.stderr_contains("stdbuf: invalid mode '1Y': Value too large for defined data type");
#[cfg(target_pointer_width = "32")]
{
new_ucmd!()
.args(&[*option, "5GB", "head"])
.fails_with_code(125)
.stderr_contains(
"stdbuf: invalid mode '5GB': Value too large for defined data type",
);
}
}
}
// macos uses DYLD_PRINT_LIBRARIES, not LD_DEBUG, so disable on macos at the moment.
// On modern Android (Bionic, API 37+), LD_DEBUG is supported and behaves similarly to glibc.
// On older Android versions (Bionic, API < 37), LD_DEBUG uses integer values instead of strings
// and is sometimes disabled. Disable test on Android for now.
// musl libc dynamic loader does not support LD_DEBUG, so disable on musl targets as well.
#[cfg(all(
not(target_os = "windows"),
not(target_os = "openbsd"),
not(target_os = "macos"),
not(target_os = "android"),
not(target_env = "musl")
))]
#[test]
fn test_libstdbuf_preload() {
use std::process::Command;
// Run a simple program with LD_DEBUG=symbols to verify that libstdbuf is loaded correctly
// and that there are no architecture mismatches when preloading the library.
// Note: This does not check which setvbuf implementation is used, as our libstdbuf does not override setvbuf.
// for https://github.com/uutils/coreutils/issues/6591
let scene = TestScenario::new(util_name!());
let coreutils_bin = &scene.bin_path;
// Test with our own echo (should have the correct architecture even when cross-compiled using cross-rs,
// in which case the "system" echo will be the host architecture)
let uutils_echo_cmd = format!(
"LD_DEBUG=symbols {} stdbuf -oL {} echo test 2>&1",
coreutils_bin.display(),
coreutils_bin.display()
);
let uutils_output = Command::new("sh")
.arg("-c")
.arg(&uutils_echo_cmd)
.output()
.expect("Failed to run uutils echo test");
let uutils_debug = String::from_utf8_lossy(&uutils_output.stdout);
// Check if libstdbuf.so / libstdbuf.dylib is in the lookup path.
// With GLIBC, the log should contain something like:
// "symbol=setvbuf; lookup in file=/tmp/.tmp0mfmCg/libstdbuf.so [0]"
// With FreeBSD dynamic loader, the log should contain something like:
// cspell:disable-next-line
// "calling init function for /tmp/.tmpu11rhP/libstdbuf.so at ..."
let libstdbuf_in_path = if cfg!(target_os = "freebsd") {
uutils_debug
.lines()
.any(|line| line.contains("calling init function") && line.contains("libstdbuf"))
} else {
uutils_debug.contains("symbol=setvbuf")
&& uutils_debug.contains("lookup in file=")
&& uutils_debug.contains("libstdbuf")
};
// Check for lack of architecture mismatch error. The potential error message with GLIBC is:
// cspell:disable-next-line
// "ERROR: ld.so: object '/tmp/.tmpCLq8jl/libstdbuf.so' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored."
let arch_mismatch_line = uutils_debug
.lines()
.find(|line| line.contains("cannot be preloaded"));
println!("LD_DEBUG output: {}", uutils_debug);
let no_arch_mismatch = arch_mismatch_line.is_none();
println!("libstdbuf in lookup path: {}", libstdbuf_in_path);
println!("No architecture mismatch: {}", no_arch_mismatch);
if let Some(error_line) = arch_mismatch_line {
println!("Architecture mismatch error: {}", error_line);
}
assert!(
libstdbuf_in_path,
"libstdbuf should be in lookup path with uutils echo"
);
assert!(
no_arch_mismatch,
"uutils echo should not show architecture mismatch"
);
}