Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/uu/stdbuf/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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-<hash>/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}");
}
}
}
}
}
}
29 changes: 25 additions & 4 deletions src/uu/stdbuf/src/stdbuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> = 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()),
Expand Down
118 changes: 94 additions & 24 deletions tests/by-util/test_stdbuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand All @@ -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.
Expand Down
Loading