Skip to content

Commit ab70974

Browse files
committed
uucore: add portable autoconf-aware branding heuristic; update mkdir tests
Default to honest '(uutils coreutils)' branding. Emit '(GNU coreutils)' only for likely autoconf probes of 'mkdir --version' when env/dir hints are present (and linux-only /proc best-effort). Normalize to basename already in place. Adjust tests to simulate autoconf env for compatibility assertion. Clippy and fmt clean; full test suite passes locally.
1 parent 58138f0 commit ab70974

2 files changed

Lines changed: 176 additions & 11 deletions

File tree

src/uucore/src/lib/lib.rs

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ use nix::sys::signal::{
127127
SaFlags, SigAction, SigHandler::SigDfl, SigSet, Signal::SIGBUS, Signal::SIGSEGV, sigaction,
128128
};
129129
use std::borrow::Cow;
130+
use std::env;
130131
use std::ffi::{OsStr, OsString};
132+
#[cfg(target_os = "linux")]
133+
use std::fs::{read_link, read_to_string};
134+
use std::io::IsTerminal;
131135
use std::io::{BufRead, BufReader};
132136
use std::iter;
133137
#[cfg(unix)]
@@ -211,19 +215,16 @@ macro_rules! bin {
211215
};
212216
}
213217

214-
/// Generate the version string for clap.
218+
/// Generate the version string for clap with runtime autoconf detection.
215219
///
216220
/// The generated string has the format `(<project name>) <version>`, for
217-
/// example: "(GNU coreutils) 0.30.0". clap will then prefix it with the util name.
221+
/// example: "(GNU coreutils) 0.30.0" when running under autoconf or
222+
/// "(uutils coreutils) 0.30.0" normally. clap will then prefix it with the util name.
218223
#[macro_export]
219224
macro_rules! crate_version {
220-
() => {{
221-
const BRAND: &str = match option_env!("UUTILS_VERSION_BRAND") {
222-
Some(v) => v,
223-
None => "uutils coreutils",
224-
};
225-
$crate::brand_version_static(BRAND, env!("CARGO_PKG_VERSION"))
226-
}};
225+
() => {
226+
$crate::runtime_version_string(env!("CARGO_PKG_VERSION"))
227+
};
227228
}
228229

229230
/// Generate the usage string for clap.
@@ -233,11 +234,143 @@ macro_rules! crate_version {
233234
/// all occurrences of `{}` with the execution phrase and returns the resulting
234235
/// `String`. It does **not** support more advanced formatting features such
235236
/// as `{0}`.
237+
/// Return true if the current directory (or parents) looks like a configure tree
238+
fn looks_like_configure_dir() -> bool {
239+
let mut current_dir = env::current_dir().ok();
240+
while let Some(dir) = current_dir {
241+
if dir.join("configure").exists()
242+
|| dir.join("configure.ac").exists()
243+
|| dir.join("configure.in").exists()
244+
|| dir.join("aclocal.m4").exists()
245+
|| dir.join("Makefile.in").exists()
246+
|| dir.join("config.log").exists()
247+
{
248+
return true;
249+
}
250+
current_dir = dir.parent().map(|p| p.to_path_buf());
251+
}
252+
false
253+
}
254+
255+
/// Return true if environment variables suggest an autoconf/automake context
256+
fn looks_like_autoconf_env() -> bool {
257+
// Common autoconf/automake indicators
258+
const VARS: [&str; 8] = [
259+
"ac_cv_path_mkdir",
260+
"ac_cv_prog_mkdir",
261+
"AUTOCONF",
262+
"AUTOMAKE",
263+
"CONFIG_SHELL",
264+
"ACLOCAL_PATH",
265+
"ac_configure_args",
266+
"ac_srcdir",
267+
];
268+
if VARS.iter().any(|v| env::var(v).is_ok()) {
269+
return true;
270+
}
271+
if let Ok(makeflags) = env::var("MAKEFLAGS") {
272+
if makeflags.contains("am__api_version") {
273+
return true;
274+
}
275+
}
276+
false
277+
}
278+
279+
/// Best-effort Linux-only detection via /proc of a configure parent
280+
#[cfg(target_os = "linux")]
281+
fn looks_like_linux_configure_parent() -> bool {
282+
if let Ok(ppid) = env::var("PPID").or_else(|_| {
283+
read_to_string("/proc/self/stat")
284+
.ok()
285+
.and_then(|content| content.split_whitespace().nth(3).map(|s| s.to_string()))
286+
.ok_or("no ppid")
287+
}) {
288+
if let Ok(ppid_num) = ppid.parse::<u32>() {
289+
if let Ok(cmdline) = read_to_string(format!("/proc/{}/cmdline", ppid_num)) {
290+
let cmdline = cmdline.replace('\0', " ");
291+
if cmdline.contains("configure") || cmdline.contains("autoconf") {
292+
return true;
293+
}
294+
}
295+
if let Ok(exe) = read_link(format!("/proc/{}/exe", ppid_num)) {
296+
if let Some(name) = exe.file_name().and_then(|n| n.to_str()) {
297+
if name == "configure" || name.contains("autoconf") {
298+
return true;
299+
}
300+
}
301+
}
302+
}
303+
}
304+
false
305+
}
306+
307+
/// Return true if the call pattern likely matches an autoconf mkdir probe
308+
fn looks_like_mkdir_version_probe() -> bool {
309+
// Determine where util args start, mirroring UTIL_NAME logic
310+
let base_index = usize::from(get_utility_is_second_arg());
311+
let is_man = usize::from(ARGV[base_index].eq("manpage"));
312+
let argv_index = base_index + is_man; // index of util in ARGV
313+
let after = ARGV.iter().skip(argv_index + 1).collect::<Vec<_>>();
314+
// Version-only flags and non-interactive stdout
315+
let is_version_only = after.len() == 1 && (after[0] == "--version" || after[0] == "-V");
316+
let stdout_is_tty = IsTerminal::is_terminal(&std::io::stdout());
317+
is_version_only && !stdout_is_tty
318+
}
319+
320+
/// Decide if we should emit GNU branding for compatibility
321+
fn should_emit_gnu_brand() -> bool {
322+
// Only for mkdir
323+
if util_name() != "mkdir" {
324+
return false;
325+
}
326+
if !looks_like_mkdir_version_probe() {
327+
return false;
328+
}
329+
if looks_like_autoconf_env() || looks_like_configure_dir() {
330+
return true;
331+
}
332+
#[cfg(target_os = "linux")]
333+
if looks_like_linux_configure_parent() {
334+
return true;
335+
}
336+
false
337+
}
338+
339+
/// Get the appropriate brand based on context
340+
fn get_runtime_brand() -> String {
341+
// First check for explicit environment variable override (compile-time)
342+
if let Some(brand) = option_env!("UUTILS_VERSION_BRAND") {
343+
return brand.to_string();
344+
}
345+
346+
// Check for runtime environment variable override
347+
if let Ok(brand) = env::var("UUTILS_VERSION_BRAND") {
348+
return brand;
349+
}
350+
351+
// If likely under autoconf probe for mkdir, use GNU branding for compatibility
352+
if should_emit_gnu_brand() {
353+
return "GNU coreutils".to_string();
354+
}
355+
356+
// Default to honest uutils branding
357+
"uutils coreutils".to_string()
358+
}
359+
236360
pub fn brand_version_static(brand: &'static str, version: &'static str) -> &'static str {
237-
let s = format!("({}) {}", brand, version);
361+
let s = format!("({brand}) {version}");
238362
Box::leak(s.into_boxed_str())
239363
}
240364

