diagnostics: let the caret rendering be compiled out

The renderer and its ariadne dependency sit behind the new
feat_diagnostics cargo feature, on by default. Without it a no-op
stand-in with the same API takes uucore::diagnostics' place, so the
utilities need no cfg of their own: enabled() is always false and
every caller falls back to its plain one-line message.

Each utility keeps a crate-level `diagnostics` feature for standalone
builds, the caret snapshot tests compile out with the renderer, and CI
gains a Build/No-diagnostics job that runs the suite without the
feature and fails if ariadne sneaks back into the dependency tree.
This commit is contained in:
Sylvestre Ledru committed 2026-08-15 13:23:02 +01:00
1 parent 3850289841
commit 77741def26
30 files changed
+204 -46

No files matched your search

+44
View File
@@ -901,6 +901,50 @@ jobs:
--features "${{ matrix.job.features }},openssl" \
-E 'test(/^test_(md5sum|sha1sum|sha224sum|sha256sum|sha384sum|sha512sum|cksum)::/)'
test_no_diagnostics:
name: Build/No-diagnostics
needs: [ min_version, deps ]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
id: sccache-setup
uses: mozilla-actions/sccache-action@v0.0.11
continue-on-error: true
- name: Export sccache
if: steps.sccache-setup.outcome == 'success'
run: |
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
# The caret diagnostics (`feat_diagnostics`) are on by default; packagers
# who want the smaller dependency tree build with them off, so that build
# must stay green. The caret snapshot tests are gated on the feature and
# compile out; everything else runs as usual against the plain messages.
- name: Test without the diagnostics feature
shell: bash
env:
RUST_BACKTRACE: "1"
run: |
cargo nextest run --hide-progress-bar --profile ci \
--no-default-features --features feat_os_unix
# Confirm the feature actually compiles the renderer out. If ariadne is
# still in the tree, some crate re-grew a hard dependency on it.
# (`cargo tree -i` exits non-zero when the crate is absent from the graph.)
- name: Verify ariadne is compiled out
shell: bash
run: |
if cargo tree -p coreutils --no-default-features --features feat_os_unix -e normal -i ariadne 2>/dev/null; then
echo "ERROR: ariadne is still in the dependency tree without feat_diagnostics"
exit 1
fi
echo "OK: ariadne is not in the dependency tree"
test_selinux:
name: Build/SELinux
needs: [ min_version, deps ]
+3
View File
@@ -59,6 +59,9 @@ jobs:
# Run host-compiled integration tests against the WASI binary.
# Tests incompatible with WASI are annotated with
# #[cfg_attr(wasi_runner, ignore)] in the test source files.
# The binary above is built without default features, so it has no
# caret diagnostics; the host test crate does have them, hence the
# not(wasi_runner) in the cfg guarding every `mod diagnostics`.
# TODO: add integration tests for these tools as WASI support is extended:
# arch b2sum cat cksum csplit date dir dircolors fmt join
# ls md5sum mkdir mv nproc pathchk pr printenv ptx pwd readlink
+15 -1
View File
@@ -20,7 +20,7 @@ categories.workspace = true
all-features = true
[features]
default = ["feat_common_core"]
default = ["feat_common_core", "feat_diagnostics"]
## OS feature shortcodes
unix = ["feat_os_unix"]
windows = ["feat_os_windows"]
@@ -62,6 +62,15 @@ uudoc = [
"dep:zip",
]
## features
# "feat_diagnostics" == render errors against the argument list with a caret
# when stderr is a terminal. On by default; build with `--no-default-features`
# (plus a `feat_os_*` selection) to compile the rendering — and its ariadne
# dependency — out. Utilities then keep their plain one-line messages.
# Enabling `uucore/diagnostics` is enough: cargo unifies features, so every
# utility in the build sees it. The matching `diagnostics` feature each `uu_*`
# crate declares is there for building that crate on its own, and is
# deliberately not listed here.
feat_diagnostics = ["uucore/diagnostics"]
## Optional feature for stdbuf
# "feat_external_libstdbuf" == use an external libstdbuf.so for stdbuf instead of embedding it
feat_external_libstdbuf = ["stdbuf/feat_external_libstdbuf"]
@@ -396,6 +405,11 @@ version = "0.10.0"
[workspace.dependencies]
ansi-width = "0.1.0"
# The diagnostics rewrite ariadne's output by row number, so a release that
# changes the shape of a report is a breaking change here even when it is not
# one for ariadne. `uucore`'s `the_rendered_report_has_the_row_shape_the_rewriting_assumes`
# is what catches it; keep the bound tight enough that only patch releases
# arrive without one running.
ariadne = "0.6.0"
base64-simd = "0.8"
bigdecimal = "0.4"
-17
View File
@@ -73,16 +73,6 @@ version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "ariadne"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8454c8a44ce2cb9cc7e7fae67fc6128465b343b92c6631e94beca3c8d1524ea5"
dependencies = [
"unicode-width",
"yansi",
]
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -2057,7 +2047,6 @@ dependencies = [
name = "uucore"
version = "0.10.0"
dependencies = [
"ariadne",
"base64-simd",
"bigdecimal",
"blake2b_simd",
@@ -2323,12 +2312,6 @@ dependencies = [
"either",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.8.3"
+4 -7
View File
@@ -18,18 +18,15 @@ doctest = false
[dependencies]
clap = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = [
"diagnostics",
"entries",
"fs",
"mode",
"perms",
] }
uucore = { workspace = true, features = ["entries", "fs", "mode", "perms"] }
fluent = { workspace = true }
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
uucore = { workspace = true, features = ["safe-traversal"] }
[features]
diagnostics = ["uucore/diagnostics"]
[lints]
workspace = true
+4 -1
View File
@@ -20,7 +20,7 @@ clap = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
onig = { workspace = true }
uucore = { workspace = true, features = ["diagnostics", "i18n-collator"] }
uucore = { workspace = true, features = ["i18n-collator"] }
thiserror = { workspace = true }
fluent = { workspace = true }
@@ -28,6 +28,9 @@ fluent = { workspace = true }
divan = { workspace = true }
uucore = { workspace = true, features = ["benchmark"] }
[features]
diagnostics = ["uucore/diagnostics"]
[lints]
workspace = true
+1 -1
View File
@@ -21,7 +21,6 @@ thiserror = { workspace = true }
uucore = { workspace = true, default-features = true, features = [
"backup-control",
"buf-copy",
"diagnostics",
"fs",
"mode",
"perms",
@@ -35,6 +34,7 @@ rustix = { workspace = true }
selinux = { workspace = true, optional = true }
[features]
diagnostics = ["uucore/diagnostics"]
selinux = ["dep:selinux", "uucore/selinux"]
[lints]
+2 -6
View File
@@ -18,18 +18,14 @@ doctest = false
[dependencies]
clap = { workspace = true }
uucore = { workspace = true, features = [
"diagnostics",
"fs",
"fsxattr",
"mode",
] }
uucore = { workspace = true, features = ["fs", "fsxattr", "mode"] }
fluent = { workspace = true }
[target.'cfg(unix)'.dependencies]
rustix = { workspace = true, features = ["process", "fs"] }
[features]
diagnostics = ["uucore/diagnostics"]
selinux = ["uucore/selinux"]
smack = ["uucore/smack"]
+2 -1
View File
@@ -18,7 +18,7 @@ doctest = false
[dependencies]
clap = { workspace = true }
uucore = { workspace = true, features = ["diagnostics", "fs", "mode"] }
uucore = { workspace = true, features = ["fs", "mode"] }
fluent = { workspace = true }
[target.'cfg(unix)'.dependencies]
@@ -28,6 +28,7 @@ rustix = { workspace = true, features = ["fs", "process"] }
libc = { workspace = true }
[features]
diagnostics = ["uucore/diagnostics"]
selinux = ["uucore/selinux"]
smack = ["uucore/smack"]
+2 -1
View File
@@ -19,11 +19,12 @@ doctest = false
[dependencies]
clap = { workspace = true }
uucore = { workspace = true, features = ["diagnostics", "fs", "mode"] }
uucore = { workspace = true, features = ["fs", "mode"] }
fluent = { workspace = true }
nix = { workspace = true }
[features]
diagnostics = ["uucore/diagnostics"]
selinux = ["uucore/selinux"]
smack = ["uucore/smack"]
+3 -1
View File
@@ -17,7 +17,6 @@ path = "src/numfmt.rs"
[dependencies]
clap = { workspace = true }
uucore = { workspace = true, features = [
"diagnostics",
"parser",
"ranges",
"i18n-common",
@@ -31,6 +30,9 @@ memchr = { workspace = true }
divan = { workspace = true }
uucore = { workspace = true, features = ["benchmark"] }
[features]
diagnostics = ["uucore/diagnostics"]
[lints]
workspace = true
+4 -5
View File
@@ -18,13 +18,12 @@ doctest = false
[dependencies]
clap = { workspace = true }
uucore = { workspace = true, features = [
"diagnostics",
"format",
"quoting-style",
] }
uucore = { workspace = true, features = ["format", "quoting-style"] }
fluent = { workspace = true }
[features]
diagnostics = ["uucore/diagnostics"]
[lints]
workspace = true
+1 -1
View File
@@ -19,6 +19,7 @@ doctest = false
[features]
default = ["i18n-collator"]
diagnostics = ["uucore/diagnostics"]
i18n-collator = ["uucore/i18n-collator"]
[dependencies]
@@ -34,7 +35,6 @@ self_cell = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = [
"diagnostics",
"fs",
"parser-size",
"version-cmp",
+4 -1
View File
@@ -20,7 +20,7 @@ clap = { workspace = true }
fluent = { workspace = true }
libc = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = ["diagnostics", "process", "wide"] }
uucore = { workspace = true, features = ["process", "wide"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { workspace = true, features = [
@@ -33,6 +33,9 @@ windows-sys = { workspace = true, features = [
[dev-dependencies]
tempfile = { workspace = true }
[features]
diagnostics = ["uucore/diagnostics"]
[lints]
workspace = true
+4 -1
View File
@@ -19,7 +19,7 @@ doctest = false
[dependencies]
nom = { workspace = true }
clap = { workspace = true }
uucore = { workspace = true, features = ["diagnostics", "fs", "signals"] }
uucore = { workspace = true, features = ["fs", "signals"] }
fluent = { workspace = true }
bytecount = { workspace = true, features = ["runtime-dispatch-simd"] }
@@ -28,6 +28,9 @@ divan = { workspace = true }
rustix = { workspace = true, features = ["stdio", "fs"] }
uucore = { workspace = true, features = ["benchmark"] }
[features]
diagnostics = ["uucore/diagnostics"]
[lints]
workspace = true
+1 -1
View File
@@ -175,7 +175,7 @@ i18n-datetime = [
"jiff-icu",
"jiff",
]
mode = ["diagnostics", "libc"]
mode = ["libc"]
perms = ["entries", "libc", "walkdir"]
buf-copy = []
parser-num = ["extendedbigdecimal", "num-traits"]
+5
View File
@@ -19,6 +19,11 @@ pub mod checksum;
pub mod colors;
#[cfg(feature = "diagnostics")]
pub mod diagnostics;
// Without the feature, a no-op stand-in keeps the API — and its callers —
// compiling; they all fall back to their plain one-line messages.
#[cfg(not(feature = "diagnostics"))]
#[path = "features/diagnostics_stub.rs"]
pub mod diagnostics;
#[cfg(feature = "encoding")]
pub mod encoding;
#[cfg(feature = "extendedbigdecimal")]
@@ -0,0 +1,94 @@
// 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.
//! No-op stand-in for the `diagnostics` module, compiled when the
//! `diagnostics` cargo feature is off.
//!
//! It mirrors the real API so callers need no `cfg` of their own:
//! [`enabled`] is always `false`, the locators find nothing and the render
//! methods render nothing, so every caller falls back to the plain one-line
//! message it would print anywhere but a terminal.
use std::ffi::{OsStr, OsString};
use std::ops::Range;
/// Always `false`: rendering is compiled out.
pub fn enabled() -> bool {
false
}
/// Always `None`: rendering is compiled out.
pub fn capture(_args: &[OsString]) -> Option<Vec<OsString>> {
None
}
/// Always `None`: rendering is compiled out.
pub fn operands(_args: &[OsString]) -> Option<Vec<OsString>> {
None
}
/// A snapshot of nothing: it finds nothing and renders nothing.
///
/// Deliberately derives nothing the real `Snapshot` does not, so that what a
/// caller may do with one does not depend on whether the feature is on.
pub struct Snapshot;
#[allow(clippy::unused_self)]
impl Snapshot {
pub fn new<S: AsRef<OsStr>>(_args: &[S]) -> Self {
Self
}
pub fn with_program<S: AsRef<OsStr>>(_args: &[S]) -> Self {
Self
}
pub fn from_bytes<S: AsRef<[u8]>>(_args: &[S]) -> Self {
Self
}
pub fn is_empty(&self) -> bool {
true
}
pub fn index_of(&self, _arg: &OsStr) -> Option<usize> {
None
}
pub fn index_of_value(
&self,
_operand: &str,
_short: Option<char>,
_long: Option<&str>,
) -> Option<usize> {
None
}
pub fn index_of_positional(&self, _n: usize) -> Option<usize> {
None
}
pub fn render(
&self,
_index: usize,
_message: &str,
_label: Option<&str>,
_help: Option<&str>,
) -> bool {
false
}
pub fn render_inside_at(
&self,
_index: usize,
_operand: &str,
_range: Range<usize>,
_message: &str,
_label: Option<&str>,
_help: Option<&str>,
) -> bool {
false
}
}
-1
View File
@@ -45,7 +45,6 @@ pub use crate::features::char_width;
pub use crate::features::checksum;
#[cfg(feature = "colors")]
pub use crate::features::colors;
#[cfg(feature = "diagnostics")]
pub use crate::features::diagnostics;
#[cfg(feature = "encoding")]
pub use crate::features::encoding;
+1
View File
@@ -1691,6 +1691,7 @@ fn test_chmod_symlink_two_links_same_dir() {
// cSpell:enable
}
#[cfg(all(feature = "feat_diagnostics", not(wasi_runner)))]
mod diagnostics {
use super::*;
#[cfg(unix)]
Loaded 20 of 30 files, more files were not shown because too many files have changed in this diff. Show more