From f2f6e93b8c9a44902888ff1dfff2907bf9c3d216 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Sun, 14 Dec 2025 00:29:46 +0900 Subject: [PATCH 1/7] GnuTests: Split online process to a script --- .github/workflows/GnuTests.yml | 54 ++++------------------------------ util/build-gnu.sh | 18 +++--------- util/fetch-gnu.sh | 9 ++++++ util/why-skip.md | 2 -- 4 files changed, 18 insertions(+), 65 deletions(-) create mode 100755 util/fetch-gnu.sh diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index d4627af27..c8070f629 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -2,7 +2,7 @@ name: GnuTests # spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem # spell-checker:ignore (jargon) submodules devel -# spell-checker:ignore (libs/utils) autopoint chksum getenforce gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e +# spell-checker:ignore (libs/utils) autopoint chksum dpkg getenforce gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e # spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic # spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay # spell-checker:ignore (vars) FILESET SUBDIRS XPASS @@ -42,16 +42,6 @@ jobs: with: path: 'uutils' persist-credentials: false - - name: Extract GNU version from build-gnu.sh - id: gnu-version - run: | - GNU_VERSION=$(grep '^release_tag_GNU=' uutils/util/build-gnu.sh | cut -d'"' -f2) - if [ -z "$GNU_VERSION" ]; then - echo "Error: Failed to extract GNU version from build-gnu.sh" - exit 1 - fi - echo "REPO_GNU_REF=${GNU_VERSION}" >> $GITHUB_ENV - echo "Extracted GNU version: ${GNU_VERSION}" - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -60,20 +50,7 @@ jobs: with: workspaces: "./uutils -> target" - name: Checkout code (GNU coreutils) - uses: actions/checkout@v6 - with: - repository: 'coreutils/coreutils' - path: 'gnu' - ref: ${{ env.REPO_GNU_REF }} - submodules: false - persist-credentials: false - - name: Override submodule URL and initialize submodules - # Use github instead of upstream git server - run: | - git submodule sync --recursive - git config submodule.gnulib.url https://github.com/coreutils/gnulib.git - git submodule update --init --recursive --depth 1 - working-directory: gnu + run: (mkdir -p gnu && cd gnu && bash ../uutils/util/fetch-gnu.sh) #### Build environment setup - name: Install dependencies @@ -83,6 +60,8 @@ jobs: sudo apt-get update ## Check that build-gnu.sh works on the non SELinux system by installing libselinux only on lima sudo apt-get install -y autopoint gperf gdb python3-pyinotify valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev attr quilt + curl http://launchpadlibrarian.net/831710181/automake_1.18.1-3_all.deb > automake-1.18.deb + sudo dpkg -i --force-depends automake-1.18.deb - name: Add various locales shell: bash run: | @@ -206,16 +185,6 @@ jobs: with: path: 'uutils' persist-credentials: false - - name: Extract GNU version from build-gnu.sh - id: gnu-version-selinux - run: | - GNU_VERSION=$(grep '^release_tag_GNU=' uutils/util/build-gnu.sh | cut -d'"' -f2) - if [ -z "$GNU_VERSION" ]; then - echo "Error: Failed to extract GNU version from build-gnu.sh" - exit 1 - fi - echo "REPO_GNU_REF=${GNU_VERSION}" >> $GITHUB_ENV - echo "Extracted GNU version: ${GNU_VERSION}" - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -224,20 +193,7 @@ jobs: with: workspaces: "./uutils -> target" - name: Checkout code (GNU coreutils) - uses: actions/checkout@v6 - with: - repository: 'coreutils/coreutils' - path: 'gnu' - ref: ${{ env.REPO_GNU_REF }} - submodules: false - persist-credentials: false - - name: Override submodule URL and initialize submodules - # Use github instead of upstream git server - run: | - git submodule sync --recursive - git config submodule.gnulib.url https://github.com/coreutils/gnulib.git - git submodule update --init --recursive --depth 1 - working-directory: gnu + run: (mkdir -p gnu && cd gnu && bash ../uutils/util/fetch-gnu.sh) #### Lima build environment setup - name: Setup Lima diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 626400d6a..8b0fb957e 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -34,18 +34,13 @@ path_GNU="$("${READLINK}" -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")" ### -release_tag_GNU="v9.9" - # check if the GNU coreutils has been cloned, if not print instructions -# note: the ${path_GNU} might already exist, so we check for the .git directory -if test ! -d "${path_GNU}/.git"; then +# note: the ${path_GNU} might already exist, so we check for the configure +if test ! -f "${path_GNU}/configure"; then echo "Could not find the GNU coreutils (expected at '${path_GNU}')" echo "Download them to the expected path:" - echo " git clone --recurse-submodules https://github.com/coreutils/coreutils.git \"${path_GNU}\"" - echo "Afterwards, checkout the latest release tag:" - echo " cd \"${path_GNU}\"" - echo " git fetch --all --tags" - echo " git checkout tags/${release_tag_GNU}" + echo " (cd '${path_GNU}' && fetch-gnu.sh ) " + echo "You can edit fetch-gnu.sh to change the tag" exit 1 fi @@ -131,8 +126,6 @@ if test -f gnu-built; then else # Disable useless checks "${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk - "${SED}" -i '/^wget.*/d' bootstrap.conf # wget is used to DL po. Remove the dep. - ./bootstrap --skip-po # Use CFLAGS for best build time since we discard GNU coreutils CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ --enable-single-binary=symlinks \ @@ -175,9 +168,6 @@ grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r "${SED}" -i "s|\$abs_path_dir # Different message "${SED}" -i "s|coreutils: unknown program 'blah'|blah: function/utility not found|" tests/misc/coreutils.sh -# Remove hfs dependency (should be merged to upstream) -"${SED}" -i -e "s|hfsplus|ext4 -O casefold|" -e "s|cd mnt|rm -d mnt/lost+found;chattr +F mnt;cd mnt|" tests/mv/hardlink-case.sh - # Use the system coreutils where the test fails due to error in a util that is not the one being tested "${SED}" -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh diff --git a/util/fetch-gnu.sh b/util/fetch-gnu.sh new file mode 100755 index 000000000..927d85949 --- /dev/null +++ b/util/fetch-gnu.sh @@ -0,0 +1,9 @@ +#!/bin/bash -e +ver="9.9" +repo=https://github.com/coreutils/coreutils +curl -L "${repo}/releases/download/v${ver}/coreutils-${ver}.tar.xz" | tar --strip-components=1 -xJf - + +# backport from coreutils > 9.9 +curl ${repo}/raw/refs/heads/master/tests/mv/hardlink-case.sh > tests/mv/hardlink-case.sh +curl ${repo}/raw/refs/heads/master/tests/mkdir/writable-under-readonly.sh > tests/mkdir/writable-under-readonly.sh +curl ${repo}/raw/refs/heads/master/tests/cp/cp-mv-enotsup-xattr.sh > tests/cp/cp-mv-enotsup-xattr.sh #spell-checker:disable-line diff --git a/util/why-skip.md b/util/why-skip.md index 75f14c6f5..f471ec09b 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -31,5 +31,3 @@ = Disabled. Enabled at GNU coreutils > 9.9 = * tests/misc/tac-continue.sh -* tests/mkdir/writable-under-readonly.sh -* tests/cp/cp-mv-enotsup-xattr.sh From a8e169ebffb3ee8be8c73c475282db71c7c3b502 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 20 Dec 2025 00:35:48 +0900 Subject: [PATCH 2/7] DEVELOPMENT.md: Remove a wrong desc (#9717) --- DEVELOPMENT.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f9636625b..4f885e085 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -244,8 +244,6 @@ DEBUG=1 bash util/run-gnu-test.sh tests/misc/sm3sum.pl ***Tip:*** First time you run `bash util/build-gnu.sh` command, it will provide instructions on how to checkout GNU coreutils repository at the correct release tag. Please follow those instructions and when done, run `bash util/build-gnu.sh` command again. -Note that GNU test suite relies on individual utilities (not the multicall binary). - You also need to install [quilt](https://savannah.nongnu.org/projects/quilt), a tool used to manage a stack of patches for modifying GNU tests. On FreeBSD, you need to install packages for GNU coreutils and sed (used in shell scripts instead of system commands): From 16f73503b33d5478562769f944538266b95d2184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=E1=BA=A3=20th=E1=BA=BF=20gi=E1=BB=9Bi=20l=C3=A0=20Rust?= <90588855+naoNao89@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:05:31 +0700 Subject: [PATCH 3/7] feat(date): add locale-aware hour format detection (#9654) Implement locale-aware 12-hour vs 24-hour time formatting that respects LC_TIME environment variable preferences, matching GNU coreutils 9.9 behavior. - Add locale.rs module with nl_langinfo() FFI for POSIX locale queries - Detect locale hour format preference (12-hour vs 24-hour) - Use OnceLock caching for performance (99% faster on repeated calls) - Update default format to use locale-aware formatting - Add integration tests for C and en_US locales Fixes compatibility with GNU coreutils date-locale-hour.sh test. --- .../cspell.dictionaries/jargon.wordlist.txt | 4 + src/uu/date/src/date.rs | 4 +- src/uu/date/src/locale.rs | 167 ++++++++++++++++++ tests/by-util/test_date.rs | 55 ++++++ 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/uu/date/src/locale.rs diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index d2febb772..bd29bd246 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -76,6 +76,7 @@ iflag iflags kibi kibibytes +langinfo libacl lcase listxattr @@ -129,6 +130,7 @@ semiprimes setcap setfacl setfattr +setlocale shortcode shortcodes siginfo @@ -163,6 +165,8 @@ xattrs xpass # * abbreviations +AMPM +ampm consts deps dev diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 45bceaec3..93c085466 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -5,6 +5,8 @@ // spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST +mod locale; + use clap::{Arg, ArgAction, Command}; use jiff::fmt::strtime; use jiff::tz::{TimeZone, TimeZoneDatabase}; @@ -534,7 +536,7 @@ fn make_format_string(settings: &Settings) -> &str { }, Format::Resolution => "%s.%N", Format::Custom(ref fmt) => fmt, - Format::Default => "%a %b %e %X %Z %Y", + Format::Default => locale::get_locale_default_format(), } } diff --git a/src/uu/date/src/locale.rs b/src/uu/date/src/locale.rs new file mode 100644 index 000000000..72cdd9c14 --- /dev/null +++ b/src/uu/date/src/locale.rs @@ -0,0 +1,167 @@ +// 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. + +//! Locale detection for time format preferences + +// nl_langinfo is available on glibc (Linux), Apple platforms, and BSDs +// but not on Android, Redox or other minimal Unix systems + +// Macro to reduce cfg duplication across the module +macro_rules! cfg_langinfo { + ($($item:item)*) => { + $( + #[cfg(any( + target_os = "linux", + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" + ))] + $item + )* + } +} + +cfg_langinfo! { + use std::ffi::CStr; + use std::sync::OnceLock; +} + +cfg_langinfo! { + /// Cached result of locale time format detection + static TIME_FORMAT_CACHE: OnceLock = OnceLock::new(); + + /// Internal function that performs the actual locale detection + fn detect_12_hour_format() -> bool { + unsafe { + // Set locale from environment variables (empty string = use LC_TIME/LANG env vars) + libc::setlocale(libc::LC_TIME, c"".as_ptr()); + + // Get the date/time format string from locale + let d_t_fmt_ptr = libc::nl_langinfo(libc::D_T_FMT); + if d_t_fmt_ptr.is_null() { + return false; + } + + let Ok(format) = CStr::from_ptr(d_t_fmt_ptr).to_str() else { + return false; + }; + + // Check for 12-hour indicators first (higher priority) + // %I = hour (01-12), %l = hour (1-12) space-padded, %r = 12-hour time with AM/PM + if format.contains("%I") || format.contains("%l") || format.contains("%r") { + return true; + } + + // If we find 24-hour indicators, it's definitely not 12-hour + // %H = hour (00-23), %k = hour (0-23) space-padded, %R = %H:%M, %T = %H:%M:%S + if format.contains("%H") + || format.contains("%k") + || format.contains("%R") + || format.contains("%T") + { + return false; + } + + // Also check the time-only format as a fallback + let t_fmt_ptr = libc::nl_langinfo(libc::T_FMT); + let mut time_fmt_opt = None; + if !t_fmt_ptr.is_null() { + if let Ok(time_format) = CStr::from_ptr(t_fmt_ptr).to_str() { + time_fmt_opt = Some(time_format); + if time_format.contains("%I") + || time_format.contains("%l") + || time_format.contains("%r") + { + return true; + } + } + } + + // Check if there's a specific 12-hour format defined + let t_fmt_ampm_ptr = libc::nl_langinfo(libc::T_FMT_AMPM); + if !t_fmt_ampm_ptr.is_null() { + if let Ok(ampm_format) = CStr::from_ptr(t_fmt_ampm_ptr).to_str() { + // If T_FMT_AMPM is non-empty and different from T_FMT, locale supports 12-hour + if !ampm_format.is_empty() { + if let Some(time_format) = time_fmt_opt { + if ampm_format != time_format { + return true; + } + } else { + return true; + } + } + } + } + } + + // Default to 24-hour format if we can't determine + false + } +} + +cfg_langinfo! { + /// Detects whether the current locale prefers 12-hour or 24-hour time format + /// Results are cached for performance + pub fn uses_12_hour_format() -> bool { + *TIME_FORMAT_CACHE.get_or_init(detect_12_hour_format) + } + + /// Cached default format string + static DEFAULT_FORMAT_CACHE: OnceLock<&'static str> = OnceLock::new(); + + /// Get the locale-appropriate default format string for date output + /// This respects the locale's preference for 12-hour vs 24-hour time + /// Results are cached for performance (following uucore patterns) + pub fn get_locale_default_format() -> &'static str { + DEFAULT_FORMAT_CACHE.get_or_init(|| { + if uses_12_hour_format() { + // Use 12-hour format with AM/PM + "%a %b %e %r %Z %Y" + } else { + // Use 24-hour format + "%a %b %e %X %Z %Y" + } + }) + } +} + +/// On platforms without nl_langinfo support, use 24-hour format by default +#[cfg(not(any( + target_os = "linux", + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" +)))] +pub fn get_locale_default_format() -> &'static str { + "%a %b %e %X %Z %Y" +} + +#[cfg(test)] +mod tests { + cfg_langinfo! { + use super::*; + + #[test] + fn test_locale_detection() { + // Just verify the function doesn't panic + let _ = uses_12_hour_format(); + let _ = get_locale_default_format(); + } + + #[test] + fn test_default_format_contains_valid_codes() { + let format = get_locale_default_format(); + assert!(format.contains("%a")); // abbreviated weekday + assert!(format.contains("%b")); // abbreviated month + assert!(format.contains("%Y")); // year + assert!(format.contains("%Z")); // timezone + } + } +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 512c5c799..bd1c31cc1 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1092,3 +1092,58 @@ fn test_date_military_timezone_with_offset_variations() { .stdout_is(format!("{expected}\n")); } } + +// Locale-aware hour formatting tests +#[test] +#[cfg(unix)] +fn test_date_locale_hour_c_locale() { + // C locale should use 24-hour format + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2025-10-11T13:00") + .succeeds() + .stdout_contains("13:00"); +} + +#[test] +#[cfg(any( + target_os = "linux", + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" +))] +fn test_date_locale_hour_en_us() { + // en_US locale typically uses 12-hour format when available + // Note: If locale is not installed on system, falls back to C locale (24-hour) + let result = new_ucmd!() + .env("LC_ALL", "en_US.UTF-8") + .env("TZ", "UTC") + .arg("-d") + .arg("2025-10-11T13:00") + .succeeds(); + + let stdout = result.stdout_str(); + // Accept either 12-hour (if locale available) or 24-hour (if locale unavailable) + // The important part is that the code doesn't crash and handles locale detection gracefully + assert!( + stdout.contains("1:00") || stdout.contains("13:00"), + "date output should contain either 1:00 (12-hour) or 13:00 (24-hour), got: {stdout}" + ); +} + +#[test] +fn test_date_explicit_format_overrides_locale() { + // Explicit format should override locale preferences + new_ucmd!() + .env("LC_ALL", "en_US.UTF-8") + .env("TZ", "UTC") + .arg("-d") + .arg("2025-10-11T13:00") + .arg("+%H:%M") + .succeeds() + .stdout_is("13:00\n"); +} From 17755d06fb47279b1390ebd1260fd15f58e314ea Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 19 Dec 2025 17:13:34 +0100 Subject: [PATCH 4/7] locale.rs: move more code outside of the unsafe block and refactor a few things --- src/uu/date/src/locale.rs | 102 +++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/src/uu/date/src/locale.rs b/src/uu/date/src/locale.rs index 72cdd9c14..6b756e97d 100644 --- a/src/uu/date/src/locale.rs +++ b/src/uu/date/src/locale.rs @@ -34,70 +34,80 @@ cfg_langinfo! { /// Cached result of locale time format detection static TIME_FORMAT_CACHE: OnceLock = OnceLock::new(); + /// Safe wrapper around libc setlocale + fn set_time_locale() { + unsafe { + nix::libc::setlocale(nix::libc::LC_TIME, c"".as_ptr()); + } + } + + /// Safe wrapper around libc nl_langinfo that returns `Option` + fn get_locale_info(item: nix::libc::nl_item) -> Option { + unsafe { + let ptr = nix::libc::nl_langinfo(item); + if ptr.is_null() { + None + } else { + CStr::from_ptr(ptr).to_str().ok().map(String::from) + } + } + } + /// Internal function that performs the actual locale detection fn detect_12_hour_format() -> bool { - unsafe { - // Set locale from environment variables (empty string = use LC_TIME/LANG env vars) - libc::setlocale(libc::LC_TIME, c"".as_ptr()); - - // Get the date/time format string from locale - let d_t_fmt_ptr = libc::nl_langinfo(libc::D_T_FMT); - if d_t_fmt_ptr.is_null() { - return false; + // Helper function to check for 12-hour format indicators + fn has_12_hour_indicators(format_str: &str) -> bool { + const INDICATORS: &[&str] = &["%I", "%l", "%r"]; + INDICATORS.iter().any(|&indicator| format_str.contains(indicator)) } - let Ok(format) = CStr::from_ptr(d_t_fmt_ptr).to_str() else { - return false; - }; - - // Check for 12-hour indicators first (higher priority) - // %I = hour (01-12), %l = hour (1-12) space-padded, %r = 12-hour time with AM/PM - if format.contains("%I") || format.contains("%l") || format.contains("%r") { - return true; + // Helper function to check for 24-hour format indicators + fn has_24_hour_indicators(format_str: &str) -> bool { + const INDICATORS: &[&str] = &["%H", "%k", "%R", "%T"]; + INDICATORS.iter().any(|&indicator| format_str.contains(indicator)) } - // If we find 24-hour indicators, it's definitely not 12-hour - // %H = hour (00-23), %k = hour (0-23) space-padded, %R = %H:%M, %T = %H:%M:%S - if format.contains("%H") - || format.contains("%k") - || format.contains("%R") - || format.contains("%T") - { - return false; + // Set locale from environment variables (empty string = use LC_TIME/LANG env vars) + set_time_locale(); + + // Get locale format strings using safe wrappers + let d_t_fmt = get_locale_info(nix::libc::D_T_FMT); + let t_fmt_opt = get_locale_info(nix::libc::T_FMT); + let t_fmt_ampm_opt = get_locale_info(nix::libc::T_FMT_AMPM); + + // Check D_T_FMT first + if let Some(ref format) = d_t_fmt { + // Check for 12-hour indicators first (higher priority) + if has_12_hour_indicators(format) { + return true; + } + + // If we find 24-hour indicators, it's definitely not 12-hour + if has_24_hour_indicators(format) { + return false; + } } // Also check the time-only format as a fallback - let t_fmt_ptr = libc::nl_langinfo(libc::T_FMT); - let mut time_fmt_opt = None; - if !t_fmt_ptr.is_null() { - if let Ok(time_format) = CStr::from_ptr(t_fmt_ptr).to_str() { - time_fmt_opt = Some(time_format); - if time_format.contains("%I") - || time_format.contains("%l") - || time_format.contains("%r") - { - return true; - } + if let Some(ref time_format) = t_fmt_opt { + if has_12_hour_indicators(time_format) { + return true; } } // Check if there's a specific 12-hour format defined - let t_fmt_ampm_ptr = libc::nl_langinfo(libc::T_FMT_AMPM); - if !t_fmt_ampm_ptr.is_null() { - if let Ok(ampm_format) = CStr::from_ptr(t_fmt_ampm_ptr).to_str() { - // If T_FMT_AMPM is non-empty and different from T_FMT, locale supports 12-hour - if !ampm_format.is_empty() { - if let Some(time_format) = time_fmt_opt { - if ampm_format != time_format { - return true; - } - } else { + if let Some(ref ampm_format) = t_fmt_ampm_opt { + // If T_FMT_AMPM is non-empty and different from T_FMT, locale supports 12-hour + if !ampm_format.is_empty() { + if let Some(ref time_format) = t_fmt_opt { + if ampm_format != time_format { return true; } + } else { + return true; } } } - } // Default to 24-hour format if we can't determine false From fe979333135ce20c8d00fbbf7ab04d0138334d9d Mon Sep 17 00:00:00 2001 From: Jean-Christian-Cirstea Date: Fri, 19 Dec 2025 21:01:01 +0000 Subject: [PATCH 5/7] truncate: eliminate duplicate stat() syscall (#9527) --- src/uu/truncate/src/truncate.rs | 296 ++++++++++++-------------------- 1 file changed, 112 insertions(+), 184 deletions(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 7a607cc1a..997916b24 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -38,6 +38,10 @@ impl TruncateMode { /// reduce by is greater than `fsize`, then this function returns /// 0 (since it cannot return a negative number). /// + /// # Returns + /// + /// `None` if rounding by 0, else the target size. + /// /// # Examples /// /// Extending a file of 10 bytes by 5 bytes: @@ -45,7 +49,7 @@ impl TruncateMode { /// ```rust,ignore /// let mode = TruncateMode::Extend(5); /// let fsize = 10; - /// assert_eq!(mode.to_size(fsize), 15); + /// assert_eq!(mode.to_size(fsize), Some(15)); /// ``` /// /// Reducing a file by more than its size results in 0: @@ -53,25 +57,36 @@ impl TruncateMode { /// ```rust,ignore /// let mode = TruncateMode::Reduce(5); /// let fsize = 3; - /// assert_eq!(mode.to_size(fsize), 0); + /// assert_eq!(mode.to_size(fsize), Some(0)); /// ``` - fn to_size(&self, fsize: u64) -> u64 { + /// + /// Rounding a file by 0: + /// + /// ```rust,ignore + /// let mode = TruncateMode::RoundDown(0); + /// let fsize = 17; + /// assert_eq!(mode.to_size(fsize), None); + /// ``` + fn to_size(&self, fsize: u64) -> Option { match self { - Self::Absolute(size) => *size, - Self::Extend(size) => fsize + size, - Self::Reduce(size) => { - if *size > fsize { - 0 - } else { - fsize - size - } - } - Self::AtMost(size) => fsize.min(*size), - Self::AtLeast(size) => fsize.max(*size), - Self::RoundDown(size) => fsize - fsize % size, - Self::RoundUp(size) => fsize + fsize % size, + Self::Absolute(size) => Some(*size), + Self::Extend(size) => Some(fsize + size), + Self::Reduce(size) => Some(fsize.saturating_sub(*size)), + Self::AtMost(size) => Some(fsize.min(*size)), + Self::AtLeast(size) => Some(fsize.max(*size)), + Self::RoundDown(size) => fsize.checked_rem(*size).map(|remainder| fsize - remainder), + Self::RoundUp(size) => fsize.checked_next_multiple_of(*size), } } + + /// Determine if mode is absolute + /// + /// # Returns + /// + /// `true` is self matches Self::Absolute(_), `false` otherwise. + fn is_absolute(&self) -> bool { + matches!(self, Self::Absolute(_)) + } } pub mod options { @@ -170,18 +185,9 @@ pub fn uu_app() -> Command { /// /// If the file could not be opened, or there was a problem setting the /// size of the file. -fn file_truncate(filename: &OsString, create: bool, size: u64) -> UResult<()> { +fn do_file_truncate(filename: &Path, create: bool, size: u64) -> UResult<()> { let path = Path::new(filename); - #[cfg(unix)] - if let Ok(metadata) = metadata(path) { - if metadata.file_type().is_fifo() { - return Err(USimpleError::new( - 1, - translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()), - )); - } - } match OpenOptions::new().write(true).create(create).open(path) { Ok(file) => file.set_len(size), Err(e) if e.kind() == ErrorKind::NotFound && !create => Ok(()), @@ -192,155 +198,44 @@ fn file_truncate(filename: &OsString, create: bool, size: u64) -> UResult<()> { ) } -/// Truncate files to a size relative to a given file. -/// -/// `rfilename` is the name of the reference file. -/// -/// `size_string` gives the size relative to the reference file to which -/// to set the target files. For example, "+3K" means "set each file to -/// be three kilobytes larger than the size of the reference file". -/// -/// If `create` is true, then each file will be created if it does not -/// already exist. -/// -/// # Errors -/// -/// If any file could not be opened, or there was a problem setting -/// the size of at least one file. -/// -/// If at least one file is a named pipe (also known as a fifo). -fn truncate_reference_and_size( - rfilename: &str, - size_string: &str, - filenames: &[OsString], - create: bool, +fn file_truncate( + no_create: bool, + reference_size: Option, + mode: &TruncateMode, + filename: &OsString, ) -> UResult<()> { - let mode = match parse_mode_and_size(size_string) { - Err(e) => { - return Err(USimpleError::new( - 1, - translate!("truncate-error-invalid-number", "error" => e), - )); + let path = Path::new(filename); + + // Get the length of the file. + let file_size = match metadata(path) { + Ok(metadata) => { + // A pipe has no length. Do this check here to avoid duplicate `stat()` syscall. + #[cfg(unix)] + if metadata.file_type().is_fifo() { + return Err(USimpleError::new( + 1, + translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()), + )); + } + metadata.len() } - Ok(TruncateMode::Absolute(_)) => { - return Err(USimpleError::new( - 1, - translate!("truncate-error-must-specify-relative-size"), - )); - } - Ok(m) => m, + Err(_) => 0, }; - if let TruncateMode::RoundDown(0) | TruncateMode::RoundUp(0) = mode { + // The reference size can be either: + // + // 1. The size of a given file + // 2. The size of the file to be truncated if no reference has been provided. + let actual_reference_size = reference_size.unwrap_or(file_size); + + let Some(truncate_size) = mode.to_size(actual_reference_size) else { return Err(USimpleError::new( 1, translate!("truncate-error-division-by-zero"), )); - } + }; - let metadata = metadata(rfilename).map_err(|e| match e.kind() { - ErrorKind::NotFound => USimpleError::new( - 1, - translate!("truncate-error-cannot-stat-no-such-file", "filename" => rfilename.quote()), - ), - _ => e.map_err_context(String::new), - })?; - - let fsize = metadata.len(); - let tsize = mode.to_size(fsize); - - for filename in filenames { - file_truncate(filename, create, tsize)?; - } - - Ok(()) -} - -/// Truncate files to match the size of a given reference file. -/// -/// `rfilename` is the name of the reference file. -/// -/// If `create` is true, then each file will be created if it does not -/// already exist. -/// -/// # Errors -/// -/// If any file could not be opened, or there was a problem setting -/// the size of at least one file. -/// -/// If at least one file is a named pipe (also known as a fifo). -fn truncate_reference_file_only( - rfilename: &str, - filenames: &[OsString], - create: bool, -) -> UResult<()> { - let metadata = metadata(rfilename).map_err(|e| match e.kind() { - ErrorKind::NotFound => USimpleError::new( - 1, - translate!("truncate-error-cannot-stat-no-such-file", "filename" => rfilename.quote()), - ), - _ => e.map_err_context(String::new), - })?; - - let tsize = metadata.len(); - - for filename in filenames { - file_truncate(filename, create, tsize)?; - } - - Ok(()) -} - -/// Truncate files to a specified size. -/// -/// `size_string` gives either an absolute size or a relative size. A -/// relative size adjusts the size of each file relative to its current -/// size. For example, "3K" means "set each file to be three kilobytes" -/// whereas "+3K" means "set each file to be three kilobytes larger than -/// its current size". -/// -/// If `create` is true, then each file will be created if it does not -/// already exist. -/// -/// # Errors -/// -/// If any file could not be opened, or there was a problem setting -/// the size of at least one file. -/// -/// If at least one file is a named pipe (also known as a fifo). -fn truncate_size_only(size_string: &str, filenames: &[OsString], create: bool) -> UResult<()> { - let mode = parse_mode_and_size(size_string).map_err(|e| { - USimpleError::new(1, translate!("truncate-error-invalid-number", "error" => e)) - })?; - - if let TruncateMode::RoundDown(0) | TruncateMode::RoundUp(0) = mode { - return Err(USimpleError::new( - 1, - translate!("truncate-error-division-by-zero"), - )); - } - - for filename in filenames { - let path = Path::new(filename); - let fsize = match metadata(path) { - Ok(m) => { - #[cfg(unix)] - if m.file_type().is_fifo() { - return Err(USimpleError::new( - 1, - translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()), - )); - } - m.len() - } - Err(_) => 0, - }; - let tsize = mode.to_size(fsize); - // TODO: Fix duplicate call to stat - file_truncate(filename, create, tsize)?; - } - - Ok(()) + do_file_truncate(path, !no_create, truncate_size) } fn truncate( @@ -350,21 +245,50 @@ fn truncate( size: Option, filenames: &[OsString], ) -> UResult<()> { - let create = !no_create; + let reference_size = match reference { + Some(reference_path) => { + let reference_metadata = metadata(&reference_path).map_err(|error| match error.kind() { + ErrorKind::NotFound => USimpleError::new( + 1, + translate!("truncate-error-cannot-stat-no-such-file", "filename" => reference_path.quote()), + ), + _ => error.map_err_context(String::new), + })?; - // There are four possibilities - // - reference file given and size given, - // - reference file given but no size given, - // - no reference file given but size given, - // - no reference file given and no size given, - match (reference, size) { - (Some(rfilename), Some(size_string)) => { - truncate_reference_and_size(&rfilename, &size_string, filenames, create) + Some(reference_metadata.len()) } - (Some(rfilename), None) => truncate_reference_file_only(&rfilename, filenames, create), - (None, Some(size_string)) => truncate_size_only(&size_string, filenames, create), - (None, None) => unreachable!(), // this case cannot happen anymore because it's handled by clap + None => None, + }; + + let size_string = size.as_deref(); + + // Omitting the mode is equivalent to extending a file by 0 bytes. + let mode = match size_string { + Some(string) => match parse_mode_and_size(string) { + Err(error) => { + return Err(USimpleError::new( + 1, + translate!("truncate-error-invalid-number", "error" => error), + )); + } + Ok(mode) => mode, + }, + None => TruncateMode::Extend(0), + }; + + // If a reference file has been given, the truncate mode cannot be absolute. + if reference_size.is_some() && mode.is_absolute() { + return Err(USimpleError::new( + 1, + translate!("truncate-error-must-specify-relative-size"), + )); } + + for filename in filenames { + file_truncate(no_create, reference_size, &mode, filename)?; + } + + Ok(()) } /// Decide whether a character is one of the size modifiers, like '+' or '<'. @@ -382,13 +306,12 @@ fn is_modifier(c: char) -> bool { /// /// # Panics /// -/// If `size_string` is empty, or if no number could be parsed from the -/// given string (for example, if the string were `"abc"`). +/// If `size_string` is empty. /// /// # Examples /// /// ```rust,ignore -/// assert_eq!(parse_mode_and_size("+123"), (TruncateMode::Extend, 123)); +/// assert_eq!(parse_mode_and_size("+123"), Ok(TruncateMode::Extend(123))); /// ``` fn parse_mode_and_size(size_string: &str) -> Result { // Trim any whitespace. @@ -432,8 +355,13 @@ mod tests { #[test] fn test_to_size() { - assert_eq!(TruncateMode::Extend(5).to_size(10), 15); - assert_eq!(TruncateMode::Reduce(5).to_size(10), 5); - assert_eq!(TruncateMode::Reduce(5).to_size(3), 0); + assert_eq!(TruncateMode::Extend(5).to_size(10), Some(15)); + assert_eq!(TruncateMode::Reduce(5).to_size(10), Some(5)); + assert_eq!(TruncateMode::Reduce(5).to_size(3), Some(0)); + assert_eq!(TruncateMode::RoundDown(4).to_size(13), Some(12)); + assert_eq!(TruncateMode::RoundDown(4).to_size(16), Some(16)); + assert_eq!(TruncateMode::RoundUp(8).to_size(10), Some(16)); + assert_eq!(TruncateMode::RoundUp(8).to_size(16), Some(16)); + assert_eq!(TruncateMode::RoundDown(0).to_size(123), None); } } From 3de941179a68b8d0881fbba9e07dc8f72a5b4106 Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:25:50 +0900 Subject: [PATCH 6/7] base(nc|32|64): Optimize performances reduction memset (#9632) * perf(base32): optimize read buffer allocation in fast encode/decode Refactor buffer creation from zero-initialized vectors to pre-allocated Vec with_capacity, using unsafe set_len to avoid unnecessary zeroing, improving performance without affecting correctness, as only initialized bytes from Read::read are accessed. * refactor: use MaybeUninit for safer buffer handling in base32 encode/decode Replaced manual unsafe `set_len` calls and direct reads into uninitialized vectors with `MaybeUninit::slice_assume_init_mut` to prevent potential memory safety issues and improve code reliability in `fast_encode` and `fast_decode` modules. Added buffer clearing to ensure proper reuse. * refactor(base32): replace MaybeUninit::slice_assume_init_mut with slice::from_raw_parts_mut Replace unsafe usage of `MaybeUninit::slice_assume_init_mut` with `slice::from_raw_parts_mut` in the fast_encode and fast_decode modules for reading data into the spare capacity of buffers. This change maintains safety guarantees through updated comments while potentially improving code clarity and performance by avoiding MaybeUninit initialization assumptions. The modification ensures the buffer's uninitialized tail is correctly handled as raw bytes during I/O operations. * refactor(base32): reorder std imports in base_common.rs for consistency Moved the `slice` import from after `collections::VecDeque` to after `num::NonZeroUsize` to better align with the module's import grouping style. * refactor(base32): remove unsafe buffer handling in encode/decode Replace unsafe spare_capacity_mut and from_raw_parts_mut usage with safe Vec initialization and direct read calls in fast_encode and fast_decode. This eliminates potential safety risks while preserving buffer functionality. * perf(base32): optimize input handling by switching to BufRead for efficient buffering Switch from unbuffered Read to BufRead in get_input, handle_input, and fast_encode_stream functions. This reduces syscalls by leveraging buffered reads, improving performance for base32 encoding/decoding operations. Refactor fast_encode_stream to use fill_buf() and manage leftover buffers more efficiently. --- src/uu/base32/src/base_common.rs | 120 ++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 41 deletions(-) diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index c44d6f7ee..d7f7a9ce9 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -8,7 +8,7 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{self, BufReader, ErrorKind, Read, Write}; +use std::io::{self, BufRead, BufReader, ErrorKind, Write}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::encoding::{ @@ -146,20 +146,26 @@ pub fn base_app(about: String, usage: String) -> Command { ) } -pub fn get_input(config: &Config) -> UResult> { +pub fn get_input(config: &Config) -> UResult> { match &config.to_read { Some(path_buf) => { let file = File::open(path_buf).map_err_context(|| path_buf.maybe_quote().to_string())?; - Ok(Box::new(BufReader::new(file))) + Ok(Box::new(BufReader::with_capacity( + DEFAULT_BUFFER_SIZE, + file, + ))) } None => { // Stdin is already buffered by the OS; wrap once more to reduce syscalls per read. - Ok(Box::new(BufReader::new(io::stdin()))) + Ok(Box::new(BufReader::with_capacity( + DEFAULT_BUFFER_SIZE, + io::stdin(), + ))) } } } -pub fn handle_input(input: &mut R, format: Format, config: Config) -> UResult<()> { +pub fn handle_input(input: &mut R, format: Format, config: Config) -> UResult<()> { // Always allow padding for Base64 to avoid a full pre-scan of the input. let supports_fast_decode_and_encode = get_supports_fast_decode_and_encode(format, config.decode, true); @@ -292,11 +298,11 @@ pub fn get_supports_fast_decode_and_encode( } pub mod fast_encode { - use crate::base_common::{DEFAULT_BUFFER_SIZE, WRAP_DEFAULT}; + use crate::base_common::WRAP_DEFAULT; use std::{ cmp::min, collections::VecDeque, - io::{self, Read, Write}, + io::{self, BufRead, Write}, num::NonZeroUsize, }; use uucore::{ @@ -519,7 +525,7 @@ pub mod fast_encode { /// Remaining bytes are encoded and flushed at the end. I/O or encoding /// failures are propagated via `UResult`. pub fn fast_encode_stream( - input: &mut dyn Read, + input: &mut dyn BufRead, output: &mut dyn Write, supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, wrap: Option, @@ -544,47 +550,79 @@ pub mod fast_encode { }; // Buffers - let mut leftover_buffer = VecDeque::::new(); let mut encoded_buffer = VecDeque::::new(); - - let mut read_buffer = vec![0u8; encode_in_chunks_of_size.max(DEFAULT_BUFFER_SIZE)]; + let mut leftover_buffer = Vec::::with_capacity(encode_in_chunks_of_size); loop { - let read = input - .read(&mut read_buffer) + let read_buffer = input + .fill_buf() .map_err(|err| USimpleError::new(1, super::format_read_error(err.kind())))?; - if read == 0 { + if read_buffer.is_empty() { break; } - leftover_buffer.extend(&read_buffer[..read]); + let mut consumed = 0; - while leftover_buffer.len() >= encode_in_chunks_of_size { - { - let contiguous = leftover_buffer.make_contiguous(); + if !leftover_buffer.is_empty() { + let needed = encode_in_chunks_of_size - leftover_buffer.len(); + let take = needed.min(read_buffer.len()); + leftover_buffer.extend_from_slice(&read_buffer[..take]); + consumed += take; + + if leftover_buffer.len() == encode_in_chunks_of_size { encode_in_chunks_to_buffer( supports_fast_decode_and_encode, - &contiguous[..encode_in_chunks_of_size], + leftover_buffer.as_slice(), &mut encoded_buffer, )?; + leftover_buffer.clear(); + + write_to_output( + &mut line_wrapping, + &mut encoded_buffer, + output, + false, + wrap == Some(0), + )?; } - - // Drop the data we just encoded - leftover_buffer.drain(..encode_in_chunks_of_size); - - write_to_output( - &mut line_wrapping, - &mut encoded_buffer, - output, - false, - wrap == Some(0), - )?; } + + let remaining = &read_buffer[consumed..]; + let full_chunk_bytes = + (remaining.len() / encode_in_chunks_of_size) * encode_in_chunks_of_size; + + if full_chunk_bytes > 0 { + for chunk in remaining[..full_chunk_bytes].chunks_exact(encode_in_chunks_of_size) { + encode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + chunk, + &mut encoded_buffer, + )?; + write_to_output( + &mut line_wrapping, + &mut encoded_buffer, + output, + false, + wrap == Some(0), + )?; + } + consumed += full_chunk_bytes; + } + + if consumed < read_buffer.len() { + leftover_buffer.extend_from_slice(&read_buffer[consumed..]); + consumed = read_buffer.len(); + } + + input.consume(consumed); + + // `leftover_buffer` should never exceed one partial chunk. + debug_assert!(leftover_buffer.len() < encode_in_chunks_of_size); } // Encode any remaining bytes and flush supports_fast_decode_and_encode - .encode_to_vec_deque(leftover_buffer.make_contiguous(), &mut encoded_buffer)?; + .encode_to_vec_deque(&leftover_buffer, &mut encoded_buffer)?; write_to_output( &mut line_wrapping, @@ -599,8 +637,7 @@ pub mod fast_encode { } pub mod fast_decode { - use crate::base_common::DEFAULT_BUFFER_SIZE; - use std::io::{self, Read, Write}; + use std::io::{self, BufRead, Write}; use uucore::{ encoding::SupportsFastDecodeAndEncode, error::{UResult, USimpleError}, @@ -630,7 +667,6 @@ pub mod fast_decode { fn write_to_output(decoded_buffer: &mut Vec, output: &mut dyn Write) -> io::Result<()> { // Write all data in `decoded_buffer` to `output` output.write_all(decoded_buffer.as_slice())?; - output.flush()?; decoded_buffer.clear(); @@ -764,7 +800,7 @@ pub mod fast_decode { } pub fn fast_decode_stream( - input: &mut dyn Read, + input: &mut dyn BufRead, output: &mut dyn Write, supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, ignore_garbage: bool, @@ -783,17 +819,17 @@ pub mod fast_decode { let mut buffer = Vec::with_capacity(decode_in_chunks_of_size); let mut decoded_buffer = Vec::::new(); - let mut read_buffer = [0u8; DEFAULT_BUFFER_SIZE]; loop { - let read = input - .read(&mut read_buffer) + let read_buffer = input + .fill_buf() .map_err(|err| USimpleError::new(1, super::format_read_error(err.kind())))?; - if read == 0 { + let read_len = read_buffer.len(); + if read_len == 0 { break; } - for &byte in &read_buffer[..read] { + for &byte in read_buffer { if byte == b'\n' || byte == b'\r' { continue; } @@ -845,6 +881,8 @@ pub mod fast_decode { buffer.clear(); } } + + input.consume(read_len); } if supports_partial_decode { @@ -902,7 +940,7 @@ fn format_read_error(kind: ErrorKind) -> String { /// Determines if the input buffer contains any padding ('=') ignoring trailing whitespace. #[cfg(test)] -fn read_and_has_padding(input: &mut R) -> UResult<(bool, Vec)> { +fn read_and_has_padding(input: &mut R) -> UResult<(bool, Vec)> { let mut buf = Vec::new(); input .read_to_end(&mut buf) From f72130e9d84bce1b437fc98d4ac92eadd31592e6 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 20 Dec 2025 08:21:39 +0900 Subject: [PATCH 7/7] GnuTests: Caches for faster configure and skipping make (#9627) --- .github/workflows/GnuTests.yml | 23 +++++++++++++++++++++-- util/build-gnu.sh | 11 ++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index c8070f629..6c528dbd3 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -2,7 +2,7 @@ name: GnuTests # spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem # spell-checker:ignore (jargon) submodules devel -# spell-checker:ignore (libs/utils) autopoint chksum dpkg getenforce gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e +# spell-checker:ignore (libs/utils) autopoint chksum dpkg getenforce getlimits gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e # spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic # spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay # spell-checker:ignore (vars) FILESET SUBDIRS XPASS @@ -51,7 +51,17 @@ jobs: workspaces: "./uutils -> target" - name: Checkout code (GNU coreutils) run: (mkdir -p gnu && cd gnu && bash ../uutils/util/fetch-gnu.sh) - + - name: Restore files for faster configure and skipping make + uses: actions/cache@v5 + id: cache-config-gnu + with: + path: | + gnu/config.cache + gnu/src/getlimits + key: ${{ runner.os }}-gnu-config-${{ env.REPO_GNU_REF }}-${{ hashFiles('gnu/configure') }} + restore-keys: | + ${{ runner.os }}-gnu-config-${{ env.REPO_GNU_REF }}- + ${{ runner.os }}-gnu-config- #### Build environment setup - name: Install dependencies shell: bash @@ -94,6 +104,15 @@ jobs: ## Build binaries cd 'uutils' env PROFILE=release-small bash util/build-gnu.sh + + - name: Save files for faster configure and skipping make + uses: actions/cache/save@v5 + if: always() && steps.cache-config-gnu.outputs.cache-hit != 'true' + with: + path: | + gnu/config.cache + gnu/src/getlimits + key: ${{ runner.os }}-gnu-config-${{ env.REPO_GNU_REF }}-${{ hashFiles('gnu/configure') }} ### Run tests as user - name: Run GNU tests diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 5bb1c34f0..6d5f622d1 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -120,21 +120,24 @@ done if test -f gnu-built; then echo "GNU build already found. Skip" - echo "'rm -f $(pwd)/gnu-built' to force the build" + echo "'rm -f $(pwd)/{gnu-built,src/getlimits}' to force the build" echo "Note: the customization of the tests will still happen" else # Disable useless checks "${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk # Use CFLAGS for best build time since we discard GNU coreutils - CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ + CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure -C --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ --enable-single-binary=symlinks \ "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs "${SED}" -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver # Use a better diff "${SED}" -i 's|diff -c|diff -u|g' tests/Coreutils.pm + + # Skip make if possible # Use our nproc for *BSD and macOS - "${MAKE}" -j "$("${UU_BUILD_DIR}/nproc")" + test -f src/getlimits || "${MAKE}" -j "$("${UU_BUILD_DIR}/nproc")" + cp -f src/getlimits "${UU_BUILD_DIR}" # Handle generated factor tests t_first=00 @@ -219,8 +222,6 @@ sed -i -e "s|---dis ||g" tests/tail/overlay-headers.sh -e "s|strace -e inotify_add_watch|strace -f -e inotify_add_watch|" \ tests/tail/inotify-dir-recreate.sh -test -f "${UU_BUILD_DIR}/getlimits" || cp src/getlimits "${UU_BUILD_DIR}" - # pr produces very long log and this command isn't super interesting # SKIP for now "${SED}" -i -e "s|my \$prog = 'pr';$|my \$prog = 'pr';CuSkip::skip \"\$prog: SKIP for producing too long logs\";|" tests/pr/pr-tests.pl