Make the C++ SMLib kernel opt-in at build time (feature smlib, off by default)

solidcore, the pure-Rust kernel, is always built and stays the default. The
`smlib` feature (apps, engine, kernel; solidcraft-smlib's `native`) now has
to be asked for to compile and link SMLib: the app, the CLI and the web app
drop it from `default`, and solidcraft-gpu's dev-dependency no longer forces
it on. Without it no C++ toolchain, fetch-smlib.sh, prebuilt or build.rs work
is needed.

On a build without it, asking for the C++ kernel (SOLIDCRAFT_KERNEL=smlib,
diagnostics.kernel, the rig) is refused with Error::NotBuilt, which now says
"this build doesn't include the C++ SMLib kernel; build with --features
smlib", and SOLIDCRAFT_KERNEL=smlib logs that once. `cargo xtask
kernel-cases` builds the CLI with --features smlib only when --kernel smlib
or both asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdcgEtwSrdwZxqTQp5MfnU
This commit is contained in:
brett19
2026-11-02 10:56:02 -07:00
parent d93a8bfd17
commit 5a93e6c4d2
16 changed files with 76 additions and 34 deletions
+4 -3
View File
@@ -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.
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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.
+4 -3
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -78,7 +78,7 @@ pub(crate) fn kernel_asked(choice: &Value, built: bool) -> Result<solidcraft_ker
use solidcraft_kernel::kernel_route::Backend;
let asked = choice.as_str().and_then(Backend::from_id).ok_or_else(|| bad("diagnostics.kernel", "`choice` is solidcore or smlib"))?;
if asked == Backend::Smlib && !built {
return Err(EngineError::Disabled("diagnostics.kernel".into(), "this build has no C++ SMLib kernel".into()));
return Err(EngineError::Disabled("diagnostics.kernel".into(), "this build doesn't include the C++ SMLib kernel; build with --features smlib".into()));
}
Ok(asked)
}
+3 -3
View File
@@ -26,9 +26,9 @@ wgpu = { workspace = true, features = ["metal", "vulkan", "dx12", "gles"] }
[dev-dependencies]
image = { workspace = true }
solidcraft-kernel = { workspace = true }
# Headless pictures of real designs (capture tests), built by the real kernel: SMLib
# (scripts/fetch-smlib.sh), as in the apps.
solidcraft-engine = { workspace = true, features = ["bundled-render-library", "smlib"] }
# Headless pictures of real designs (capture tests), built by the real kernel: solidcore, as in the
# apps (the C++ SMLib kernel only when the build turns on solidcraft-engine/smlib).
solidcraft-engine = { workspace = true, features = ["bundled-render-library"] }
solidcraft-io = { workspace = true }
serde_json = { workspace = true }
# The HLSL the D3D12 backend compiles (tests check it suits FXC).
+3 -3
View File
@@ -9,9 +9,9 @@ license.workspace = true
workspace = true
[features]
# Link NVIDIA's SMLib (tangency-labs/smlib) into the kernel; needs a C++ toolchain and
# scripts/fetch-smlib.sh (ADR 0002). Without it every geometric operation answers that SMLib
# was not built.
# Also link NVIDIA's C++ SMLib (tangency-labs/smlib), off by default; needs a C++ toolchain and
# scripts/fetch-smlib.sh (ADR 0002). solidcore, the Rust port, answers every operation either way;
# without this, asking for the C++ kernel (SOLIDCRAFT_KERNEL=smlib) is refused as not built.
smlib = ["solidcraft-smlib/native"]
[dependencies]
+1 -1
View File
@@ -56,7 +56,7 @@ fn smlib_validity(body: &Body) -> Option<SmlibCheck> {
/// 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
+4 -2
View File
@@ -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]
+15 -4
View File
@@ -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<T> = std::result::Result<T, Error>;
/// 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,
+5 -1
View File
@@ -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<Backend> {
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
})
}
+3
View File
@@ -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.
+6 -5
View File
@@ -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/<id> # one case
cargo run -p solidcraft-cli -- kernel-cases tests/kernel-cases/booleans/<id> # 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/<area>-<case>/`: 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:
+18 -1
View File
@@ -166,13 +166,20 @@ fn wait_draining(mut child: std::process::Child, timeout: Duration) -> Result<Op
Ok(Some(std::process::Output { status, stdout: join(stdout)?, stderr: join(stderr)? }))
}
/// The features `cargo run` builds the CLI with for a run on `kernels`: the C++ kernel is opt-in at
/// build time (`--features smlib`), so only a run that asks for it builds it. One build for the
/// whole run (`both` included), so the cases don't rebuild the CLI between kernels.
fn cli_features(kernels: &[&str]) -> &'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<String, String> {
let names: Vec<String> = cases.iter().map(|(c, _)| c.to_string()).collect();
let statuses: Vec<&str> = cases.iter().map(|(_, s)| *s).collect();
+3 -1
View File
@@ -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/<area>/KNOWN_FAILING, CI's Kernel rig workflow); it grades