365+
/// Generate version string with runtime autoconf detection
366+
pub fn runtime_version_string(_version: &'static str) -> &'static str {
367+
static VERSION_CACHE: LazyLock<String> = LazyLock::new(|| {
368+
let brand = get_runtime_brand();
369+
format!("({brand}) {}", env!("CARGO_PKG_VERSION"))
370+
});
371+
VERSION_CACHE.as_str()
372+
}
373+
241374
pub fn format_usage(s: &str) -> String {
242375
let s = s.replace('\n', &format!("\n{}", " ".repeat(7)));
243376
s.replace("{}", crate::execution_phrase())

tests/by-util/test_mkdir.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,45 @@ use uutests::util_name;
2222
#[test]
2323
fn test_version_format_autoconf_compatibility() {
2424
// Test that --version output contains "(GNU coreutils)" for autoconf compatibility
25+
// when autoconf-like environment is present (pre-2.72 detection style)
2526
// See: https://github.com/uutils/coreutils/issues/8880
26-
new_ucmd!()
27+
let mut cmd = new_ucmd!();
28+
cmd.env("ac_cv_path_mkdir", "/usr/bin/mkdir")
29+
.arg("--version")
30+
.succeeds()
31+
.stdout_contains("(GNU coreutils)");
32+
}
33+
34+
#[test]
35+
fn test_runtime_autoconf_detection_with_env_var() {
36+
// Test explicit environment variable override
37+
let mut cmd = new_ucmd!();
38+
cmd.env("UUTILS_VERSION_BRAND", "GNU coreutils")
39+
.arg("--version")
40+
.succeeds()
41+
.stdout_contains("(GNU coreutils)");
42+
}
43+
44+
#[test]
45+
fn test_runtime_autoconf_detection_with_configure_env() {
46+
// Test autoconf environment variable detection
47+
let mut cmd = new_ucmd!();
48+
cmd.env("ac_cv_path_mkdir", "/usr/bin/mkdir")
2749
.arg("--version")
2850
.succeeds()
2951
.stdout_contains("(GNU coreutils)");
3052
}
3153

54+
#[test]
55+
fn test_default_branding_without_autoconf() {
56+
// Test that without autoconf context, we show uutils branding
57+
// (This might show GNU if autoconf is detected, which is fine)
58+
let result = new_ucmd!().arg("--version").succeeds();
59+
let output = result.stdout_str();
60+
// Should contain either uutils or GNU branding (GNU if autoconf detected)
61+
assert!(output.contains("(uutils coreutils)") || output.contains("(GNU coreutils)"));
62+
}
63+
3264
#[test]
3365
fn test_invalid_arg() {
3466
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);

0 commit comments

Comments
 (0)