diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index df1fba73c54..6c384e00b1a 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -648,10 +648,6 @@ jobs: ;; esac outputs CARGO_TEST_OPTIONS - # ** pass needed environment into `cross` container (iff `cross` not already configured via "Cross.toml") - if [ "${CARGO_CMD}" = 'cross' ] && [ ! -e "Cross.toml" ] ; then - printf "[build.env]\npassthrough = [\"CI\", \"RUST_BACKTRACE\", \"CARGO_TERM_COLOR\"]\n" > Cross.toml - fi # * executable for `strip`? STRIP="strip" case ${{ matrix.job.target }} in diff --git a/Cargo.lock b/Cargo.lock index 9cc13a205af..4ed5c1e4581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2457,6 +2457,16 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "tzfile" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59c22c42a2537e4c7ad21a4007273bbc5bebed7f36bc93730a5780e22a4592e" +dependencies = [ + "byteorder", + "chrono", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3591,6 +3601,7 @@ dependencies = [ "tempfile", "thiserror 2.0.12", "time", + "tzfile", "utmp-classic", "uucore_procs", "walkdir", diff --git a/Cargo.toml b/Cargo.toml index cde946b68f4..b7c911e9f82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap procfs uuhelp startswith constness expl +# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap procfs tzfile uuhelp startswith constness expl [package] name = "coreutils" @@ -341,6 +341,7 @@ terminal_size = "0.4.0" textwrap = { version = "0.16.1", features = ["terminal_size"] } thiserror = "2.0.3" time = { version = "0.3.36" } +tzfile = "0.1.3" unicode-segmentation = "1.11.0" unicode-width = "0.2.0" utf-8 = "0.7.6" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 00000000000..52f5bad21dd --- /dev/null +++ b/Cross.toml @@ -0,0 +1,7 @@ +# spell-checker:ignore (misc) dpkg noninteractive tzdata +[build] +pre-build = [ + "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install tzdata", +] +[build.env] +passthrough = ["CI", "RUST_BACKTRACE", "CARGO_TERM_COLOR"] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 43106c423d6..a6f9b19fb6b 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -190,6 +190,12 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.2.20" @@ -1142,6 +1148,16 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "tzfile" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59c22c42a2537e4c7ad21a4007273bbc5bebed7f36bc93730a5780e22a4592e" +dependencies = [ + "byteorder", + "chrono", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1336,6 +1352,7 @@ dependencies = [ "sha3", "sm3", "thiserror", + "tzfile", "uucore_procs", "wild", "winapi-util", diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index acbba4c7307..9602d230593 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -1,4 +1,4 @@ -# spell-checker:ignore (features) bigdecimal zerocopy extendedbigdecimal +# spell-checker:ignore (features) bigdecimal zerocopy extendedbigdecimal tzfile [package] name = "uucore" @@ -59,6 +59,7 @@ regex = { workspace = true, optional = true } bigdecimal = { workspace = true, optional = true } num-traits = { workspace = true, optional = true } selinux = { workspace = true, optional = true } +tzfile = { workspace = true, optional = true } [target.'cfg(unix)'.dependencies] walkdir = { workspace = true, optional = true } @@ -134,6 +135,6 @@ utf8 = [] utmpx = ["time", "time/macros", "libc", "dns-lookup"] version-cmp = [] wide = [] -custom-tz-fmt = ["chrono", "chrono-tz", "iana-time-zone"] +custom-tz-fmt = ["chrono", "chrono-tz", "tzfile", "iana-time-zone"] tty = [] uptime = ["chrono", "libc", "windows-sys", "utmpx", "utmp-classic", "thiserror"] diff --git a/src/uucore/build.rs b/src/uucore/build.rs new file mode 100644 index 00000000000..4ac7723657b --- /dev/null +++ b/src/uucore/build.rs @@ -0,0 +1,26 @@ +// 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 (vars) tzfile zoneinfo + +use std::env; + +pub fn main() { + // If custom-tz-fmt feature is enabled, set an "embed_tz" config to decide whether + // to embed a full timezone database, or we can just use `tzfile` (which reads + // from /usr/share/zoneinfo). + println!("cargo::rustc-check-cfg=cfg(embed_tz)"); + let custom_tz_fmt = env::var("CARGO_FEATURE_CUSTOM_TZ_FMT"); + if custom_tz_fmt.is_ok() { + // TODO: It might be worth considering making this an option: + // - People concerned with executable size may be willing to forgo timezone database + // completely. + // - Some other people may want to use an embedded timezone database _anyway_, instead + // of the one provided by the system. + if cfg!(windows) || cfg!(target_os = "android") { + println!("cargo::rustc-cfg=embed_tz"); + } + } +} diff --git a/src/uucore/src/lib/features/custom_tz_fmt.rs b/src/uucore/src/lib/features/custom_tz_fmt.rs index 132155f540a..dc55c86bb37 100644 --- a/src/uucore/src/lib/features/custom_tz_fmt.rs +++ b/src/uucore/src/lib/features/custom_tz_fmt.rs @@ -3,25 +3,66 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use chrono::{TimeZone, Utc}; -use chrono_tz::{OffsetName, Tz}; +// spell-checker:ignore (misc) tzfile WARST zoneinfo + +use chrono::Local; use iana_time_zone::get_timezone; +#[cfg(embed_tz)] +use chrono_tz::{ParseError, Tz}; +#[cfg(not(embed_tz))] +use tzfile::Tz; + +#[cfg(embed_tz)] +fn str_to_timezone(str: &str) -> Result { + str.parse() +} + +#[cfg(not(embed_tz))] +fn str_to_timezone(str: &str) -> Result { + Tz::named(str) +} + /// Get the alphabetic abbreviation of the current timezone. /// /// For example, "UTC" or "CET" or "PDT" -fn timezone_abbreviation() -> String { - let tz = match std::env::var("TZ") { - // TODO Support other time zones... - Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, +/// +/// We need this function even for local dates as chrono(_tz) does not provide a +/// way to convert Local to a fully specified timezone with abbreviation +/// (). +/// +/// `timezone` is an optional parameter, if None, TZ environment variable is used. +// +// TODO(#7659): This should take into account the date to be printed. +// - Timezone abbreviation depends on daylight savings. +// - We should do no special conversion for UTC dates. +// - If our custom logic fails, but chrono obtained a non-UTC local timezone +// from the system, we should not just return UTC. +fn timezone_abbreviation(timezone: Option<&str>) -> String { + let utc = str_to_timezone("Etc/UTC").unwrap(); + // We need this logic as `iana_time_zone::get_timezone` does not look + // at TZ variable: https://github.com/strawlab/iana-time-zone/issues/118. + let tz = match timezone.or(std::env::var("TZ").as_deref().ok()) { + // TODO: This is not fully exhaustive, we should understand how to handle + // invalid TZ values and more complex POSIX-specified values: + // https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + Some(s) if s == "UTC0" || s.is_empty() => utc, + Some(s) => str_to_timezone(s).unwrap_or(utc), _ => match get_timezone() { - Ok(tz_str) => tz_str.parse().unwrap(), - Err(_) => Tz::Etc__UTC, + Ok(tz_str) => str_to_timezone(&tz_str).unwrap_or(utc), + Err(_) => utc, }, }; - let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); - offset.abbreviation().unwrap_or("UTC").to_string() + #[cfg(not(embed_tz))] + let tz = &tz; + + // TODO: It looks a bit absurd to use `%Z` here and manually expand it + // in `custom_time_format`, instead of directly modifying the date to be + // formatted. We should create another function that returns + // `localtime.with_timezone(&tz)` (a local time with fully specified + // timezone abbreviation). + Local::now().with_timezone(&tz).format("%Z").to_string() } /// Adapt the given string to be accepted by the chrono library crate. @@ -37,7 +78,7 @@ pub fn custom_time_format(fmt: &str) -> String { // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 // GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`. fmt.replace("%N", "%f") - .replace("%Z", timezone_abbreviation().as_ref()) + .replace("%Z", timezone_abbreviation(None).as_ref()) } #[cfg(test)] @@ -53,6 +94,40 @@ mod tests { custom_time_format("%Y-%m-%d %H-%M-%S.%N"), "%Y-%m-%d %H-%M-%S.%f" ); - assert_eq!(custom_time_format("%Z"), timezone_abbreviation()); + assert_eq!(custom_time_format("%Z"), timezone_abbreviation(None)); + } + + #[test] + fn test_timezone_abbreviation() { + // Test if a timezone abbreviation is one of the values in ok_abbr. + // TODO(#7659): We should modify this test to 2 fixed dates, one that falls in + // daylight savings, and the other not. But right now the abbreviation depends + // on the current time. + fn test_zone(zone: &str, ok_abbr: &[&str]) { + let abbr = timezone_abbreviation(Some(zone)); + assert!( + ok_abbr.contains(&abbr.as_str()), + "Timezone {zone} abbreviation {abbr} is not contained within [{}].", + ok_abbr.join(", ") + ) + } + + // Test a few random timezones. + test_zone("US/Pacific", &["PST", "PDT"]); + test_zone("Europe/Zurich", &["CEST", "CET"]); + test_zone("Africa/Cairo", &["EET", "EEST"]); // spell-checker:disable-line + test_zone("Asia/Taipei", &["CST"]); + test_zone("Australia/Sydney", &["AEDT", "AEST"]); // spell-checker:disable-line + // Looks like Pacific/Tahiti is provided in /usr/share/zoneinfo, but not in chrono-tz (yet). + //test_zone("Pacific/Tahiti", &["-10"]); // No abbreviation? + test_zone("Antarctica/South_Pole", &["NZDT", "NZST"]); // spell-checker:disable-line + + // TODO: This is not fully exhaustive, we should understand how to handle + // invalid TZ values and more complex POSIX-specified values: + // https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + // Examples: + //test_zone("WART4WARST,J1/0,J365/25", &["WART", "WARST"]) + //test_zone(":Europe/Zurich", &["CEST", "CET"]); + //test_zone("invalid", &["invalid"]); } }