From 0a95c2f09475347e4d5cead3c9ecb06d243bcb0f Mon Sep 17 00:00:00 2001 From: Etienne Cordonnier Date: Mon, 19 Jan 2026 14:24:52 +0100 Subject: [PATCH] stdbuf: support libstdbuf in same directory as stdbuf Make stdbuf search for libstdbuf first in the directory where stdbuf is running. This matches GNU coreutils behavior. Add a symlink to deps/libstdbuf in order to make it easier to run the stdbuf tests with feat_external_stdbuf enabled. Remove tests which are assuming that libstdbuf is not found. Those tests are now always failing, because the build directory contains a symlink to deps/libstdbuf and libstdbuf is always found. Fixes https://github.com/uutils/coreutils/issues/10345 Signed-off-by: Etienne Cordonnier --- src/uu/stdbuf/build.rs | 36 +++++++++++ src/uu/stdbuf/src/stdbuf.rs | 29 +++++++-- tests/by-util/test_stdbuf.rs | 118 ++++++++++++++++++++++++++++------- 3 files changed, 155 insertions(+), 28 deletions(-) diff --git a/src/uu/stdbuf/build.rs b/src/uu/stdbuf/build.rs index d844f37907a..43eea3f38b9 100644 --- a/src/uu/stdbuf/build.rs +++ b/src/uu/stdbuf/build.rs @@ -145,4 +145,40 @@ fn main() { found, "Could not find built libstdbuf library. Searched in: {possible_paths:?}." ); + + // Create a symlink to libstdbuf in the main target directory for development convenience + // This allows running stdbuf directly from the build directory (e.g., target/debug/coreutils) + // without needing to install the library to LIBSTDBUF_DIR. This is particularly useful for + // running tests and manual testing during development. + #[cfg(all(unix, feature = "feat_external_libstdbuf"))] + { + use std::path::PathBuf; + + // Get the main target directory (e.g., target/debug or target/release) + // OUT_DIR is something like target/debug/build/uu_stdbuf-/out + let out_dir_path = PathBuf::from(&out_dir); + if let Some(target_dir) = out_dir_path + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + { + let lib_filename = format!("libstdbuf{}", platform::DYLIB_EXT); + let source = target_dir.join("deps").join(&lib_filename); + let dest = target_dir.join(&lib_filename); + + // Remove old symlink if it exists (in case it points to the wrong place) + let _ = fs::remove_file(&dest); + + // Create symlink if the source exists + if source.exists() { + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + if let Err(e) = symlink(&source, &dest) { + eprintln!("Warning: Failed to create symlink for libstdbuf: {e}"); + } + } + } + } + } } diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index b18e73d5d97..31c8cd5edbe 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -171,13 +171,34 @@ fn get_preload_env(_tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { // Use the directory provided at compile time via LIBSTDBUF_DIR environment variable // This will fail to compile if LIBSTDBUF_DIR is not set, which is the desired behavior const LIBSTDBUF_DIR: &str = env!("LIBSTDBUF_DIR"); + + // Search paths in order: + // 1. Directory where stdbuf is located (program_path) + // 2. Compile-time directory from LIBSTDBUF_DIR + let mut search_paths: Vec = Vec::new(); + + // First, try to get the directory where stdbuf is running from + if let Ok(exe_path) = std::env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + search_paths.push(exe_dir.to_path_buf()); + } + } + + // Add the compile-time directory as fallback + search_paths.push(PathBuf::from(LIBSTDBUF_DIR)); + + // Search for libstdbuf in each path + for base_path in search_paths { + let path_buf = base_path.join("libstdbuf").with_extension(extension); + if path_buf.exists() { + return Ok((preload.to_owned(), path_buf)); + } + } + + // If not found in any path, report error let path_buf = PathBuf::from(LIBSTDBUF_DIR) .join("libstdbuf") .with_extension(extension); - if path_buf.exists() { - return Ok((preload.to_owned(), path_buf)); - } - Err(USimpleError::new( 1, translate!("stdbuf-error-external-libstdbuf-not-found", "path" => path_buf.display()), diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 00e117f47e4..2f234f1830a 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -25,20 +25,101 @@ fn test_permission() { .stderr_contains("Permission denied"); } -// TODO: Tests below are brittle when feat_external_libstdbuf is enabled and libstdbuf is not installed. -// Align stdbuf with GNU search order to enable deterministic testing without installation: -// 1) search for libstdbuf next to the stdbuf binary, 2) then in LIBSTDBUF_DIR, 3) then system locations. -// After implementing this, rework tests to provide a temporary symlink rather than depending on system state. - -#[cfg(feature = "feat_external_libstdbuf")] +// LD_DEBUG is not available on macOS, OpenBSD, Android, or musl +#[cfg(all( + feature = "feat_external_libstdbuf", + not(target_os = "windows"), + not(target_os = "openbsd"), + not(target_os = "macos"), + not(target_os = "android"), + not(target_env = "musl") +))] #[test] -fn test_permission_external_missing_lib() { - // When built with external libstdbuf, running stdbuf fails early if lib is not installed - new_ucmd!() - .arg("-o1") - .arg(".") - .fails_with_code(1) - .stderr_contains("External libstdbuf not found"); +fn test_stdbuf_search_order_exe_dir_first() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + // Test that stdbuf searches for libstdbuf in its own directory first, + // before checking LIBSTDBUF_DIR. + let ts = TestScenario::new(util_name!()); + let temp_dir = tempfile::tempdir().unwrap(); + let temp_path = temp_dir.path(); + + // Determine the correct library extension for this platform + let lib_extension = if cfg!(target_vendor = "apple") { + "dylib" + } else { + "so" + }; + let lib_name = format!("libstdbuf.{lib_extension}"); + + // Look for libstdbuf in the build directory deps folder + // During build, libstdbuf.so is in target/debug/deps/ or target/release/deps/ + // This allows running tests without requiring installation to a root-owned path + // ts.bin_path is the path to the binary file, so we get its parent directory first + let source_lib = ts + .bin_path + .parent() + .expect("Binary should have a parent directory") + .join("deps") + .join(&lib_name); + + // Fail test if the library doesn't exist - it should have been built + assert!( + source_lib.exists(), + "libstdbuf not found at {}. It should have been built.", + source_lib.display() + ); + + // Copy stdbuf binary to temp directory + // ts.bin_path is the full path to the coreutils binary + let stdbuf_copy = temp_path.join("stdbuf"); + fs::copy(&ts.bin_path, &stdbuf_copy).unwrap(); + + // Make the copied binary executable + let mut perms = fs::metadata(&stdbuf_copy).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&stdbuf_copy, perms).unwrap(); + + // Copy libstdbuf to the same directory as stdbuf + let lib_copy = temp_path.join(&lib_name); + fs::copy(&source_lib, &lib_copy).unwrap(); + + // Run the copied stdbuf with LD_DEBUG to verify it loads the local libstdbuf + // This proves the exe-dir search happens first, before checking LIBSTDBUF_DIR + let output = std::process::Command::new(&stdbuf_copy) + .env("LD_DEBUG", "libs") + .args(["-o0", "echo", "test_output"]) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + + // Verify the library was loaded from the temp directory (same dir as exe) + // LD_DEBUG output will show something like: + // " trying file=/tmp/.../libstdbuf.so" + let temp_dir_str = temp_path.to_string_lossy(); + let loaded_from_exe_dir = stderr + .lines() + .any(|line| line.contains(&*lib_name) && line.contains(&*temp_dir_str)); + + assert!( + loaded_from_exe_dir, + "libstdbuf should be loaded from exe directory ({}), not from LIBSTDBUF_DIR. LD_DEBUG output:\n{}", + temp_path.display(), + stderr + ); + + // The command should succeed and produce the expected output + assert!( + output.status.success(), + "stdbuf should succeed when libstdbuf is in the same directory. stderr: {stderr}" + ); + assert_eq!( + String::from_utf8_lossy(&output.stdout).trim(), + "test_output", + "stdbuf should execute echo successfully" + ); } #[cfg(not(feature = "feat_external_libstdbuf"))] @@ -51,17 +132,6 @@ fn test_no_such() { .stderr_contains("No such file or directory"); } -#[cfg(feature = "feat_external_libstdbuf")] -#[test] -fn test_no_such_external_missing_lib() { - // With external lib mode and missing installation, stdbuf fails before spawning the command - new_ucmd!() - .arg("-o1") - .arg("no_such") - .fails_with_code(1) - .stderr_contains("External libstdbuf not found"); -} - // 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.