diff --git a/apps/solidcraft-cli/Cargo.toml b/apps/solidcraft-cli/Cargo.toml index 258dd32f9..ed69dc559 100644 --- a/apps/solidcraft-cli/Cargo.toml +++ b/apps/solidcraft-cli/Cargo.toml @@ -20,11 +20,12 @@ name = "solvecraft-cli" path = "src/alias.rs" [features] -default = ["raytrace-cpu", "smlib", "profile"] +default = ["raytrace-cpu", "profile"] bundled-render-library = ["solidcraft-engine/bundled-render-library"] raytrace-cpu = ["solidcraft-engine/raytrace-cpu"] -# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh). SMLib is the -# only kernel, so without it the command line models nothing. +# Also link the C++ SMLib kernel (solidcraft-engine/smlib), off by default: needs a C++ toolchain +# and scripts/fetch-smlib.sh. solidcore, the Rust kernel, is always built and is the default either +# way; with this on, SOLIDCRAFT_KERNEL=smlib or the Geometry kernel preference can choose C++. smlib = ["solidcraft-engine/smlib"] # The profiler, compiled in so any build can say where its time goes (`--profile`, # SOLIDCRAFT_PROFILE=1); it records nothing until asked. diff --git a/apps/solidcraft-cli/src/kernel_cases.rs b/apps/solidcraft-cli/src/kernel_cases.rs index 40bd66730..71ec42387 100644 --- a/apps/solidcraft-cli/src/kernel_cases.rs +++ b/apps/solidcraft-cli/src/kernel_cases.rs @@ -695,7 +695,7 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>, validation: solidcraft && solidcraft_engine::kernel::kernel_route::backend() == solidcraft_engine::kernel::kernel_route::Backend::Smlib { out["status"] = json!("unsupported"); - out["error"] = json!("this build has no SMLib: build solidcraft-cli with --features smlib"); + out["error"] = json!("this build doesn't include the C++ SMLib kernel; build solidcraft-cli with --features smlib"); return out; } // `refused`: only a clean refusal is right; `solid_or_refused`: a clean refusal, or a result diff --git a/apps/solidcraft-web/Cargo.toml b/apps/solidcraft-web/Cargo.toml index 7a7661a88..57b7c539f 100644 --- a/apps/solidcraft-web/Cargo.toml +++ b/apps/solidcraft-web/Cargo.toml @@ -14,11 +14,11 @@ name = "solidcraft-web" path = "src/main.rs" [features] -default = ["smlib", "profile"] +default = ["profile"] bundled-render-library = ["solidcraft-engine/bundled-render-library"] -# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh -# wasm32-unknown-unknown, or the sources and wasi-sdk in WASI_SDK_PATH, crates/smlib/build.rs). SMLib -# is the only kernel, so without it the web app models nothing. +# Also link the C++ SMLib kernel (solidcraft-engine/smlib), off by default: needs +# scripts/fetch-smlib.sh wasm32-unknown-unknown, or the sources and wasi-sdk in WASI_SDK_PATH +# (crates/smlib/build.rs). solidcore, the Rust kernel, is always built and is the default either way. smlib = ["solidcraft-engine/smlib"] # The profiler, compiled in so any build can say where its time goes (the Profile operations # preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked. diff --git a/apps/solidcraft/Cargo.toml b/apps/solidcraft/Cargo.toml index 5e3bfc360..6860706a1 100644 --- a/apps/solidcraft/Cargo.toml +++ b/apps/solidcraft/Cargo.toml @@ -8,11 +8,12 @@ publish = false default-run = "solidcraft" [features] -default = ["raytrace-cpu", "bundled-render-library", "smlib", "profile"] +default = ["raytrace-cpu", "bundled-render-library", "profile"] bundled-render-library = ["solidcraft-engine/bundled-render-library"] raytrace-cpu = ["solidcraft-engine/raytrace-cpu"] -# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh). SMLib is the -# only kernel, so without it the app models nothing. +# Also link the C++ SMLib kernel (solidcraft-engine/smlib), off by default: needs a C++ toolchain +# and scripts/fetch-smlib.sh. solidcore, the Rust kernel, is always built and is the default either +# way; with this on, SOLIDCRAFT_KERNEL=smlib or the Geometry kernel preference can choose C++. smlib = ["solidcraft-engine/smlib"] # The profiler, compiled in so any build can say where its time goes (the Profile operations # preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked. diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 6d813d931..4a8429319 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -13,7 +13,7 @@ default = [] # Optional authored texture/HDR bytes; the command registry and metadata remain available. bundled-render-library = [] raytrace-cpu = ["solidcraft-render/raytrace-cpu"] -# Link SMLib into the kernel (solidcraft-kernel/smlib; needs scripts/fetch-smlib.sh). +# Also link the C++ SMLib kernel (solidcraft-kernel/smlib, off by default; needs scripts/fetch-smlib.sh). smlib = ["solidcraft-kernel/smlib"] # Turns on the parameter-contract audit recorder (`SOLIDCRAFT_PARAM_AUDIT`, see # `src/schema/record.rs`), for checking a new contract against the whole test suite. diff --git a/crates/engine/src/cmd/diagnostics.rs b/crates/engine/src/cmd/diagnostics.rs index 3439bdbaf..b31a65619 100644 --- a/crates/engine/src/cmd/diagnostics.rs +++ b/crates/engine/src/cmd/diagnostics.rs @@ -78,7 +78,7 @@ pub(crate) fn kernel_asked(choice: &Value, built: bool) -> Result Option { /// Whether this build links SMLib (the `smlib` feature). pub fn smlib_built() -> bool { - cfg!(feature = "smlib") + solidcraft_smlib::built() } /// Where every kernel call that fails from now on leaves its inputs for replay diff --git a/crates/smlib/Cargo.toml b/crates/smlib/Cargo.toml index c92792c19..bc849548a 100644 --- a/crates/smlib/Cargo.toml +++ b/crates/smlib/Cargo.toml @@ -20,8 +20,10 @@ type_complexity = "allow" neg_cmp_op_on_partial_ord = "allow" [features] -# Compile and link SMLib. Off, the crate builds without a C++ toolchain or the SMLib sources, and -# every call returns `Error::NotBuilt`, so workspace-wide builds and checks need neither. +# Compile and link the C++ SMLib (the apps' and the kernel's `smlib` feature turns it on; off by +# default). Off, the crate builds without a C++ toolchain, the SMLib sources or a prebuilt library, +# build.rs does nothing, solidcore answers every operation, and a call asked of the C++ kernel +# returns `Error::NotBuilt`. native = [] [dependencies] diff --git a/crates/smlib/src/lib.rs b/crates/smlib/src/lib.rs index 82d2a872f..213ac9708 100644 --- a/crates/smlib/src/lib.rs +++ b/crates/smlib/src/lib.rs @@ -6,8 +6,10 @@ //! the handles, turns status codes into [`Error`], and sends SMLib's one-line-per-call log to the //! `log` crate (target `smlib`). //! -//! Built without the `native` feature, the crate needs no C++ toolchain and every call returns -//! [`Error::NotBuilt`]. +//! The `native` feature (the apps' `smlib`, off by default) compiles and links the C++ kernel. +//! Without it the crate needs no C++ toolchain, no SMLib sources and no prebuilt library: solidcore +//! answers every operation, and asking for the C++ kernel (`SOLIDCRAFT_KERNEL=smlib`) makes every +//! call that would reach it return [`Error::NotBuilt`], never the other kernel's answer. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] use std::ffi::CString; @@ -77,7 +79,8 @@ pub enum Error { Exception, /// The C API is a different version from the one this crate was written against. Version(i32), - /// This build has no SMLib (the `native` feature is off). + /// The C++ kernel was asked for on a build without it (the `native` feature, which the apps' + /// `smlib` feature turns on, is off): [`NOT_BUILT`]. NotBuilt, /// A B-rep record SMLib cannot take (see [`import::Unsupported`]). Record(import::Unsupported), @@ -123,7 +126,7 @@ impl std::fmt::Display for Error { } Error::Exception => write!(f, "SMLib raised an internal exception"), Error::Version(v) => write!(f, "SMLib C API version {v}, expected {}", ffi::ABI_VERSION), - Error::NotBuilt => write!(f, "not supported yet: this build has no SMLib"), + Error::NotBuilt => write!(f, "{NOT_BUILT}"), Error::Record(u) => write!(f, "{u}"), Error::Solidcore(e) => write!(f, "solidcore: {e}"), Error::Internal(e) => write!(f, "solidcore failed internally in {e}"), @@ -136,6 +139,14 @@ impl std::error::Error for Error {} pub type Result = std::result::Result; +/// What [`Error::NotBuilt`] says: the C++ kernel is opt-in at build time, and how to opt in. +pub const NOT_BUILT: &str = "this build doesn't include the C++ SMLib kernel; build with --features smlib"; + +/// Whether this build links the C++ SMLib kernel (the `native` feature). +pub const fn built() -> bool { + cfg!(feature = "native") +} + fn error(s: i32) -> Error { match s { ffi::ERR_ARG | ffi::ERR_BUFFER => Error::Argument, diff --git a/crates/smlib/src/route.rs b/crates/smlib/src/route.rs index 6daf9c31a..60cff4d62 100644 --- a/crates/smlib/src/route.rs +++ b/crates/smlib/src/route.rs @@ -4,7 +4,8 @@ //! The C++ kernel is an opt-in: for a test, a rig or a user who wants the old behaviour. A call that //! fails on solidcore fails, and a path solidcore has no answer for says it is not supported: there is //! no fallback to the other kernel at run time, which would hide its bugs. Asking for the C++ kernel on -//! a build without it (wasm included) stays on it too: every call answers [`crate::Error::NotBuilt`]. +//! a build without it (the default, wasm included) stays on it too: every call answers +//! [`crate::Error::NotBuilt`], which says to build with `--features smlib`. //! //! `SOLIDCRAFT_KERNEL=smlib|solidcore` chooses for the whole process (the rig, the test matrix); //! otherwise the preference ([`set_choice`]) does, and it defaults to solidcore. @@ -104,6 +105,9 @@ fn environment() -> Option { if c.is_none() { log::warn!(target: "smlib", "SOLIDCRAFT_KERNEL={v}: not smlib or solidcore; ignored"); } + if c == Some(Backend::Smlib) && !crate::built() { + log::warn!(target: "smlib", "SOLIDCRAFT_KERNEL=smlib: {}; every operation will be refused", crate::NOT_BUILT); + } c }) } diff --git a/crates/smlib/src/tests.rs b/crates/smlib/src/tests.rs index edd54fbb8..41ac8e13e 100644 --- a/crates/smlib/src/tests.rs +++ b/crates/smlib/src/tests.rs @@ -144,6 +144,9 @@ fn without_native_every_call_says_so() { // solidcore builds a box without SMLib; asking the C++ kernel about it says there is none let b = core::make_box([0.0; 3], [1.0; 3]).map(Brep::made_core).unwrap(); assert_eq!(b.cpp().err(), Some(Error::NotBuilt)); + // and says how to get it + assert!(!built()); + assert!(Error::NotBuilt.to_string().contains("--features smlib"), "{}", Error::NotBuilt); } /// The dump folder is process-wide: the tests that set it take turns. diff --git a/tests/kernel-cases/README.md b/tests/kernel-cases/README.md index 0dd65e836..312012161 100644 --- a/tests/kernel-cases/README.md +++ b/tests/kernel-cases/README.md @@ -10,12 +10,13 @@ runs them on solidcore; `--kernel smlib` runs them on C++ and writes cargo xtask kernel-cases # every case, on solidcore cargo xtask kernel-cases --cli target/release/solidcraft-cli --out /tmp/run # with a CLI already built cargo xtask kernel-cases --kernel both --cli target/release/solidcraft-cli # SMLib and solidcore side by side -cargo run -p solidcraft-cli --features smlib -- kernel-cases tests/kernel-cases/booleans/ # one case +cargo run -p solidcraft-cli -- kernel-cases tests/kernel-cases/booleans/ # one case ``` -The CLI links SMLib by default (`scripts/fetch-smlib.sh` fetches the prebuilt library first); -built with `--no-default-features`, solidcore still answers every operation, and asking for SMLib -answers that it is not built. The run's output folder (`--out`, default `target/kernel-cases`) +The CLI builds with solidcore alone by default. The C++ SMLib kernel is opt-in at build time: +`--features smlib` links it (`scripts/fetch-smlib.sh` fetches the prebuilt library first), and +`--kernel smlib` or `both` without `--cli` builds the CLI that way. On a build without it, asking +for SMLib answers that this build doesn't include it. The run's output folder (`--out`, default `target/kernel-cases`) gets `results.json`, every case's full result, and `dumps/-/`: the inputs of each SMLib call a case failed in, for replay (the CLI's `--dumps=DIR`, which the rig always passes). `--validation MODE` runs the cases' features under that mode of SMLib's check @@ -137,7 +138,7 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage Every area is a regression suite: each case is one small case of one issue, with its exact answer. `.github/workflows/kernel-rig.yml` runs them, and the ladder, on pull requests (not drafts). A plan job (`scripts/ci/kernel_rig_plan.py`) picks the areas from the changed files, one -job builds `solidcraft-cli` (release, SMLib) and the rig's runner, and a matrix of shards, one per +job builds `solidcraft-cli` (release, `--features smlib`, for the ladder's `--kernel smlib`) and the rig's runner, and a matrix of shards, one per selected area plus the ladder, runs them in parallel; the `kernel-rig` check passes when every selected shard does (and when none was selected). Pushes to main and develop, merge groups, the release gate (`release-gate.yml`) and a run by hand run every area. Each area's shard runs: diff --git a/xtask/src/kernel_cases.rs b/xtask/src/kernel_cases.rs index 9ba0c5cd8..5efcd9f82 100644 --- a/xtask/src/kernel_cases.rs +++ b/xtask/src/kernel_cases.rs @@ -166,13 +166,20 @@ fn wait_draining(mut child: std::process::Child, timeout: Duration) -> Result &'static [&'static str] { + if kernels.contains(&"smlib") { &["--features", "smlib"] } else { &[] } +} + fn run_case(o: &Options, case: &Path, kernel: &str) -> Value { let rel = case.strip_prefix(root()).unwrap_or(case).to_path_buf(); let mut cmd = match &o.cli { Some(bin) => Command::new(bin), None => { let mut c = cargo(); - c.args(["run", "-q", "--release", "-p", "solidcraft-cli", "--features", "smlib", "--"]); + c.args(["run", "-q", "--release", "-p", "solidcraft-cli"]).args(cli_features(&o.kernels)).arg("--"); c } }; @@ -827,6 +834,16 @@ mod tests { assert_eq!(options(&["--kernel", "both"], "x").unwrap().kernels, ["smlib", "solidcore"]); } + /// Only a run that asks for the C++ kernel builds the CLI with it. + #[test] + fn the_cli_is_built_with_the_cpp_kernel_only_when_a_run_asks_for_it() { + let features = |args: &[&str]| cli_features(&options(args, "x").unwrap().kernels).to_vec(); + assert!(features(&[]).is_empty()); + assert!(features(&["--kernel", "solidcore"]).is_empty()); + assert_eq!(features(&["--kernel", "smlib"]), ["--features", "smlib"]); + assert_eq!(features(&["--kernel", "both"]), ["--features", "smlib"]); + } + fn gate_on(text: &str, cases: &[(&str, &str)]) -> Result { let names: Vec = cases.iter().map(|(c, _)| c.to_string()).collect(); let statuses: Vec<&str> = cases.iter().map(|(_, s)| *s).collect(); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 6e83b8d7c..949c2e1b3 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -58,7 +58,9 @@ commands: --kernel solidcore|smlib|both (default solidcore) sets SOLIDCRAFT_KERNEL per case: solidcore keeps results.json, dumps/ and report.md in DIR/solidcore/; smlib writes docs/kernel-cases.md; both runs each case on each, and also writes the - side by side to docs/kernel-cases-backends.md and DIR/backends.json + side by side to docs/kernel-cases-backends.md and DIR/backends.json; smlib and + both need the C++ kernel, so without --cli they build the CLI with --features smlib + (a --cli built without it reports every case as unsupported) --known-failing FILE: a regression gate instead of the report: fails on a case that fails and FILE does not list, and on a listed case that passes (tests/kernel-cases//KNOWN_FAILING, CI's Kernel rig workflow); it grades