diff --git a/.site/spi/.spdev/overrides.py b/.site/spi/.spdev/overrides.py index a170a0fb63..6da5db9f4f 100644 --- a/.site/spi/.spdev/overrides.py +++ b/.site/spi/.spdev/overrides.py @@ -36,7 +36,7 @@ def inject_credentials(super_script_list: spdev.shell.Script) -> spdev.shell.Scr "1", "sed", "-i", - '"s|https://github.com|https://$GITHUB_SPFS_PULL_USERNAME:$GITHUB_SPFS_PULL_PASSWORD@github.com|"', + '"s|https://github.com/spkenv|https://$GITHUB_SPFS_PULL_USERNAME:$GITHUB_SPFS_PULL_PASSWORD@github.com/spkenv|"', ) ) diff --git a/Cargo.lock b/Cargo.lock index 46bb44f784..06cea3b940 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,14 +29,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.8" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.3", "once_cell", "version_check", - "zerocopy 0.7.32", + "zerocopy", ] [[package]] @@ -364,6 +365,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -652,6 +665,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.14.0" @@ -700,7 +722,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.10", "once_cell", "tiny-keccak", ] @@ -1127,6 +1149,15 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "elsa" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1161,6 +1192,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "faccess" version = "0.2.4" @@ -1196,6 +1238,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flatbuffers" version = "25.2.10" @@ -1266,6 +1314,12 @@ dependencies = [ "serde_yaml 0.8.26", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "fuser" version = "0.15.1" @@ -1279,7 +1333,7 @@ dependencies = [ "page_size", "pkg-config", "smallvec", - "zerocopy 0.8.24", + "zerocopy", ] [[package]] @@ -1395,7 +1449,19 @@ checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -1826,15 +1892,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2038,7 +2095,7 @@ dependencies = [ "hermit-abi 0.3.9", "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2306,6 +2363,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2420,7 +2483,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", + "indexmap 2.9.0", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset 0.5.7", "indexmap 2.9.0", ] @@ -2673,11 +2746,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck 0.5.0", - "itertools 0.12.1", + "itertools 0.14.0", "log", "multimap", "once_cell", - "petgraph", + "petgraph 0.6.4", "prettyplease", "prost", "prost-types", @@ -2693,7 +2766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.100", @@ -2738,6 +2811,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -2765,7 +2850,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.10", ] [[package]] @@ -2830,7 +2915,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", + "getrandom 0.2.10", "redox_syscall 0.2.16", "thiserror", ] @@ -2947,6 +3032,24 @@ dependencies = [ "winreg", ] +[[package]] +name = "resolvo" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba027c8e5dd4b5e5a690cfcfb3900d5ffe6985adb048cbd111d5aa596a6c0c8" +dependencies = [ + "ahash", + "bitvec", + "elsa", + "event-listener", + "futures", + "indexmap 2.9.0", + "itertools 0.14.0", + "petgraph 0.7.1", + "tokio", + "tracing", +] + [[package]] name = "ring" version = "0.17.14" @@ -2955,7 +3058,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.10", "libc", "untrusted", "windows-sys 0.52.0", @@ -4080,6 +4183,7 @@ dependencies = [ "futures", "itertools 0.14.0", "miette", + "rstest", "spfs", "spk-build", "spk-cli-common", @@ -4117,6 +4221,7 @@ dependencies = [ "miette", "spfs", "spk-cli-common", + "spk-solve", "tokio", "tracing", ] @@ -4133,6 +4238,7 @@ dependencies = [ "spk-cli-common", "spk-exec", "spk-schema", + "spk-solve", "tokio", ] @@ -4150,6 +4256,7 @@ dependencies = [ "spk-build", "spk-cli-common", "spk-schema", + "spk-solve", "spk-storage", "tempfile", "tokio", @@ -4197,6 +4304,7 @@ dependencies = [ "spfs", "spk-cli-common", "spk-exec", + "spk-solve", "spk-storage", "tokio", "tracing", @@ -4380,6 +4488,7 @@ dependencies = [ "spk-schema-foundation", "tap", "thiserror", + "variantly", ] [[package]] @@ -4411,6 +4520,7 @@ dependencies = [ "crossterm", "ctrlc", "dyn-clone", + "enum_dispatch", "futures", "itertools 0.14.0", "miette", @@ -4418,6 +4528,7 @@ dependencies = [ "num-format", "once_cell", "priority-queue", + "resolvo", "rstest", "sentry", "serde_json", @@ -4433,9 +4544,12 @@ dependencies = [ "spk-storage", "statsd 0.15.0", "strip-ansi-escapes 0.2.0", + "strum", + "tap", "thiserror", "tokio", "tracing", + "variantly", ] [[package]] @@ -4602,6 +4716,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -5110,9 +5230,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -5121,9 +5241,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -5145,9 +5265,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -5384,7 +5504,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom", + "getrandom 0.2.10", ] [[package]] @@ -5393,7 +5513,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ - "getrandom", + "getrandom 0.2.10", "serde", ] @@ -5494,6 +5614,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -5862,6 +5991,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xattr" version = "1.0.1" @@ -5880,33 +6027,13 @@ dependencies = [ "linked-hash-map", ] -[[package]] -name = "zerocopy" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" -dependencies = [ - "zerocopy-derive 0.7.32", -] - [[package]] name = "zerocopy" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ - "zerocopy-derive 0.8.24", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "zerocopy-derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f48fba4fa0..52a40a8820 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,8 +87,9 @@ prost = "0.13" rand = "0.8.5" regex = "1.6" relative-path = "1.3" +resolvo = "0.9.1" ring = "0.17.14" -rstest = "0.25" +rstest = "0.25.0" sentry = { version = "0.34.0", default-features = false, features = [ # all the default features except `debug-images` which causes a deadlock on # centos 7: https://github.com/getsentry/sentry-rust/issues/358 diff --git a/crates/spk-build/src/archive_test.rs b/crates/spk-build/src/archive_test.rs index 2ed93eebbf..134f0cf9f4 100644 --- a/crates/spk-build/src/archive_test.rs +++ b/crates/spk-build/src/archive_test.rs @@ -5,14 +5,25 @@ use rstest::rstest; use spk_schema::foundation::option_map; use spk_schema::{Package, recipe}; +use spk_solve::SolverImpl; use spk_storage::export_package; use spk_storage::fixtures::*; use crate::{BinaryPackageBuilder, BuildSource}; +fn step_solver() -> SolverImpl { + SolverImpl::Step(spk_solve::StepSolver::default()) +} + +fn resolvo_solver() -> SolverImpl { + SolverImpl::Resolvo(spk_solve::ResolvoSolver::default()) +} + #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_archive_create_parents() { +async fn test_archive_create_parents(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let spec = recipe!( { @@ -21,7 +32,7 @@ async fn test_archive_create_parents() { } ); rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .build_and_publish(option_map! {}, &*rt.tmprepo) .await diff --git a/crates/spk-build/src/build/binary.rs b/crates/spk-build/src/build/binary.rs index 7f6d5aa54d..0d96414803 100644 --- a/crates/spk-build/src/build/binary.rs +++ b/crates/spk-build/src/build/binary.rs @@ -37,7 +37,7 @@ use spk_schema::{ }; use spk_solve::graph::Graph; use spk_solve::solution::Solution; -use spk_solve::{BoxedResolverCallback, Named, ResolverCallback, StepSolver}; +use spk_solve::{DecisionFormatter, Named, SolverExt, SolverMut}; use spk_storage as storage; use crate::report::{BuildOutputReport, BuildReport, BuildSetupReport}; @@ -117,8 +117,9 @@ where /// /// ```no_run /// # use spk_schema::{recipe, foundation::option_map}; +/// # use spk_solve::StepSolver; /// # async fn demo() { -/// spk_build::BinaryPackageBuilder::from_recipe(recipe!({ +/// spk_build::BinaryPackageBuilder::<_, StepSolver>::from_recipe(recipe!({ /// "pkg": "my-pkg", /// "build": {"script": "echo hello, world"}, /// })) @@ -127,14 +128,14 @@ where /// .unwrap(); /// # } /// ``` -pub struct BinaryPackageBuilder<'a, Recipe> { +pub struct BinaryPackageBuilder { prefix: PathBuf, recipe: Recipe, source: BuildSource, - solver: StepSolver, + solver: Solver, environment: HashMap, - source_resolver: BoxedResolverCallback<'a>, - build_resolver: BoxedResolverCallback<'a>, + source_solve_formatter: DecisionFormatter, + build_solve_formatter: DecisionFormatter, last_solve_graph: Arc>, repos: Vec>, interactive: bool, @@ -142,29 +143,30 @@ pub struct BinaryPackageBuilder<'a, Recipe> { allow_circular_dependencies: bool, } -impl<'a, Recipe> BinaryPackageBuilder<'a, Recipe> +impl BinaryPackageBuilder where Recipe: spk_schema::Recipe, - Recipe::Output: Package + serde::Serialize, { - /// Create a new builder that builds a binary package from the given recipe - pub fn from_recipe(recipe: Recipe) -> Self { + /// Create a new builder that builds a binary package from the given recipe. + /// + /// Use the provided solver. + pub fn from_recipe_with_solver(recipe: Recipe, solver: Solver) -> Self { let source = BuildSource::SourcePackage(recipe.ident().to_build_ident(Build::Source).into()); Self { recipe, source, prefix: PathBuf::from("/spfs"), - solver: StepSolver::default(), + solver, environment: Default::default(), #[cfg(test)] - source_resolver: Box::new(spk_solve::DecisionFormatter::new_testing()), + source_solve_formatter: DecisionFormatter::new_testing(), #[cfg(not(test))] - source_resolver: Box::new(spk_solve::DefaultResolver {}), + source_solve_formatter: DecisionFormatter::default(), #[cfg(test)] - build_resolver: Box::new(spk_solve::DecisionFormatter::new_testing()), + build_solve_formatter: DecisionFormatter::new_testing(), #[cfg(not(test))] - build_resolver: Box::new(spk_solve::DefaultResolver {}), + build_solve_formatter: DecisionFormatter::default(), last_solve_graph: Arc::new(tokio::sync::RwLock::new(Graph::new())), repos: Default::default(), interactive: false, @@ -172,7 +174,27 @@ where allow_circular_dependencies: false, } } +} +impl BinaryPackageBuilder +where + Recipe: spk_schema::Recipe, + Solver: Default, +{ + /// Create a new builder that builds a binary package from the given recipe. + /// + /// This will use a default instance of the generic solver type. + pub fn from_recipe(recipe: Recipe) -> Self { + Self::from_recipe_with_solver(recipe, Solver::default()) + } +} + +impl BinaryPackageBuilder +where + Recipe: spk_schema::Recipe, + Recipe::Output: Package + serde::Serialize, + Solver: SolverExt + SolverMut, +{ /// Allow circular dependencies when resolving dependencies. /// /// Normally if a build dependency has a dependency on the package being @@ -214,31 +236,15 @@ where self } - /// Provide a function that will be called when resolving the source package. - /// - /// This function should run the provided solver runtime to - /// completion, returning the final result. This function - /// is useful for introspecting and reporting on the solve - /// process as needed. - pub fn with_source_resolver(&mut self, resolver: F) -> &mut Self - where - F: ResolverCallback + 'a, - { - self.source_resolver = Box::new(resolver); + /// Provide a formatter to use when resolving the source environment. + pub fn with_source_formatter(&mut self, formatter: DecisionFormatter) -> &mut Self { + self.source_solve_formatter = formatter; self } - /// Provide a function that will be called when resolving the build environment. - /// - /// This function should run the provided solver runtime to - /// completion, returning the final result. This function - /// is useful for introspecting and reporting on the solve - /// process as needed. - pub fn with_build_resolver(&mut self, resolver: F) -> &mut Self - where - F: ResolverCallback + 'a, - { - self.build_resolver = Box::new(resolver); + /// Provide a formatter to use when resolving the build environment. + pub fn with_build_formatter(&mut self, formatter: DecisionFormatter) -> &mut Self { + self.build_solve_formatter = formatter; self } @@ -441,8 +447,10 @@ where self.solver.add_request(request.into()); - let (solution, graph) = self.source_resolver.solve(&self.solver).await?; - self.last_solve_graph = graph; + let solution = self + .solver + .run_and_print_resolve(&self.source_solve_formatter) + .await?; Ok(solution) } @@ -466,8 +474,10 @@ where self.solver.add_request(request.clone()); } - let (solution, graph) = self.build_resolver.solve(&self.solver).await?; - self.last_solve_graph = graph; + let solution = self + .solver + .run_and_print_resolve(&self.build_solve_formatter) + .await?; Ok(solution) } diff --git a/crates/spk-build/src/build/binary_test.rs b/crates/spk-build/src/build/binary_test.rs index 8ee4583db4..ce0a5ec9b6 100644 --- a/crates/spk-build/src/build/binary_test.rs +++ b/crates/spk-build/src/build/binary_test.rs @@ -13,7 +13,7 @@ use spk_schema::foundation::ident_component::Component; use spk_schema::foundation::{opt_name, option_map}; use spk_schema::ident::{PkgRequest, RangeIdent, Request}; use spk_schema::{ComponentSpecList, FromYaml, OptionMap, Package, Recipe, SpecRecipe, recipe}; -use spk_solve::Solution; +use spk_solve::{Solution, SolverImpl}; use spk_storage::fixtures::*; use spk_storage::{self as storage, Repository}; @@ -97,9 +97,19 @@ fn test_var_with_build_assigns_build() { if name.as_str() == "my-dep" && digest.digest() == "QYB6QLCN")); } +fn step_solver() -> SolverImpl { + SolverImpl::Step(spk_solve::StepSolver::default()) +} + +fn resolvo_solver() -> SolverImpl { + SolverImpl::Resolvo(spk_solve::ResolvoSolver::default()) +} + #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_workdir(tmpdir: tempfile::TempDir) { +async fn test_build_workdir(tmpdir: tempfile::TempDir, #[case] solver: SolverImpl) { let rt = spfs_runtime().await; let out_file = tmpdir.path().join("out.log"); let recipe = recipe!({ @@ -113,7 +123,7 @@ async fn test_build_workdir(tmpdir: tempfile::TempDir) { }); rt.tmprepo.publish_recipe(&recipe).await.unwrap(); - BinaryPackageBuilder::from_recipe(recipe) + BinaryPackageBuilder::from_recipe_with_solver(recipe, solver) .with_source(BuildSource::LocalPath(tmpdir.path().to_owned())) .build_and_publish(&option_map! {}, &*rt.tmprepo) .await @@ -129,8 +139,10 @@ async fn test_build_workdir(tmpdir: tempfile::TempDir) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_package_options() { +async fn test_build_package_options(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let dep_spec = recipe!( {"pkg": "dep/1.0.0", "build": {"script": "touch /spfs/dep-file"}} @@ -161,7 +173,7 @@ async fn test_build_package_options() { rt.tmprepo.publish_recipe(&dep_spec).await.unwrap(); - BinaryPackageBuilder::from_recipe(dep_spec) + BinaryPackageBuilder::from_recipe_with_solver(dep_spec, solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -175,7 +187,7 @@ async fn test_build_package_options() { // specific option takes precedence "top.dep" => "1.0.0", }; - let (spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(variant, &*rt.tmprepo) @@ -198,7 +210,10 @@ async fn test_build_package_options() { #[case::camel_case("fromBuildEnv")] #[case::lower_case("frombuildenv")] #[tokio::test] -async fn test_build_package_pinning(#[case] from_build_env_str: &str) { +async fn test_build_package_pinning( + #[case] from_build_env_str: &str, + #[values(step_solver(), resolvo_solver())] solver: SolverImpl, +) { let rt = spfs_runtime().await; let dep_spec = recipe!( {"pkg": "dep/1.0.0", "build": {"script": "touch /spfs/dep-file"}} @@ -217,14 +232,14 @@ async fn test_build_package_pinning(#[case] from_build_env_str: &str) { ); rt.tmprepo.publish_recipe(&dep_spec).await.unwrap(); - BinaryPackageBuilder::from_recipe(dep_spec) + BinaryPackageBuilder::from_recipe_with_solver(dep_spec, solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) .await .unwrap(); rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) @@ -242,8 +257,10 @@ async fn test_build_package_pinning(#[case] from_build_env_str: &str) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_package_pinning_optional_requirement() { +async fn test_build_package_pinning_optional_requirement(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let dep1_spec = recipe!( {"pkg": "dep1/1.0.0", "build": {"script": "touch /spfs/dep-file"}} @@ -273,7 +290,7 @@ async fn test_build_package_pinning_optional_requirement() { for dep_spec in [dep1_spec, dep2_spec] { rt.tmprepo.publish_recipe(&dep_spec).await.unwrap(); - BinaryPackageBuilder::from_recipe(dep_spec) + BinaryPackageBuilder::from_recipe_with_solver(dep_spec, solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) @@ -285,7 +302,7 @@ async fn test_build_package_pinning_optional_requirement() { let default_variants = spec.default_variants(&OptionMap::default()); for (variant, expected_dep) in default_variants.iter().zip(["dep1", "dep2"].iter()) { - let (spec, _) = BinaryPackageBuilder::from_recipe(spec.clone()) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec.clone(), solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(variant, &*rt.tmprepo) @@ -304,8 +321,12 @@ async fn test_build_package_pinning_optional_requirement() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_package_pinning_optional_requirement_without_frombuildenv() { +async fn test_build_package_pinning_optional_requirement_without_frombuildenv( + #[case] solver: SolverImpl, +) { let rt = spfs_runtime().await; let dep1_spec = recipe!( {"pkg": "dep1/1.0.0", "build": {"script": "touch /spfs/dep-file"}} @@ -335,7 +356,7 @@ async fn test_build_package_pinning_optional_requirement_without_frombuildenv() for dep_spec in [dep1_spec, dep2_spec] { rt.tmprepo.publish_recipe(&dep_spec).await.unwrap(); - BinaryPackageBuilder::from_recipe(dep_spec) + BinaryPackageBuilder::from_recipe_with_solver(dep_spec, solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) @@ -347,7 +368,7 @@ async fn test_build_package_pinning_optional_requirement_without_frombuildenv() let default_variants = spec.default_variants(&OptionMap::default()); for (variant, expected_dep) in default_variants.iter().zip(["dep1", "dep2"].iter()) { - let (spec, _) = BinaryPackageBuilder::from_recipe(spec.clone()) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec.clone(), solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(variant, &*rt.tmprepo) @@ -366,8 +387,10 @@ async fn test_build_package_pinning_optional_requirement_without_frombuildenv() } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_var_pinning_optional_requirement() { +async fn test_build_var_pinning_optional_requirement(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let dep2_spec = recipe!( {"pkg": "dep2/1.0.0", "build": { @@ -397,7 +420,7 @@ async fn test_build_var_pinning_optional_requirement() { for dep_spec in [dep2_spec] { rt.tmprepo.publish_recipe(&dep_spec).await.unwrap(); - BinaryPackageBuilder::from_recipe(dep_spec) + BinaryPackageBuilder::from_recipe_with_solver(dep_spec, solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) @@ -417,7 +440,7 @@ async fn test_build_var_pinning_optional_requirement() { ] .into_iter(), ) { - let (spec, _) = BinaryPackageBuilder::from_recipe(spec.clone()) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec.clone(), solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(variant, &*rt.tmprepo) @@ -435,8 +458,10 @@ async fn test_build_var_pinning_optional_requirement() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_package_missing_deps() { +async fn test_build_package_missing_deps(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let spec = recipe!( { @@ -449,7 +474,7 @@ async fn test_build_package_missing_deps() { // should not fail to resolve build env and build even though // runtime dependency is missing in the current repos - BinaryPackageBuilder::from_recipe(spec) + BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) @@ -458,8 +483,10 @@ async fn test_build_package_missing_deps() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_var_pinning() { +async fn test_build_var_pinning(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let dep_spec = recipe!( { @@ -493,13 +520,13 @@ async fn test_build_var_pinning() { rt.tmprepo.publish_recipe(&dep_spec).await.unwrap(); rt.tmprepo.publish_recipe(&spec).await.unwrap(); - BinaryPackageBuilder::from_recipe(dep_spec) + BinaryPackageBuilder::from_recipe_with_solver(dep_spec, solver.clone()) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) .await .unwrap(); - let (spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) @@ -520,8 +547,10 @@ async fn test_build_var_pinning() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_bad_options() { +async fn test_build_bad_options(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let spec = recipe!( { @@ -536,7 +565,7 @@ async fn test_build_bad_options() { ); rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let res = BinaryPackageBuilder::from_recipe(spec) + let res = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .build_and_publish(option_map! {"debug" => "false"}, &*rt.tmprepo) .await; @@ -551,8 +580,10 @@ async fn test_build_bad_options() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_package_source_cleanup() { +async fn test_build_package_source_cleanup(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let spec = recipe!( { @@ -580,7 +611,7 @@ async fn test_build_package_source_cleanup() { .await .unwrap(); - let (pkg, _) = BinaryPackageBuilder::from_recipe(spec) + let (pkg, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) .await @@ -616,8 +647,10 @@ async fn test_build_package_source_cleanup() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_filters_reset_files() { +async fn test_build_filters_reset_files(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; // Create a package that can be used as a dependency... @@ -642,7 +675,7 @@ async fn test_build_filters_reset_files() { .await .unwrap(); - let _ = BinaryPackageBuilder::from_recipe(spec) + let _ = BinaryPackageBuilder::from_recipe_with_solver(spec, solver.clone()) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) .await @@ -676,7 +709,7 @@ async fn test_build_filters_reset_files() { .await .unwrap(); - let (pkg, _) = BinaryPackageBuilder::from_recipe(spec) + let (pkg, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) .await @@ -751,8 +784,10 @@ async fn test_default_build_component() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_components_metadata() { +async fn test_build_components_metadata(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let spec = recipe!( { @@ -767,7 +802,7 @@ async fn test_build_components_metadata() { } ); rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (spec, _) = BinaryPackageBuilder::from_recipe(spec.clone()) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec.clone(), solver) .with_source(BuildSource::LocalPath(".".into())) .build_and_publish(option_map! {}, &*rt.tmprepo) .await @@ -795,8 +830,10 @@ async fn test_build_components_metadata() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_add_startup_files(tmpdir: tempfile::TempDir) { +async fn test_build_add_startup_files(tmpdir: tempfile::TempDir, #[case] solver: SolverImpl) { let rt = spfs_runtime().await; let recipe = recipe!( { @@ -815,7 +852,7 @@ async fn test_build_add_startup_files(tmpdir: tempfile::TempDir) { let spec = recipe .generate_binary_build(&option_map! {}, &Solution::default()) .unwrap(); - BinaryPackageBuilder::from_recipe(recipe) + BinaryPackageBuilder::from_recipe_with_solver(recipe, solver) .with_prefix(tmpdir.path().into()) .generate_startup_scripts(&spec) .unwrap(); @@ -866,8 +903,10 @@ async fn test_build_multiple_priority_startup_files() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_priority_startup_files(tmpdir: tempfile::TempDir) { +async fn test_build_priority_startup_files(tmpdir: tempfile::TempDir, #[case] solver: SolverImpl) { let rt = spfs_runtime().await; let recipe = recipe!( { @@ -884,7 +923,7 @@ async fn test_build_priority_startup_files(tmpdir: tempfile::TempDir) { let spec = recipe .generate_binary_build(&option_map! {}, &Solution::default()) .unwrap(); - BinaryPackageBuilder::from_recipe(recipe) + BinaryPackageBuilder::from_recipe_with_solver(recipe, solver) .with_prefix(tmpdir.path().into()) .generate_startup_scripts(&spec) .unwrap(); @@ -896,8 +935,13 @@ async fn test_build_priority_startup_files(tmpdir: tempfile::TempDir) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_variable_substitution_in_build_env(tmpdir: tempfile::TempDir) { +async fn test_variable_substitution_in_build_env( + tmpdir: tempfile::TempDir, + #[case] solver: SolverImpl, +) { let rt = spfs_runtime().await; let dep_spec = recipe!( { @@ -931,14 +975,14 @@ async fn test_variable_substitution_in_build_env(tmpdir: tempfile::TempDir) { rt.tmprepo.publish_recipe(&dep_spec).await.unwrap(); rt.tmprepo.publish_recipe(&spec).await.unwrap(); - BinaryPackageBuilder::from_recipe(dep_spec) + BinaryPackageBuilder::from_recipe_with_solver(dep_spec, solver.clone()) .with_source(BuildSource::LocalPath(tmpdir.path().to_owned())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) .await .unwrap(); - BinaryPackageBuilder::from_recipe(spec) + BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(tmpdir.path().to_owned())) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) @@ -994,9 +1038,14 @@ async fn test_variable_substitution_in_build_env(tmpdir: tempfile::TempDir) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] #[serial_test::serial(env)] // env manipulation must be reliable -async fn test_dependant_variable_substitution_in_startup_files(tmpdir: tempfile::TempDir) { +async fn test_dependant_variable_substitution_in_startup_files( + tmpdir: tempfile::TempDir, + #[case] solver: SolverImpl, +) { let rt = spfs_runtime().await; // Safety: this is unsafe. serial_test is used to prevent multiple tests @@ -1021,7 +1070,7 @@ async fn test_dependant_variable_substitution_in_startup_files(tmpdir: tempfile: let spec = recipe .generate_binary_build(&option_map! {}, &Solution::default()) .unwrap(); - BinaryPackageBuilder::from_recipe(recipe) + BinaryPackageBuilder::from_recipe_with_solver(recipe, solver) .with_prefix(tmpdir.path().into()) .generate_startup_scripts(&spec) .unwrap(); @@ -1066,8 +1115,10 @@ fn test_path_and_parents() { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_build_options_respect_components() { +async fn test_build_options_respect_components(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; // Create a base package that has a couple components with unique // contents. @@ -1123,7 +1174,7 @@ async fn test_build_options_respect_components() { .build_and_publish(".", &*rt.tmprepo) .await .unwrap(); - let _base_pkg = BinaryPackageBuilder::from_recipe(base_spec) + let _base_pkg = BinaryPackageBuilder::from_recipe_with_solver(base_spec, solver.clone()) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) .await @@ -1134,7 +1185,7 @@ async fn test_build_options_respect_components() { .await .unwrap(); - let r = BinaryPackageBuilder::from_recipe(top_spec) + let r = BinaryPackageBuilder::from_recipe_with_solver(top_spec, solver) .with_repository(rt.tmprepo.clone()) .build_and_publish(option_map! {}, &*rt.tmprepo) .await; diff --git a/crates/spk-cli/cmd-build/src/cmd_build.rs b/crates/spk-cli/cmd-build/src/cmd_build.rs index 1e5afd5d7b..234f7f2ef0 100644 --- a/crates/spk-cli/cmd-build/src/cmd_build.rs +++ b/crates/spk-cli/cmd-build/src/cmd_build.rs @@ -18,7 +18,7 @@ pub struct Build { #[clap(flatten)] runtime: flags::Runtime, #[clap(flatten)] - repos: flags::Repositories, + solver: flags::Solver, #[clap(flatten)] options: flags::Options, @@ -44,9 +44,6 @@ pub struct Build { #[clap(flatten)] variant: flags::Variant, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// Allow dependencies of the package being built to have a dependency on /// this package. #[clap(long)] @@ -107,14 +104,13 @@ impl Run for Build { let mut make_binary = spk_cmd_make_binary::cmd_make_binary::MakeBinary { verbose: self.verbose, runtime: self.runtime.clone(), - repos: self.repos.clone(), options: self.options.clone(), + solver: self.solver.clone(), here: self.here, interactive: self.interactive, env: self.env, packages, variant: self.variant.clone(), - formatter_settings: self.formatter_settings.clone(), allow_circular_dependencies: self.allow_circular_dependencies, created_builds: spk_cli_common::BuildResult::default(), }; diff --git a/crates/spk-cli/cmd-build/src/cmd_build_test/environment.rs b/crates/spk-cli/cmd-build/src/cmd_build_test/environment.rs index 61584d0590..79549d88da 100644 --- a/crates/spk-cli/cmd-build/src/cmd_build_test/environment.rs +++ b/crates/spk-cli/cmd-build/src/cmd_build_test/environment.rs @@ -25,6 +25,7 @@ async fn basic_environment_generation( tmpdir: tempfile::TempDir, #[case] env_spec: &str, #[case] expected: &str, + #[values("cli", "checks", "resolvo")] solver_to_run: &str, ) { let rt = spfs_runtime().await; @@ -43,6 +44,7 @@ install: {env_spec} "# ), + solver_to_run ); let mut result = result.expect("Expected build to succeed"); diff --git a/crates/spk-cli/cmd-build/src/cmd_build_test/mod.rs b/crates/spk-cli/cmd-build/src/cmd_build_test/mod.rs index dab74d0e5f..033e1c25d6 100644 --- a/crates/spk-cli/cmd-build/src/cmd_build_test/mod.rs +++ b/crates/spk-cli/cmd-build/src/cmd_build_test/mod.rs @@ -26,8 +26,14 @@ struct Opt { } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_variant_options_contribute_to_build_hash(tmpdir: tempfile::TempDir) { +async fn test_variant_options_contribute_to_build_hash( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { // A var that appears in the variant list and doesn't appear in the // build.options list should still affect the build hash / produce a // unique build. @@ -48,6 +54,7 @@ build: script: - "true" "#, + solver_to_run ); let ident = version_ident!("three-variants/1.0.0"); @@ -64,8 +71,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_hash_not_affected_by_dependency_version(tmpdir: tempfile::TempDir) { +async fn test_build_hash_not_affected_by_dependency_version( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { // The same recipe should produce the same build hash even if there is a // change in its dependencies (at resolve time). let rt = spfs_runtime().await; @@ -81,7 +94,8 @@ pkg: dependency/1.0.0 build: script: - "true" -"# +"#, + solver_to_run ); // Build a package that depends on "dependency". @@ -98,6 +112,7 @@ build: script: - "true" "#, + solver_to_run ); // Now build a newer version of the dependency. @@ -112,10 +127,11 @@ build: script: - "true" "#, + solver_to_run ); // And build the other package again. - build_package!(tmpdir, package_filename); + build_package!(tmpdir, package_filename, solver_to_run); // The second time building "package" we expect it to build something with // the _same_ build digest (e.g., the change in version of one of its @@ -138,6 +154,7 @@ build: #[rstest] #[case::cli("cli")] #[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] async fn test_build_with_circular_dependency_allow_with_validation( tmpdir: tempfile::TempDir, @@ -159,7 +176,6 @@ build: script: - "true" "#, - "--solver-to-run", solver_to_run ); @@ -184,7 +200,6 @@ install: - pkg: one fromBuildEnv: true "#, - "--solver-to-run", solver_to_run ); @@ -212,7 +227,6 @@ install: - pkg: two fromBuildEnv: true "#, - "--solver-to-run", solver_to_run ); @@ -220,8 +234,14 @@ install: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_package_with_circular_dep_can_modify_files(tmpdir: tempfile::TempDir) { +async fn test_package_with_circular_dep_can_modify_files( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { // A package that depends on itself should be able to modify files // belonging to itself. let _rt = spfs_runtime().await; @@ -236,7 +256,8 @@ build: script: - echo "1.0.0" > $PREFIX/a.txt - echo "1.0.0" > $PREFIX/z.txt -"# +"#, + solver_to_run ); build_package!( @@ -248,7 +269,8 @@ pkg: circ/1.0.0 build: script: - echo "1.0.0" > $PREFIX/version.txt -"# +"#, + solver_to_run ); // Force middle to pick up exactly 1.0.0 so for the multiple builds below @@ -269,6 +291,7 @@ install: requirements: - pkg: circ/=1.0.0 "#, + solver_to_run ); // Attempt to build a newer version of circ, but now it depends on `middle` @@ -293,6 +316,7 @@ build: rules: - allow: RecursiveBuild "#, + solver_to_run ); for other_file in ["a", "z"] { @@ -321,8 +345,9 @@ build: - echo "1.0.1" > $PREFIX/version.txt # try to modify a file belonging to 'other' too - echo "1.0.1" > $PREFIX/{other_file}.txt -"# +"#, ), + solver_to_run ) .1 .expect_err("Expected build to fail"); @@ -330,8 +355,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_package_with_circular_dep_can_build_major_version_change(tmpdir: tempfile::TempDir) { +async fn test_package_with_circular_dep_can_build_major_version_change( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { // A package that depends on itself should be able to build a new major // version of itself, as in something not compatible with the version // being brought in via the circular dependency. @@ -346,7 +377,8 @@ pkg: circ/1.0.0 build: script: - echo "1.0.0" > $PREFIX/version.txt -"# +"#, + solver_to_run ); build_package!( @@ -365,6 +397,7 @@ install: - pkg: circ fromBuildEnv: true "#, + solver_to_run ); // Attempt to build a 2.0.0 version of circ, which shouldn't prevent @@ -384,12 +417,19 @@ build: rules: - allow: RecursiveBuild "#, + solver_to_run ); } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_package_with_circular_dep_collects_all_files(tmpdir: tempfile::TempDir) { +async fn test_package_with_circular_dep_collects_all_files( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { // Building a new version of a package that depends on itself should // produce a package containing all the expected files, even if the new // build creates files with the same content as the previous build. @@ -406,7 +446,8 @@ build: - echo "1.0.0" > $PREFIX/version.txt - echo "hello world" > $PREFIX/hello.txt - echo "unchanged" > $PREFIX/unchanged.txt -"# +"#, + solver_to_run ); build_package!( @@ -425,6 +466,7 @@ install: - pkg: circ fromBuildEnv: true "#, + solver_to_run ); // This build overwrites a file from the previous build, but it has the same @@ -446,6 +488,7 @@ build: rules: - allow: RecursiveBuild "#, + solver_to_run ); let build = rt @@ -494,8 +537,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_package_with_circular_dep_does_not_collect_file_removals(tmpdir: tempfile::TempDir) { +async fn test_package_with_circular_dep_does_not_collect_file_removals( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { // Building a new version of a package that depends on itself should not // collect "negative files" (e.g., files that were removed in the new // build). @@ -512,7 +561,8 @@ pkg: empty/1.0.0 build: script: - mkdir $PREFIX/subdir -"# +"#, + solver_to_run ); build_package!( @@ -526,7 +576,8 @@ build: - echo "1.0.0" > $PREFIX/version.txt - mkdir -p $PREFIX/subdir/v1 - echo "hello world" > $PREFIX/subdir/v1/hello.txt -"# +"#, + solver_to_run ); // This build deletes a subdir that is owned by the previous build. It should @@ -550,6 +601,7 @@ build: rules: - allow: RecursiveBuild "#, + solver_to_run ); let build = rt @@ -592,46 +644,51 @@ build: ); } -#[rstest] -// cases not involving host options -#[should_panic] -#[case::empty_value_fails("varname", "", &["yes", "no"], true)] -#[case::non_empty_value_succeeds("varname/yes", "", &["yes", "no"], true)] -#[should_panic] -#[case::non_empty_value_bad_value_fails("varname/what", "", &["yes", "no"], true)] -// cases involving host options -#[case::empty_value_for_host_option_succeeds("os", "", &["linux", "windows"], true)] -#[case::non_empty_value_for_host_option_succeeds("os", "linux", &["linux", "windows"], true)] -#[should_panic] -#[case::empty_value_for_host_option_fails_if_host_options_disabled("os", "", &["linux", "windows"], false)] -// this case verifies that the --no-host option is respected -#[case::non_empty_value_for_host_option_good_value_succeeds_with_host_options_disabled("os", "beos", &["beos"], false)] -#[should_panic] -#[case::non_empty_value_for_host_option_bad_value_fails_with_host_options_disabled("os", "beos", &["linux", "windows"], false)] -// this case passes because host options override default values, and the -// provided host option value of "linux" is a valid choice. -#[case::non_empty_value_for_host_option_bad_value_succeeds_with_host_options_enabled("os", "beos", &["linux", "windows"], true)] -#[serial_test::serial(host_options)] -#[tokio::test] -async fn test_options_with_choices_and_empty_values( - tmpdir: tempfile::TempDir, - #[case] name: &'static str, - #[case] value: &'static str, - #[case] choices: &'static [&'static str], - #[case] host_options_enabled: bool, -) { - let _rt = spfs_runtime().await; - - // Force "os" host option to "linux" to make this test pass on any OS. - HOST_OPTIONS - .scoped_options(Ok(option_map! { "os" => "linux" }), async move { - let name_maybe_value = if value.is_empty() { - name.to_string() - } else { - format!("{name}/{value}") - }; - let generated_spec = format!( - r#" +#[allow(clippy::too_many_arguments)] +mod workaround_rstest_not_preserving_attrs { + use super::*; + + #[rstest] + // cases not involving host options + #[should_panic] + #[case::empty_value_fails("varname", "", &["yes", "no"], true)] + #[case::non_empty_value_succeeds("varname/yes", "", &["yes", "no"], true)] + #[should_panic] + #[case::non_empty_value_bad_value_fails("varname/what", "", &["yes", "no"], true)] + // cases involving host options + #[case::empty_value_for_host_option_succeeds("os", "", &["linux", "windows"], true)] + #[case::non_empty_value_for_host_option_succeeds("os", "linux", &["linux", "windows"], true)] + #[should_panic] + #[case::empty_value_for_host_option_fails_if_host_options_disabled("os", "", &["linux", "windows"], false)] + // this case verifies that the --no-host option is respected + #[case::non_empty_value_for_host_option_good_value_succeeds_with_host_options_disabled("os", "beos", &["beos"], false)] + #[should_panic] + #[case::non_empty_value_for_host_option_bad_value_fails_with_host_options_disabled("os", "beos", &["linux", "windows"], false)] + // this case passes because host options override default values, and the + // provided host option value of "linux" is a valid choice. + #[case::non_empty_value_for_host_option_bad_value_succeeds_with_host_options_enabled("os", "beos", &["linux", "windows"], true)] + #[tokio::test] + #[serial_test::serial(host_options)] + async fn test_options_with_choices_and_empty_values( + tmpdir: tempfile::TempDir, + #[case] name: &'static str, + #[case] value: &'static str, + #[case] choices: &'static [&'static str], + #[case] host_options_enabled: bool, + #[values("cli", "checks", "resolvo")] solver_to_run: &'static str, + ) { + let _rt = spfs_runtime().await; + + // Force "os" host option to "linux" to make this test pass on any OS. + HOST_OPTIONS + .scoped_options(Ok(option_map! { "os" => "linux" }), async move { + let name_maybe_value = if value.is_empty() { + name.to_string() + } else { + format!("{name}/{value}") + }; + let generated_spec = format!( + r#" pkg: dummy/1.0.0 api: v0/package build: @@ -641,25 +698,35 @@ build: script: - "true" "#, - choices = choices.join(", ") - ); - - if !host_options_enabled { - build_package!(tmpdir, "dummy.spk.yaml", generated_spec, "--no-host"); - } else { - build_package!(tmpdir, "dummy.spk.yaml", generated_spec); - } - - Ok::<_, ()>(()) - }) - .await - .unwrap(); + choices = choices.join(", ") + ); + + if !host_options_enabled { + build_package!( + tmpdir, + "dummy.spk.yaml", + generated_spec, + solver_to_run, + "--no-host" + ); + } else { + build_package!(tmpdir, "dummy.spk.yaml", generated_spec, solver_to_run); + } + + Ok::<_, ()>(()) + }) + .await + .unwrap(); + } } /// A package may contain files/directories with a leading dot #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_dot_files_are_collected(tmpdir: tempfile::TempDir) { +async fn test_dot_files_are_collected(tmpdir: tempfile::TempDir, #[case] solver_to_run: &str) { let rt = spfs_runtime().await; build_package!( @@ -679,6 +746,7 @@ build: - touch /spfs/.dot2/.dot - ln -s .dot2 /spfs/.dot3 "#, + solver_to_run ); let build = rt @@ -732,8 +800,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_package_with_environment_ops_preserves_ops_in_recipe(tmpdir: tempfile::TempDir) { +async fn test_package_with_environment_ops_preserves_ops_in_recipe( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let rt = spfs_runtime().await; build_package!( @@ -749,7 +823,8 @@ install: environment: - set: FOO value: bar -"# +"#, + solver_to_run ); let recipe = rt diff --git a/crates/spk-cli/cmd-build/src/cmd_build_test/variant_filter.rs b/crates/spk-cli/cmd-build/src/cmd_build_test/variant_filter.rs index d4d44a8984..4793e587ad 100644 --- a/crates/spk-cli/cmd-build/src/cmd_build_test/variant_filter.rs +++ b/crates/spk-cli/cmd-build/src/cmd_build_test/variant_filter.rs @@ -22,8 +22,14 @@ struct Opt { } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_variant_acts_as_variant_filter(tmpdir: tempfile::TempDir) { +async fn test_build_with_variant_acts_as_variant_filter( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -42,6 +48,7 @@ build: - { color: green } - { color: blue } "#, + solver_to_run, // By saying --variant color=green, we are asking for the second variant "--variant", "color=green", @@ -73,8 +80,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_opts_acts_as_override(tmpdir: tempfile::TempDir) { +async fn test_build_with_opts_acts_as_override( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -93,6 +106,7 @@ build: - { color: green } - { color: blue } "#, + solver_to_run, // By saying --opt color=green, we are asking to override the color in // all the variants (pruning duplicates). "--opt", @@ -125,8 +139,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_variant_acts_as_variant_filter_no_match(tmpdir: tempfile::TempDir) { +async fn test_build_with_variant_acts_as_variant_filter_no_match( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -145,6 +165,7 @@ build: - { color: green } - { color: blue } "#, + solver_to_run, // By saying --variant color=purple, we are asking for a variant that // doesn't exist. "--variant", @@ -155,9 +176,13 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] async fn test_build_with_variant_on_recipe_with_no_variants_match_default( tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, ) { let _rt = spfs_runtime().await; @@ -173,6 +198,7 @@ build: script: - 'echo "color: $SPK_OPT_color" > "$PREFIX/color.txt"' "#, + solver_to_run, // By saying --variant color=blue, we are asking for the "default" // variant, because the default color is blue. "--variant", @@ -205,8 +231,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_variant_on_recipe_with_no_variants_no_match(tmpdir: tempfile::TempDir) { +async fn test_build_with_variant_on_recipe_with_no_variants_no_match( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -221,6 +253,7 @@ build: script: - 'echo "color: $SPK_OPT_color" > "$PREFIX/color.txt"' "#, + solver_to_run, // By saying --variant color=green, we are asking for a variant that // doesn't exist. "--variant", @@ -231,8 +264,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_new_variant_on_recipe_with_no_variants(tmpdir: tempfile::TempDir) { +async fn test_build_with_new_variant_on_recipe_with_no_variants( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -247,6 +286,7 @@ build: script: - 'echo "color: $SPK_OPT_color" > "$PREFIX/color.txt"' "#, + solver_to_run, // By saying --new-variant with color=green, we are asking for a bespoke // variant "--new-variant", @@ -279,8 +319,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_variant_acts_as_variant_filter_two_opts(tmpdir: tempfile::TempDir) { +async fn test_build_with_variant_acts_as_variant_filter_two_opts( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -300,6 +346,7 @@ build: - { color: green, fruit: apple } - { color: blue, fruit: orange } "#, + solver_to_run, // By saying --variant color=green,fruit=apple we are asking for the // second variant "--variant", @@ -336,9 +383,13 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] async fn test_build_with_variant_acts_as_variant_filter_two_opts_no_match( tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, ) { let _rt = spfs_runtime().await; @@ -359,6 +410,7 @@ build: - { color: green, fruit: apple } - { color: blue, fruit: orange } "#, + solver_to_run, // The first option matches, but the second doesn't "--variant", "color=green,fruit=orange", @@ -368,9 +420,13 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] async fn test_build_with_variant_and_opts_acts_as_variant_filter_and_override( tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, ) { let _rt = spfs_runtime().await; @@ -391,6 +447,7 @@ build: - { color: green } - { color: blue } "#, + solver_to_run, // By saying --variant color=green, we are asking for the second variant "--variant", "color=green", @@ -429,8 +486,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_opts_acts_as_an_override(tmpdir: tempfile::TempDir) { +async fn test_build_with_opts_acts_as_an_override( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -450,6 +513,7 @@ build: - { color: green } - { color: blue } "#, + solver_to_run, // Setting an option that doesn't appear in the variants will // not filter out any variants, but will override the default "--opt", @@ -484,8 +548,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_opts_and_variant_index(tmpdir: tempfile::TempDir) { +async fn test_build_with_opts_and_variant_index( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -505,6 +575,7 @@ build: - { color: green, fruit: apple } - { color: blue, fruit: orange } "#, + solver_to_run, // By saying --variant 0, we are explicitly asking for the first variant "--variant", "0", @@ -544,8 +615,11 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_variant_spec(tmpdir: tempfile::TempDir) { +async fn test_build_with_variant_spec(tmpdir: tempfile::TempDir, #[case] solver_to_run: &str) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -565,6 +639,7 @@ build: - { color: green, fruit: apple } - { color: blue, fruit: orange } "#, + solver_to_run, // By supplying a variant spec, we are asking for a bespoke variant "--new-variant", r#"{ "color": "brown", "fruit": "kiwi" }"#, @@ -601,8 +676,14 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_build_with_variant_spec_and_override(tmpdir: tempfile::TempDir) { +async fn test_build_with_variant_spec_and_override( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let (_, result) = try_build_package!( @@ -622,6 +703,7 @@ build: - { color: green, fruit: apple } - { color: blue, fruit: orange } "#, + solver_to_run, // By supplying a variant spec, we are asking for a bespoke variant "--new-variant", r#"{ "color": "brown", "fruit": "kiwi" }"#, @@ -661,9 +743,15 @@ build: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[serial_test::serial(host_options)] #[tokio::test] -async fn test_build_filters_variants_based_on_host_opts(tmpdir: tempfile::TempDir) { +async fn test_build_filters_variants_based_on_host_opts( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &'static str, +) { let _rt = spfs_runtime().await; // Force "distro" host option to "centos" to make this test pass on any OS. @@ -686,6 +774,7 @@ async fn test_build_filters_variants_based_on_host_opts(tmpdir: tempfile::TempDi - { distro: rocky, color: green } - { distro: centos, color: blue } "#, + solver_to_run ); let mut result = result.expect("Expected build to succeed"); diff --git a/crates/spk-cli/cmd-build/src/macros.rs b/crates/spk-cli/cmd-build/src/macros.rs index e1758a56af..ec9eb22cba 100644 --- a/crates/spk-cli/cmd-build/src/macros.rs +++ b/crates/spk-cli/cmd-build/src/macros.rs @@ -14,20 +14,20 @@ pub struct BuildOpt { #[macro_export] macro_rules! build_package { - ($tmpdir:ident, $filename:literal, $recipe:literal $(,$extra_build_args:expr)* $(,)?) => {{ - let (filename, r) = $crate::try_build_package!($tmpdir, $filename, $recipe, $($extra_build_args),*); + ($tmpdir:ident, $filename:literal, $recipe:literal, $solver_to_run:expr $(,$extra_build_args:expr)* $(,)?) => {{ + let (filename, r) = $crate::try_build_package!($tmpdir, $filename, $recipe, $solver_to_run, $($extra_build_args),*); r.unwrap(); filename }}; - ($tmpdir:ident, $filename:literal, $recipe:ident $(,$extra_build_args:expr)* $(,)?) => {{ - let (filename, r) = $crate::try_build_package!($tmpdir, $filename, $recipe, $($extra_build_args),*); + ($tmpdir:ident, $filename:literal, $recipe:ident, $solver_to_run:expr $(,$extra_build_args:expr)* $(,)?) => {{ + let (filename, r) = $crate::try_build_package!($tmpdir, $filename, $recipe, $solver_to_run, $($extra_build_args),*); r.unwrap(); filename }}; - ($tmpdir:ident, $filename:ident $(,$extra_build_args:expr)* $(,)?) => {{ - let (filename, r) = $crate::try_build_package!($tmpdir, $filename, $($extra_build_args),*); + ($tmpdir:ident, $filename:ident, $solver_to_run:expr $(,$extra_build_args:expr)* $(,)?) => {{ + let (filename, r) = $crate::try_build_package!($tmpdir, $filename, $solver_to_run, $($extra_build_args),*); r.unwrap(); filename }}; @@ -35,7 +35,7 @@ macro_rules! build_package { #[macro_export] macro_rules! try_build_package { - ($tmpdir:ident, $filename:literal, $recipe:literal $(,$extra_build_args:expr)* $(,)?) => {{ + ($tmpdir:ident, $filename:literal, $recipe:literal, $solver_to_run:expr $(,$extra_build_args:expr)* $(,)?) => {{ // Leak `filename` for convenience. let filename = Box::leak(Box::new($tmpdir.path().join($filename))); { @@ -46,10 +46,10 @@ macro_rules! try_build_package { let filename_str = filename.as_os_str().to_str().unwrap(); - $crate::try_build_package!($tmpdir, filename_str, $($extra_build_args),*) + $crate::try_build_package!($tmpdir, filename_str, $solver_to_run, $($extra_build_args),*) }}; - ($tmpdir:ident, $filename:literal, $recipe:expr $(,$extra_build_args:expr)* $(,)?) => {{ + ($tmpdir:ident, $filename:literal, $recipe:expr, $solver_to_run:expr $(,$extra_build_args:expr)* $(,)?) => {{ // Leak `filename` for convenience. let filename = Box::leak(Box::new($tmpdir.path().join($filename))); { @@ -60,10 +60,10 @@ macro_rules! try_build_package { let filename_str = filename.as_os_str().to_str().unwrap(); - $crate::try_build_package!($tmpdir, filename_str, $($extra_build_args),*) + $crate::try_build_package!($tmpdir, filename_str, $solver_to_run, $($extra_build_args),*) }}; - ($tmpdir:ident, $filename:ident $(,$extra_build_args:expr)* $(,)?) => {{ + ($tmpdir:ident, $filename:ident, $solver_to_run:expr $(,$extra_build_args:expr)* $(,)?) => {{ // Build the package so it can be tested. use clap::Parser; let mut opt = $crate::macros::BuildOpt::try_parse_from([ @@ -72,6 +72,8 @@ macro_rules! try_build_package { // coverage testing. "--no-runtime", "--disable-repo=origin", + "--solver-to-run", + $solver_to_run, $($extra_build_args,)* $filename, ]) diff --git a/crates/spk-cli/cmd-convert/src/cmd_convert.rs b/crates/spk-cli/cmd-convert/src/cmd_convert.rs index 06959873f6..73650f8e62 100644 --- a/crates/spk-cli/cmd-convert/src/cmd_convert.rs +++ b/crates/spk-cli/cmd-convert/src/cmd_convert.rs @@ -22,9 +22,6 @@ pub struct Convert { #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// Options for showing progress #[clap(long, value_enum)] pub progress: Option, @@ -61,7 +58,6 @@ impl Run for Convert { runtime: self.runtime.clone(), requests: self.requests.clone(), verbose: self.verbose, - formatter_settings: self.formatter_settings.clone(), progress: self.progress, requested: vec![converter_package], command, diff --git a/crates/spk-cli/cmd-du/Cargo.toml b/crates/spk-cli/cmd-du/Cargo.toml index 0e417337bc..9e5d4ae2a5 100644 --- a/crates/spk-cli/cmd-du/Cargo.toml +++ b/crates/spk-cli/cmd-du/Cargo.toml @@ -20,6 +20,7 @@ clap = { workspace = true } colored = { workspace = true } futures = "0.3.9" itertools = { workspace = true } +rstest = { workspace = true } spfs = { workspace = true } spk-build = { workspace = true } spk-cli-common = { workspace = true } diff --git a/crates/spk-cli/cmd-du/src/cmd_du_test.rs b/crates/spk-cli/cmd-du/src/cmd_du_test.rs index e9d723aa6d..6500ed9eeb 100644 --- a/crates/spk-cli/cmd-du/src/cmd_du_test.rs +++ b/crates/spk-cli/cmd-du/src/cmd_du_test.rs @@ -8,6 +8,7 @@ use std::sync::Mutex; use clap::Parser; use colored::Colorize; use itertools::Itertools; +use rstest::rstest; use spfs::RemoteAddress; use spfs::config::Remote; use spfs::encoding::EMPTY_DIGEST; @@ -15,7 +16,7 @@ use spk_build::{BinaryPackageBuilder, BuildSource}; use spk_schema::foundation::ident_component::Component; use spk_schema::foundation::option_map; use spk_schema::recipe; -use spk_solve::spec; +use spk_solve::{SolverImpl, spec}; use spk_storage::fixtures::*; use super::{Du, Output, Run}; @@ -42,8 +43,19 @@ struct Opt { du: Du, } +fn step_solver() -> SolverImpl { + SolverImpl::Step(spk_solve::StepSolver::default()) +} + +fn resolvo_solver() -> SolverImpl { + SolverImpl::Resolvo(spk_solve::ResolvoSolver::default()) +} + +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_du_trivially_works() { +async fn test_du_trivially_works(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -65,7 +77,7 @@ async fn test_du_trivially_works() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -132,8 +144,11 @@ async fn test_du_warnings_when_object_is_tree_or_blob() { assert_eq!(opt.du.output.warnings.lock().unwrap().len(), 2); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_du_non_existing_version() { +async fn test_du_non_existing_version(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -150,7 +165,7 @@ async fn test_du_non_existing_version() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -162,8 +177,11 @@ async fn test_du_non_existing_version() { assert_eq!(opt.du.output.vec.lock().unwrap().len(), 0); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_du_out_of_range_input() { +async fn test_du_out_of_range_input(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -180,7 +198,7 @@ async fn test_du_out_of_range_input() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -196,8 +214,11 @@ async fn test_du_out_of_range_input() { assert_eq!(opt.du.output.vec.lock().unwrap().len(), 0); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_du_is_not_counting_links() { +async fn test_du_is_not_counting_links(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -219,7 +240,7 @@ async fn test_du_is_not_counting_links() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -239,8 +260,11 @@ async fn test_du_is_not_counting_links() { assert_ne!(build_component_output[0].parse::().unwrap(), 0); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_du_is_counting_links() { +async fn test_du_is_counting_links(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -262,7 +286,7 @@ async fn test_du_is_counting_links() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -284,8 +308,11 @@ async fn test_du_is_counting_links() { ); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_du_total_size() { +async fn test_du_total_size(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -302,7 +329,7 @@ async fn test_du_total_size() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -330,8 +357,11 @@ async fn test_du_total_size() { assert_eq!(total, calculated_total_size_from_output); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_du_summarize_output_enabled() { +async fn test_du_summarize_output_enabled(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -353,7 +383,7 @@ async fn test_du_summarize_output_enabled() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -370,8 +400,11 @@ async fn test_du_summarize_output_enabled() { assert_eq!(generated_output, expected_output); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_du_summarize_output_is_not_enabled() { +async fn test_du_summarize_output_is_not_enabled(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -393,7 +426,7 @@ async fn test_du_summarize_output_is_not_enabled() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -425,8 +458,11 @@ async fn test_du_summarize_output_is_not_enabled() { assert_eq!(expected_output.len(), 0); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_deprecate_flag() { +async fn test_deprecate_flag(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -448,7 +484,7 @@ async fn test_deprecate_flag() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) @@ -476,8 +512,11 @@ async fn test_deprecate_flag() { assert_eq!(expected_output, generated_output); } +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_human_readable_flag() { +async fn test_human_readable_flag(#[case] solver: SolverImpl) { let mut rt = spfs_runtime().await; let remote_repo = spfsrepo().await; rt.add_remote_repo( @@ -499,7 +538,7 @@ async fn test_human_readable_flag() { rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .with_repository(rt.tmprepo.clone()) .build_and_publish(&option_map! {}, &*rt.tmprepo) diff --git a/crates/spk-cli/cmd-env/src/cmd_env.rs b/crates/spk-cli/cmd-env/src/cmd_env.rs index caaac453f1..83b0f25750 100644 --- a/crates/spk-cli/cmd-env/src/cmd_env.rs +++ b/crates/spk-cli/cmd-env/src/cmd_env.rs @@ -13,6 +13,7 @@ use spk_cli_common::{CommandArgs, Run, build_required_packages, flags}; use spk_exec::setup_runtime_with_reporter; #[cfg(feature = "statsd")] use spk_solve::{SPK_RUN_TIME_METRIC, get_metrics_client}; +use spk_solve::{Solver, SolverMut}; /// Resolve and run an environment on-the-fly /// @@ -33,9 +34,6 @@ pub struct Env { #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// The requests to resolve and run #[clap(name = "REQUESTS")] pub requested: Vec, @@ -86,10 +84,13 @@ impl Run for Env { solver.add_request(request) } - let formatter = self.formatter_settings.get_formatter(self.verbose)?; - let (solution, _) = formatter.run_and_print_resolve(&solver).await?; + let formatter = self + .solver + .decision_formatter_settings + .get_formatter(self.verbose)?; + let solution = solver.run_and_print_resolve(&formatter).await?; - let solution = build_required_packages(&solution).await?; + let solution = build_required_packages(&solution, solver).await?; rt.status.editable = self.runtime.editable() || self.requests.any_build_stage_requests(&self.requested)?; diff --git a/crates/spk-cli/cmd-explain/Cargo.toml b/crates/spk-cli/cmd-explain/Cargo.toml index 7522bacb99..82a0e8bcdb 100644 --- a/crates/spk-cli/cmd-explain/Cargo.toml +++ b/crates/spk-cli/cmd-explain/Cargo.toml @@ -17,6 +17,7 @@ miette = { workspace = true, features = ["fancy"] } async-trait = { workspace = true } clap = { workspace = true } spk-cli-common = { workspace = true } +spk-solve = { workspace = true } # The dependency on spfs can be removed after the deprecated runtime flags are # removed. spfs = { workspace = true } diff --git a/crates/spk-cli/cmd-explain/src/cmd_explain.rs b/crates/spk-cli/cmd-explain/src/cmd_explain.rs index a02da56a8b..84de847414 100644 --- a/crates/spk-cli/cmd-explain/src/cmd_explain.rs +++ b/crates/spk-cli/cmd-explain/src/cmd_explain.rs @@ -5,6 +5,7 @@ use clap::Args; use miette::Result; use spk_cli_common::{CommandArgs, Run, flags}; +use spk_solve::{Solver, SolverMut}; /// Show the resolve process for a set of packages. #[derive(Args)] @@ -19,9 +20,6 @@ pub struct Explain { #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// The requests to resolve #[clap(name = "REQUESTS", required = true)] pub requested: Vec, @@ -81,11 +79,12 @@ impl Run for Explain { // Always show the solution packages for the solve let formatter = self - .formatter_settings + .solver + .decision_formatter_settings .get_formatter_builder(self.verbose + 1)? .with_solution(true) .build(); - formatter.run_and_print_resolve(&solver).await?; + solver.run_and_print_resolve(&formatter).await?; Ok(0) } diff --git a/crates/spk-cli/cmd-install/Cargo.toml b/crates/spk-cli/cmd-install/Cargo.toml index 5d63928b26..fe0e204f31 100644 --- a/crates/spk-cli/cmd-install/Cargo.toml +++ b/crates/spk-cli/cmd-install/Cargo.toml @@ -21,4 +21,5 @@ futures = { workspace = true } spk-cli-common = { workspace = true } spk-exec = { workspace = true } spk-schema = { workspace = true } +spk-solve = { workspace = true } tokio = { workspace = true, features = ["rt"] } diff --git a/crates/spk-cli/cmd-install/src/cmd_install.rs b/crates/spk-cli/cmd-install/src/cmd_install.rs index 9dbb50d9ed..b3a53ad49a 100644 --- a/crates/spk-cli/cmd-install/src/cmd_install.rs +++ b/crates/spk-cli/cmd-install/src/cmd_install.rs @@ -14,6 +14,7 @@ use spk_exec::setup_current_runtime; use spk_schema::Package; use spk_schema::foundation::format::FormatIdent; use spk_schema::foundation::spec_ops::Named; +use spk_solve::{Solver, SolverMut}; /// Install a package into the current environment #[derive(Args)] @@ -32,9 +33,6 @@ pub struct Install { #[clap(long, short)] yes: bool, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// The packages to install #[clap(name = "PKG", required = true)] pub packages: Vec, @@ -62,12 +60,14 @@ impl Run for Install { solver.add_request(request); } - let formatter = self.formatter_settings.get_formatter(self.verbose)?; - let (solution, _) = formatter.run_and_print_resolve(&solver).await?; + let formatter = self + .solver + .decision_formatter_settings + .get_formatter(self.verbose)?; + let solution = solver.run_and_print_resolve(&formatter).await?; println!("The following packages will be installed:\n"); let requested: HashSet<_> = solver - .get_initial_state() .get_pkg_requests() .iter() .map(|r| r.pkg.name.clone()) @@ -119,7 +119,7 @@ impl Run for Install { } } - let compiled_solution = build_required_packages(&solution) + let compiled_solution = build_required_packages(&solution, solver) .await .wrap_err("Failed to build one or more packages from source")?; setup_current_runtime(&compiled_solution).await?; diff --git a/crates/spk-cli/cmd-make-binary/Cargo.toml b/crates/spk-cli/cmd-make-binary/Cargo.toml index 5e823eadeb..0fbf8dc8c0 100644 --- a/crates/spk-cli/cmd-make-binary/Cargo.toml +++ b/crates/spk-cli/cmd-make-binary/Cargo.toml @@ -14,10 +14,10 @@ workspace = true [features] migration-to-components = [ - "spk-build/migration-to-components", - "spk-cli-common/migration-to-components", - "spk-schema/migration-to-components", - "spk-storage/migration-to-components", + "spk-build/migration-to-components", + "spk-cli-common/migration-to-components", + "spk-schema/migration-to-components", + "spk-storage/migration-to-components", ] [dependencies] @@ -30,6 +30,7 @@ spfs = { workspace = true } spk-build = { workspace = true } spk-cli-common = { workspace = true } spk-schema = { workspace = true } +spk-solve = { workspace = true } spk-storage = { workspace = true } tokio = { workspace = true, features = ["rt"] } tracing = { workspace = true } diff --git a/crates/spk-cli/cmd-make-binary/src/cmd_make_binary.rs b/crates/spk-cli/cmd-make-binary/src/cmd_make_binary.rs index 4cf6104f0b..78bdadf124 100644 --- a/crates/spk-cli/cmd-make-binary/src/cmd_make_binary.rs +++ b/crates/spk-cli/cmd-make-binary/src/cmd_make_binary.rs @@ -26,7 +26,7 @@ mod cmd_make_binary_test; #[clap(visible_aliases = &["mkbinary", "mkbin", "mkb"])] pub struct MakeBinary { #[clap(flatten)] - pub repos: flags::Repositories, + pub solver: flags::Solver, #[clap(flatten)] pub options: flags::Options, #[clap(flatten)] @@ -54,9 +54,6 @@ pub struct MakeBinary { #[clap(flatten)] pub variant: flags::Variant, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// Allow dependencies of the package being built to have a dependency on /// this package. #[clap(long)] @@ -93,7 +90,7 @@ impl Run for MakeBinary { let (_runtime, local, repos) = tokio::try_join!( self.runtime.ensure_active_runtime(&["make-binary", "mkbinary", "mkbin", "mkb"]), storage::local_repository().map_ok(storage::RepositoryHandle::from).map_err(miette::Error::from), - async { self.repos.get_repos_for_non_destructive_operation().await } + async { self.solver.repos.get_repos_for_non_destructive_operation().await } )?; let repos = repos .into_iter() @@ -164,7 +161,8 @@ impl Run for MakeBinary { // Always show the solution packages for the solves let mut fmt_builder = self - .formatter_settings + .solver + .decision_formatter_settings .get_formatter_builder(self.verbose)?; let src_formatter = fmt_builder .with_solution(true) @@ -175,12 +173,14 @@ impl Run for MakeBinary { .with_header("Build Resolver ") .build(); - let mut builder = BinaryPackageBuilder::from_recipe((*recipe).clone()); + let solver = self.solver.get_solver(&self.options).await?; + let mut builder = + BinaryPackageBuilder::from_recipe_with_solver((*recipe).clone(), solver); builder .with_repositories(repos.iter().cloned()) .set_interactive(self.interactive) - .with_source_resolver(&src_formatter) - .with_build_resolver(&build_formatter) + .with_source_formatter(src_formatter) + .with_build_formatter(build_formatter) .with_allow_circular_dependencies(self.allow_circular_dependencies); if self.here { diff --git a/crates/spk-cli/cmd-render/Cargo.toml b/crates/spk-cli/cmd-render/Cargo.toml index 30f14179bd..a709f34853 100644 --- a/crates/spk-cli/cmd-render/Cargo.toml +++ b/crates/spk-cli/cmd-render/Cargo.toml @@ -20,6 +20,7 @@ dunce = { workspace = true } spfs = { workspace = true } spk-cli-common = { workspace = true } spk-exec = { workspace = true } +spk-solve = { workspace = true } spk-storage = { workspace = true } tokio = { workspace = true, features = ["rt"] } tracing = { workspace = true } diff --git a/crates/spk-cli/cmd-render/src/cmd_render.rs b/crates/spk-cli/cmd-render/src/cmd_render.rs index e35a09482c..850e03ea91 100644 --- a/crates/spk-cli/cmd-render/src/cmd_render.rs +++ b/crates/spk-cli/cmd-render/src/cmd_render.rs @@ -9,6 +9,7 @@ use miette::{Context, IntoDiagnostic, Result, bail}; use spfs::storage::fallback::FallbackProxy; use spk_cli_common::{CommandArgs, Run, build_required_packages, flags}; use spk_exec::resolve_runtime_layers; +use spk_solve::{Solver, SolverMut}; /// Output the contents of an spk environment (/spfs) to a folder #[derive(Args)] @@ -23,9 +24,6 @@ pub struct Render { #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// The packages to resolve and render #[clap(name = "PKG", required = true)] packages: Vec, @@ -51,10 +49,13 @@ impl Run for Render { solver.add_request(name); } - let formatter = self.formatter_settings.get_formatter(self.verbose)?; - let (solution, _) = formatter.run_and_print_resolve(&solver).await?; + let formatter = self + .solver + .decision_formatter_settings + .get_formatter(self.verbose)?; + let solution = solver.run_and_print_resolve(&formatter).await?; - let solution = build_required_packages(&solution).await?; + let solution = build_required_packages(&solution, solver.clone()).await?; let stack = resolve_runtime_layers(true, &solution).await?; std::fs::create_dir_all(&self.target) .into_diagnostic() diff --git a/crates/spk-cli/cmd-test/src/cmd_test.rs b/crates/spk-cli/cmd-test/src/cmd_test.rs index fc402b693d..af0e62f5fc 100644 --- a/crates/spk-cli/cmd-test/src/cmd_test.rs +++ b/crates/spk-cli/cmd-test/src/cmd_test.rs @@ -32,16 +32,13 @@ pub struct CmdTest { #[clap(flatten)] pub runtime: flags::Runtime, #[clap(flatten)] - pub repos: flags::Repositories, + pub solver: flags::Solver, #[clap(flatten)] pub workspace: flags::Workspace, #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// Test in the current directory, instead of the source package /// /// This is mostly relevant when testing source and build stages @@ -69,7 +66,7 @@ impl Run for CmdTest { let options = self.options.get_options()?; let (_runtime, repos) = tokio::try_join!( self.runtime.ensure_active_runtime(&["test"]), - self.repos.get_repos_for_non_destructive_operation() + self.solver.repos.get_repos_for_non_destructive_operation() )?; let repos = repos .into_iter() @@ -158,7 +155,8 @@ impl Run for CmdTest { ); for (index, test) in selected.into_iter().enumerate() { let mut builder = self - .formatter_settings + .solver + .decision_formatter_settings .get_formatter_builder(self.verbose)?; let src_formatter = builder.with_header("Source Resolver ").build(); let build_src_formatter = @@ -169,22 +167,32 @@ impl Run for CmdTest { let mut tester: Box = match stage { TestStage::Sources => { - let mut tester = - PackageSourceTester::new((*recipe).clone(), test.script()); + let solver = self.solver.get_solver(&self.options).await?; + + let mut tester = PackageSourceTester::new( + (*recipe).clone(), + test.script(), + solver, + ); tester .with_options(variant.options().into_owned()) .with_repositories(repos.iter().cloned()) .with_requirements(test.additional_requirements()) .with_source(source.clone()) - .watch_environment_resolve(&src_formatter); + .watch_environment_formatter(src_formatter); Box::new(tester) } TestStage::Build => { - let mut tester = - PackageBuildTester::new((*recipe).clone(), test.script()); + let solver = self.solver.get_solver(&self.options).await?; + + let mut tester = PackageBuildTester::new( + (*recipe).clone(), + test.script(), + solver, + ); tester .with_options(variant.options().into_owned()) @@ -208,17 +216,20 @@ impl Run for CmdTest { }, ), ) - .with_source_resolver(&build_src_formatter) - .with_build_resolver(&build_formatter); + .with_source_formatter(build_src_formatter) + .with_build_formatter(build_formatter); Box::new(tester) } TestStage::Install => { + let solver = self.solver.get_solver(&self.options).await?; + let mut tester = PackageInstallTester::new( (*recipe).clone(), test.script(), &variant, + solver, ); tester @@ -227,7 +238,7 @@ impl Run for CmdTest { .with_requirements(test.additional_requirements()) .with_requirements(options_reqs.clone()) .with_source(source.clone()) - .watch_environment_resolve(&install_formatter); + .watch_environment_formatter(install_formatter); Box::new(tester) } diff --git a/crates/spk-cli/cmd-test/src/cmd_test_test.rs b/crates/spk-cli/cmd-test/src/cmd_test_test.rs index 2c7c63e734..e1ecd169d7 100644 --- a/crates/spk-cli/cmd-test/src/cmd_test_test.rs +++ b/crates/spk-cli/cmd-test/src/cmd_test_test.rs @@ -18,8 +18,11 @@ struct TestOpt { } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_all_test_stages_succeed(tmpdir: tempfile::TempDir) { +async fn test_all_test_stages_succeed(tmpdir: tempfile::TempDir, #[case] solver_to_run: &str) { // A var that appears in the variant list and doesn't appear in the // build.options list should still affect the build hash / produce a // unique build. @@ -44,7 +47,8 @@ tests: - stage: install script: - "true" -"# +"#, + solver_to_run ); let mut opt = TestOpt::try_parse_from([ @@ -60,8 +64,14 @@ tests: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_install_test_picks_same_digest_as_build(tmpdir: tempfile::TempDir) { +async fn test_install_test_picks_same_digest_as_build( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; build_package!( @@ -72,7 +82,8 @@ pkg: a-pkg-with-no-version-specified/1.0.0 build: script: - "true" -"# +"#, + solver_to_run ); let filename_str = build_package!( @@ -90,7 +101,8 @@ tests: - stage: install script: - "true" -"# +"#, + solver_to_run ); let mut opt = TestOpt::try_parse_from([ @@ -112,9 +124,13 @@ tests: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] async fn test_install_test_picks_same_digest_as_build_with_new_dep_in_variant( tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, ) { let _rt = spfs_runtime().await; @@ -126,7 +142,8 @@ pkg: dep-a/1.2.3 build: script: - "true" -"# +"#, + solver_to_run ); build_package!( @@ -137,7 +154,8 @@ pkg: dep-b/1.2.3 build: script: - "true" -"# +"#, + solver_to_run ); // Note that "dep-b" is introduced as a new dependency in the variant. @@ -158,7 +176,8 @@ tests: - stage: install script: - "true" -"# +"#, + solver_to_run ); let mut opt = TestOpt::try_parse_from([ @@ -180,9 +199,13 @@ tests: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] async fn test_install_test_picks_same_digest_as_build_with_new_dep_in_variant_plus_command_line_overrides( tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, ) { let _rt = spfs_runtime().await; @@ -194,7 +217,8 @@ pkg: dep-a/1.2.5 build: script: - "true" -"# +"#, + solver_to_run ); build_package!( @@ -205,7 +229,8 @@ pkg: dep-b/1.2.3 build: script: - "true" -"# +"#, + solver_to_run ); let filename_str = build_package!( @@ -226,6 +251,7 @@ tests: script: - "true" "#, + solver_to_run, // Extra build options specified here. "--opt", "dep-a=1.2.4" @@ -253,9 +279,13 @@ tests: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] async fn test_install_test_picks_same_digest_as_build_with_circular_dependencies( tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, ) { let _rt = spfs_runtime().await; @@ -268,7 +298,8 @@ pkg: some-other/1.2.0 build: script: - "true" -"# +"#, + solver_to_run ); build_package!( @@ -285,7 +316,8 @@ install: requirements: - pkg: some-other fromBuildEnv: true -"# +"#, + solver_to_run ); build_package!( @@ -305,7 +337,8 @@ install: fromBuildEnv: true - pkg: some-other fromBuildEnv: true -"# +"#, + solver_to_run ); let filename_str = build_package!( @@ -333,6 +366,7 @@ tests: script: - "true" "#, + solver_to_run ); let mut opt = TestOpt::try_parse_from([ @@ -354,8 +388,14 @@ tests: } #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] -async fn test_selectors_with_component_names_match_correctly(tmpdir: tempfile::TempDir) { +async fn test_selectors_with_component_names_match_correctly( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { let _rt = spfs_runtime().await; let _ = build_package!( @@ -376,7 +416,8 @@ install: - name: comp2 files: - comp2 -"# +"#, + solver_to_run ); // This package is expected to pass both tests. @@ -404,7 +445,8 @@ tests: - { "base:comp2": "1.0.0" } script: - test -f "$PREFIX"/comp2 -"# +"#, + solver_to_run ); let mut opt = TestOpt::try_parse_from([ @@ -442,7 +484,8 @@ tests: # If the whole test run fails we know that this selector matched as # expected. - test -f "$PREFIX"/comp2 -"# +"#, + solver_to_run ); let mut opt = TestOpt::try_parse_from([ @@ -480,7 +523,8 @@ tests: # If the whole test run fails we know that this selector matched as # expected. - test -f "$PREFIX"/comp1 -"# +"#, + solver_to_run ); let mut opt = TestOpt::try_parse_from([ @@ -497,3 +541,58 @@ tests: .await .expect_err("the test run should fail, otherwise the selectors aren't working properly"); } + +#[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] +#[tokio::test] +async fn build_stage_test_env_includes_build_deps( + tmpdir: tempfile::TempDir, + #[case] solver_to_run: &str, +) { + let _rt = spfs_runtime().await; + + let _ = build_package!( + tmpdir, + "base.spk.yaml", + br#" +pkg: base/1.0.0 +build: + script: + - touch "$PREFIX"/base +"#, + solver_to_run + ); + + let filename_str = build_package!( + tmpdir, + "simple1.spk.yaml", + br#" +pkg: simple1/1.0.0 +build: + options: + - pkg: base + script: + - "true" + +tests: + - stage: build + script: + # simple's build options should exist in a build stage test environment. + - test -f "$PREFIX"/base +"#, + solver_to_run + ); + + let mut opt = TestOpt::try_parse_from([ + "test", + // Don't exec a new process to move into a new runtime, this confuses + // coverage testing. + "--no-runtime", + "--disable-repo=origin", + filename_str, + ]) + .unwrap(); + opt.test.run().await.unwrap(); +} diff --git a/crates/spk-cli/cmd-test/src/test/build.rs b/crates/spk-cli/cmd-test/src/test/build.rs index d3c4e547c0..c42ef40a2d 100644 --- a/crates/spk-cli/cmd-test/src/test/build.rs +++ b/crates/spk-cli/cmd-test/src/test/build.rs @@ -15,25 +15,32 @@ use spk_schema::foundation::option_map::OptionMap; use spk_schema::ident::{PkgRequest, PreReleasePolicy, RangeIdent, Request, RequestedBy}; use spk_schema::{AnyIdent, Recipe, SpecRecipe}; use spk_solve::solution::Solution; -use spk_solve::{BoxedResolverCallback, DefaultResolver, ResolverCallback, StepSolver}; +use spk_solve::{DecisionFormatter, SolverExt, SolverMut}; use spk_storage as storage; use super::Tester; -pub struct PackageBuildTester<'a> { +pub struct PackageBuildTester +where + Solver: Send, +{ prefix: PathBuf, recipe: SpecRecipe, script: String, repos: Vec>, + solver: Solver, options: OptionMap, additional_requirements: Vec, source: BuildSource, - source_resolver: BoxedResolverCallback<'a>, - build_resolver: BoxedResolverCallback<'a>, + source_formatter: DecisionFormatter, + build_formatter: DecisionFormatter, } -impl<'a> PackageBuildTester<'a> { - pub fn new(recipe: SpecRecipe, script: String) -> Self { +impl PackageBuildTester +where + Solver: SolverExt + SolverMut + Clone + Send, +{ + pub fn new(recipe: SpecRecipe, script: String, solver: Solver) -> Self { let source = BuildSource::SourcePackage(recipe.ident().to_any_ident(Some(Build::Source)).into()); Self { @@ -41,11 +48,12 @@ impl<'a> PackageBuildTester<'a> { recipe, script, repos: Vec::new(), + solver, options: OptionMap::default(), additional_requirements: Vec::new(), source, - source_resolver: Box::new(DefaultResolver {}), - build_resolver: Box::new(DefaultResolver {}), + source_formatter: DecisionFormatter::default(), + build_formatter: DecisionFormatter::default(), } } @@ -74,31 +82,15 @@ impl<'a> PackageBuildTester<'a> { self } - /// Provide a function that will be called when resolving the source package. - /// - /// This function should run the provided solver runtime to - /// completion, returning the final result. This function - /// is useful for introspecting and reporting on the solve - /// process as needed. - pub fn with_source_resolver(&mut self, resolver: F) -> &mut Self - where - F: ResolverCallback + 'a, - { - self.source_resolver = Box::new(resolver); + /// Provide a formatter to use when resolving the source package. + pub fn with_source_formatter(&mut self, formatter: DecisionFormatter) -> &mut Self { + self.source_formatter = formatter; self } - /// Provide a function that will be called when resolving the build environment. - /// - /// This function should run the provided solver runtime to - /// completion, returning the final result. This function - /// is useful for introspecting and reporting on the solve - /// process as needed. - pub fn with_build_resolver(&mut self, resolver: F) -> &mut Self - where - F: ResolverCallback + 'a, - { - self.build_resolver = Box::new(resolver); + /// Provide a formatter to use when resolving the build environment. + pub fn with_build_formatter(&mut self, formatter: DecisionFormatter) -> &mut Self { + self.build_formatter = formatter; self } @@ -117,18 +109,22 @@ impl<'a> PackageBuildTester<'a> { } } - let mut solver = StepSolver::default(); + // Use a clone of the solver before changing settings so + // `resolve_source_package` can do the same. + let mut solver = self.solver.clone(); solver.set_binary_only(true); solver.update_options(self.options.clone()); for repo in self.repos.iter().cloned() { solver.add_repository(repo); } + // Configure solver for build environment. solver.configure_for_build_environment(&self.recipe)?; for request in self.additional_requirements.drain(..) { solver.add_request(request) } - let (solution, _) = self.build_resolver.solve(&solver).await?; + // let (solution, _) = self.build_resolver.solve(&solver).await?; + let solution = solver.run_and_print_resolve(&self.build_formatter).await?; for layer in resolve_runtime_layers(requires_localization, &solution).await? { rt.push_digest(layer); @@ -154,7 +150,9 @@ impl<'a> PackageBuildTester<'a> { } async fn resolve_source_package(&mut self, package: &AnyIdent) -> Result { - let mut solver = StepSolver::default(); + // Use a clone of the solver before changing settings so `test` can do + // the same. + let mut solver = self.solver.clone(); solver.update_options(self.options.clone()); let local_repo: Arc = Arc::new(storage::local_repository().await?.into()); @@ -175,15 +173,19 @@ impl<'a> PackageBuildTester<'a> { solver.add_request(request.into()); - let (solution, _) = self.source_resolver.solve(&solver).await?; + // let (solution, _) = self.source_resolver.solve(&solver).await?; + let solution = solver.run_and_print_resolve(&self.source_formatter).await?; Ok(solution) } } #[async_trait::async_trait] -impl Tester for PackageBuildTester<'_> { +impl Tester for PackageBuildTester +where + Solver: SolverExt + SolverMut + Clone + Send, +{ async fn test(&mut self) -> Result<()> { - self.test().await + PackageBuildTester::test(self).await } fn prefix(&self) -> &Path { &self.prefix diff --git a/crates/spk-cli/cmd-test/src/test/install.rs b/crates/spk-cli/cmd-test/src/test/install.rs index eac4b64ea5..0921a66e28 100644 --- a/crates/spk-cli/cmd-test/src/test/install.rs +++ b/crates/spk-cli/cmd-test/src/test/install.rs @@ -12,37 +12,43 @@ use spk_schema::foundation::option_map::OptionMap; use spk_schema::ident::{PkgRequest, PreReleasePolicy, RangeIdent, Request, RequestedBy}; use spk_schema::ident_build::Build; use spk_schema::{Recipe, SpecRecipe, Variant, VariantExt}; -use spk_solve::{BoxedResolverCallback, DefaultResolver, ResolverCallback, StepSolver}; +use spk_solve::{DecisionFormatter, SolverExt, SolverMut}; use spk_storage as storage; use super::Tester; -pub struct PackageInstallTester<'a, V> { +pub struct PackageInstallTester +where + Solver: Send, +{ prefix: PathBuf, recipe: SpecRecipe, script: String, repos: Vec>, + solver: Solver, options: OptionMap, additional_requirements: Vec, source: Option, - env_resolver: BoxedResolverCallback<'a>, + env_formatter: DecisionFormatter, variant: V, } -impl<'a, V> PackageInstallTester<'a, V> +impl PackageInstallTester where V: Clone + Variant + Send, + Solver: SolverExt + SolverMut + Send, { - pub fn new(recipe: SpecRecipe, script: String, variant: V) -> Self { + pub fn new(recipe: SpecRecipe, script: String, variant: V, solver: Solver) -> Self { Self { prefix: PathBuf::from("/spfs"), recipe, script, repos: Vec::new(), + solver, options: OptionMap::default(), additional_requirements: Vec::new(), source: None, - env_resolver: Box::new(DefaultResolver {}), + env_formatter: DecisionFormatter::default(), variant, } } @@ -72,17 +78,9 @@ where self } - /// Provide a function that will be called when resolving the test environment. - /// - /// This function should run the provided solver runtime to - /// completion, returning the final result. This function - /// is useful for introspecting and reporting on the solve - /// process as needed. - pub fn watch_environment_resolve(&mut self, resolver: F) -> &mut Self - where - F: ResolverCallback + 'a, - { - self.env_resolver = Box::new(resolver); + /// Provide a formatter to use when resolving the test environment. + pub fn watch_environment_formatter(&mut self, formatter: DecisionFormatter) -> &mut Self { + self.env_formatter = formatter; self } @@ -94,11 +92,10 @@ where let requires_localization = rt.config.mount_backend.requires_localization(); - let mut solver = StepSolver::default(); - solver.set_binary_only(true); - solver.update_options(self.options.clone()); + self.solver.set_binary_only(true); + self.solver.update_options(self.options.clone()); for repo in self.repos.iter().cloned() { - solver.add_repository(repo); + self.solver.add_repository(repo); } // Request the specific build that goes with the selected build variant. @@ -117,12 +114,16 @@ where .with_prerelease(Some(PreReleasePolicy::IncludeAll)) .with_pin(None) .with_compat(None); - solver.add_request(request.into()); + self.solver.add_request(request.into()); for request in self.additional_requirements.drain(..) { - solver.add_request(request) + self.solver.add_request(request) } - let (solution, _) = self.env_resolver.solve(&solver).await?; + // let (solution, _) = self.env_resolver.solve(&solver).await?; + let solution = self + .solver + .run_and_print_resolve(&self.env_formatter) + .await?; for layer in resolve_runtime_layers(requires_localization, &solution).await? { rt.push_digest(layer); @@ -142,9 +143,10 @@ where } #[async_trait::async_trait] -impl Tester for PackageInstallTester<'_, V> +impl Tester for PackageInstallTester where V: Clone + Variant + Send, + Solver: SolverExt + SolverMut + Send, { async fn test(&mut self) -> Result<()> { PackageInstallTester::test(self).await diff --git a/crates/spk-cli/cmd-test/src/test/sources.rs b/crates/spk-cli/cmd-test/src/test/sources.rs index bef95c641a..24272c08e5 100644 --- a/crates/spk-cli/cmd-test/src/test/sources.rs +++ b/crates/spk-cli/cmd-test/src/test/sources.rs @@ -13,33 +13,41 @@ use spk_schema::foundation::ident_component::Component; use spk_schema::foundation::option_map::OptionMap; use spk_schema::ident::{PkgRequest, PreReleasePolicy, RangeIdent, Request, RequestedBy}; use spk_schema::{Recipe, SpecRecipe}; -use spk_solve::{BoxedResolverCallback, DefaultResolver, ResolverCallback, StepSolver}; +use spk_solve::{DecisionFormatter, SolverExt, SolverMut}; use spk_storage as storage; use super::Tester; -pub struct PackageSourceTester<'a> { +pub struct PackageSourceTester +where + Solver: Send, +{ prefix: PathBuf, recipe: SpecRecipe, script: String, repos: Vec>, + solver: Solver, options: OptionMap, additional_requirements: Vec, source: Option, - env_resolver: BoxedResolverCallback<'a>, + env_formatter: DecisionFormatter, } -impl<'a> PackageSourceTester<'a> { - pub fn new(recipe: SpecRecipe, script: String) -> Self { +impl PackageSourceTester +where + Solver: SolverExt + SolverMut + Send, +{ + pub fn new(recipe: SpecRecipe, script: String, solver: Solver) -> Self { Self { prefix: PathBuf::from("/spfs"), recipe, script, repos: Vec::new(), + solver, options: OptionMap::default(), additional_requirements: Vec::new(), source: None, - env_resolver: Box::new(DefaultResolver {}), + env_formatter: DecisionFormatter::default(), } } @@ -69,17 +77,9 @@ impl<'a> PackageSourceTester<'a> { self } - /// Provide a function that will be called when resolving the test environment. - /// - /// This function should run the provided solver runtime to - /// completion, returning the final result. This function - /// is useful for introspecting and reporting on the solve - /// process as needed. - pub fn watch_environment_resolve(&mut self, resolver: F) -> &mut Self - where - F: ResolverCallback + 'a, - { - self.env_resolver = Box::new(resolver); + /// Provide a formatter to use when resolving the test environment. + pub fn watch_environment_formatter(&mut self, formatter: DecisionFormatter) -> &mut Self { + self.env_formatter = formatter; self } @@ -92,11 +92,10 @@ impl<'a> PackageSourceTester<'a> { let requires_localization = rt.config.mount_backend.requires_localization(); - let mut solver = StepSolver::default(); - solver.set_binary_only(true); - solver.update_options(self.options.clone()); + self.solver.set_binary_only(true); + self.solver.update_options(self.options.clone()); for repo in self.repos.iter().cloned() { - solver.add_repository(repo); + self.solver.add_repository(repo); } if self.source.is_none() { @@ -109,14 +108,18 @@ impl<'a> PackageSourceTester<'a> { .with_prerelease(Some(PreReleasePolicy::IncludeAll)) .with_pin(None) .with_compat(None); - solver.add_request(request.into()); + self.solver.add_request(request.into()); } for request in self.additional_requirements.drain(..) { - solver.add_request(request) + self.solver.add_request(request) } - let (solution, _) = self.env_resolver.solve(&solver).await?; + // let (solution, _) = self.env_resolver.solve(&solver).await?; + let solution = self + .solver + .run_and_print_resolve(&self.env_formatter) + .await?; for layer in resolve_runtime_layers(requires_localization, &solution).await? { rt.push_digest(layer); @@ -137,9 +140,12 @@ impl<'a> PackageSourceTester<'a> { } #[async_trait::async_trait] -impl Tester for PackageSourceTester<'_> { +impl Tester for PackageSourceTester +where + Solver: SolverExt + SolverMut + Send, +{ async fn test(&mut self) -> Result<()> { - self.test().await + PackageSourceTester::test(self).await } fn prefix(&self) -> &Path { &self.prefix diff --git a/crates/spk-cli/common/src/exec.rs b/crates/spk-cli/common/src/exec.rs index 7a83024360..f753b6f7a0 100644 --- a/crates/spk-cli/common/src/exec.rs +++ b/crates/spk-cli/common/src/exec.rs @@ -8,6 +8,7 @@ use spk_build::BinaryPackageBuilder; use spk_schema::Package; use spk_schema::foundation::format::{FormatIdent, FormatOptionMap}; use spk_solve::solution::{PackageSource, Solution}; +use spk_solve::{SolverExt, SolverMut}; use spk_storage as storage; use crate::Result; @@ -15,7 +16,13 @@ use crate::Result; /// Build any packages in the given solution that need building. /// /// Returns a new solution of only binary packages. -pub async fn build_required_packages(solution: &Solution) -> Result { +pub async fn build_required_packages( + solution: &Solution, + solver: Solver, +) -> Result +where + Solver: SolverExt + SolverMut + Clone, +{ let handle: storage::RepositoryHandle = storage::local_repository().await?.into(); let local_repo = Arc::new(handle); let repos = solution.repositories(); @@ -35,10 +42,11 @@ pub async fn build_required_packages(solution: &Solution) -> Result { item.spec.ident().format_ident(), options.format_option_map() ); - let (package, components) = BinaryPackageBuilder::from_recipe((**recipe).clone()) - .with_repositories(repos.clone()) - .build_and_publish(&options, &*local_repo) - .await?; + let (package, components) = + BinaryPackageBuilder::from_recipe_with_solver((**recipe).clone(), solver.clone()) + .with_repositories(repos.clone()) + .build_and_publish(&options, &*local_repo) + .await?; let source = PackageSource::Repository { repo: local_repo.clone(), components, diff --git a/crates/spk-cli/common/src/flags.rs b/crates/spk-cli/common/src/flags.rs index 3c5954313a..724c6f98af 100644 --- a/crates/spk-cli/common/src/flags.rs +++ b/crates/spk-cli/common/src/flags.rs @@ -15,6 +15,9 @@ use solve::{ DecisionFormatter, DecisionFormatterBuilder, MultiSolverKind, + SolverExt, + SolverImpl, + SolverMut, }; use spk_schema::foundation::format::FormatIdent; use spk_schema::foundation::ident_build::Build; @@ -232,6 +235,9 @@ pub struct Solver { #[clap(flatten)] pub repos: Repositories, + #[clap(flatten)] + pub decision_formatter_settings: DecisionFormatterSettings, + /// If true, build packages from source if needed #[clap(long)] pub allow_builds: bool, @@ -257,10 +263,30 @@ pub struct Solver { } impl Solver { - pub async fn get_solver(&self, options: &Options) -> Result { + pub async fn get_solver( + &self, + options: &Options, + ) -> Result { let option_map = options.get_options()?; - let mut solver = solve::StepSolver::default(); + let mut solver = match self.decision_formatter_settings.solver_to_run { + SolverToRun::Resolvo => SolverImpl::Resolvo(solve::ResolvoSolver::default()), + _ => { + let mut solver = solve::StepSolver::default(); + // These settings are only applicable to the Step solver. + solver.set_initial_request_impossible_checks( + self.check_impossible_initial || self.check_impossible_all, + ); + solver.set_resolve_validation_impossible_checks( + self.check_impossible_validation || self.check_impossible_all, + ); + solver.set_build_key_impossible_checks( + self.check_impossible_builds || self.check_impossible_all, + ); + SolverImpl::Step(solver) + } + }; + solver.update_options(option_map); for (name, repo) in self.repos.get_repos_for_non_destructive_operation().await? { @@ -268,15 +294,6 @@ impl Solver { solver.add_repository(repo); } solver.set_binary_only(!self.allow_builds); - solver.set_initial_request_impossible_checks( - self.check_impossible_initial || self.check_impossible_all, - ); - solver.set_resolve_validation_impossible_checks( - self.check_impossible_validation || self.check_impossible_all, - ); - solver.set_build_key_impossible_checks( - self.check_impossible_builds || self.check_impossible_all, - ); for r in options.get_var_requests()? { solver.add_request(r.into()); @@ -629,7 +646,7 @@ impl Requests { let mut req = serde_yaml::from_value::(request_data.into()) .into_diagnostic() .wrap_err_with(|| format!("Failed to parse request {request}"))? - .into_pkg() + .pkg() .ok_or_else(|| miette!("Expected a package request, got None"))?; req.add_requester(RequestedBy::CommandLine); @@ -1172,17 +1189,22 @@ pub enum SolverToRun { Cli, /// Run and show output from the "impossible requests" checking solver Checks, - /// Run both solvers, showing the output from the basic solver, - /// unless overridden with --solver-to-run + /// Run both "cli" and "checks" solvers, showing the output from the "cli" + /// solver, unless overridden with --solver-to-run All, + /// Run the Resolvo-based SAT solver + Resolvo, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum SolverToShow { - /// Show output from the basic solver + /// Show output from the basic solver. Cli, - /// Show output from the "impossible requests" checking solver + /// Show output from the "impossible requests" checking solver. Checks, + /// Show output from the Resolvo SAT solver. This is only possible when + /// running with that solver. + Resolvo, } impl From for MultiSolverKind { @@ -1191,6 +1213,7 @@ impl From for MultiSolverKind { SolverToRun::Cli => MultiSolverKind::Unchanged, SolverToRun::Checks => MultiSolverKind::AllImpossibleChecks, SolverToRun::All => MultiSolverKind::All, + SolverToRun::Resolvo => MultiSolverKind::Resolvo, } } } @@ -1200,6 +1223,7 @@ impl From for MultiSolverKind { match item { SolverToShow::Cli => MultiSolverKind::Unchanged, SolverToShow::Checks => MultiSolverKind::AllImpossibleChecks, + SolverToShow::Resolvo => MultiSolverKind::Resolvo, } } } diff --git a/crates/spk-cli/common/src/flags_test.rs b/crates/spk-cli/common/src/flags_test.rs index 3069bf9aff..fcd4c6c9bd 100644 --- a/crates/spk-cli/common/src/flags_test.rs +++ b/crates/spk-cli/common/src/flags_test.rs @@ -7,6 +7,9 @@ use spk_schema::foundation::name::OptName; use spk_schema::foundation::option_map::OptionMap; use spk_schema::ident::VarRequest; use spk_schema::option_map::HOST_OPTIONS; +use spk_solve::Solver; + +use crate::flags::{DecisionFormatterSettings, SolverToRun, SolverToShow}; #[rstest] #[case(&["hello:world"], &[("hello", "world")])] @@ -37,13 +40,20 @@ fn test_option_flags_parsing(#[case] args: &[&str], #[case] expected: &[(&str, & } #[rstest] -#[case::no_host_true(true)] -#[case::no_host_false(false)] +#[case::cli(SolverToRun::Cli, SolverToShow::Cli)] +#[case::cli(SolverToRun::Checks, SolverToShow::Checks)] +#[case::cli(SolverToRun::Resolvo, SolverToShow::Resolvo)] #[tokio::test] -async fn test_get_solver_with_host_options(#[case] no_host: bool) { +async fn test_get_solver_with_host_options( + #[case] solver_to_run: SolverToRun, + #[case] solver_to_show: SolverToShow, + #[values(true, false)] no_host: bool, +) { // Test the get_solver() method adds the host options to the solver // correctly. + use std::collections::HashSet; + let options_flags = crate::flags::Options { options: Vec::new(), no_host, @@ -58,6 +68,26 @@ async fn test_get_solver_with_host_options(#[case] no_host: bool) { when: None, legacy_spk_version_tags: false, }, + decision_formatter_settings: DecisionFormatterSettings { + time: Default::default(), + increase_verbosity: Default::default(), + max_verbosity_increase_level: Default::default(), + timeout: Default::default(), + show_solution: Default::default(), + long_solves: Default::default(), + max_frequent_errors: Default::default(), + status_bar: Default::default(), + solver_to_run, + solver_to_show, + show_search_size: Default::default(), + compare_solvers: Default::default(), + stop_on_block: Default::default(), + step_on_block: Default::default(), + step_on_decision: Default::default(), + output_to_dir: Default::default(), + output_to_dir_min_verbosity: Default::default(), + output_file_prefix: Default::default(), + }, allow_builds: false, check_impossible_initial: false, check_impossible_validation: false, @@ -66,7 +96,10 @@ async fn test_get_solver_with_host_options(#[case] no_host: bool) { }; let solver = solver_flags.get_solver(&options_flags).await.unwrap(); - let initial_state = solver.get_initial_state(); + let var_requests = solver + .get_var_requests() + .into_iter() + .collect::>(); assert!( !HOST_OPTIONS.get().unwrap().is_empty(), @@ -76,9 +109,9 @@ async fn test_get_solver_with_host_options(#[case] no_host: bool) { for (name, value) in HOST_OPTIONS.get().unwrap() { let var_request = VarRequest::new_with_value(name, value); if no_host { - assert!(!initial_state.contains_var_request(&var_request)); + assert!(!var_requests.contains(&var_request)); } else { - assert!(initial_state.contains_var_request(&var_request)); + assert!(var_requests.contains(&var_request)); } } } diff --git a/crates/spk-cli/group1/src/cmd_bake.rs b/crates/spk-cli/group1/src/cmd_bake.rs index 0f992f3a1a..9612825671 100644 --- a/crates/spk-cli/group1/src/cmd_bake.rs +++ b/crates/spk-cli/group1/src/cmd_bake.rs @@ -10,6 +10,7 @@ use spk_cli_common::{CommandArgs, Run, current_env, flags}; use spk_schema::Package; use spk_schema::ident::RequestedBy; use spk_solve::solution::{LayerPackageAndComponents, PackageSource, get_spfs_layers_to_packages}; +use spk_solve::{Solver, SolverMut}; #[cfg(test)] #[path = "./cmd_bake_test.rs"] @@ -63,9 +64,6 @@ pub struct Bake { #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// The requests to resolve and bake #[clap(name = "REQUESTS")] pub requested: Vec, @@ -237,8 +235,11 @@ impl Bake { solver.add_request(request) } - let formatter = self.formatter_settings.get_formatter(self.verbose)?; - let (solution, _) = formatter.run_and_print_resolve(&solver).await?; + let formatter = self + .solver + .decision_formatter_settings + .get_formatter(self.verbose)?; + let solution = solver.run_and_print_resolve(&formatter).await?; // The solution order is the order things were found during // the solve. Need to reverse it to match up with the spfs diff --git a/crates/spk-cli/group3/src/cmd_export_test.rs b/crates/spk-cli/group3/src/cmd_export_test.rs index 900d134ec0..c1abb116b6 100644 --- a/crates/spk-cli/group3/src/cmd_export_test.rs +++ b/crates/spk-cli/group3/src/cmd_export_test.rs @@ -7,11 +7,22 @@ use spfs::prelude::*; use spk_build::{BinaryPackageBuilder, BuildSource}; use spk_schema::foundation::option_map; use spk_schema::{Package, recipe}; +use spk_solve::SolverImpl; use spk_storage::fixtures::*; +fn step_solver() -> SolverImpl { + SolverImpl::Step(spk_solve::StepSolver::default()) +} + +fn resolvo_solver() -> SolverImpl { + SolverImpl::Resolvo(spk_solve::ResolvoSolver::default()) +} + #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_export_works_with_missing_builds() { +async fn test_export_works_with_missing_builds(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let spec = recipe!( @@ -26,12 +37,13 @@ async fn test_export_works_with_missing_builds() { } ); rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (blue_spec, _) = BinaryPackageBuilder::from_recipe(spec.clone()) - .with_source(BuildSource::LocalPath(".".into())) - .build_and_publish(option_map! {"color" => "blue"}, &*rt.tmprepo) - .await - .unwrap(); - let (red_spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (blue_spec, _) = + BinaryPackageBuilder::from_recipe_with_solver(spec.clone(), solver.clone()) + .with_source(BuildSource::LocalPath(".".into())) + .build_and_publish(option_map! {"color" => "blue"}, &*rt.tmprepo) + .await + .unwrap(); + let (red_spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .build_and_publish(option_map! {"color" => "red"}, &*rt.tmprepo) .await diff --git a/crates/spk-cli/group3/src/cmd_import_test.rs b/crates/spk-cli/group3/src/cmd_import_test.rs index f0d72e0def..613c99fca1 100644 --- a/crates/spk-cli/group3/src/cmd_import_test.rs +++ b/crates/spk-cli/group3/src/cmd_import_test.rs @@ -7,11 +7,22 @@ use spk_build::{BinaryPackageBuilder, BuildSource}; use spk_cli_common::Run; use spk_schema::foundation::option_map; use spk_schema::{Package, recipe}; +use spk_solve::SolverImpl; use spk_storage::fixtures::*; +fn step_solver() -> SolverImpl { + SolverImpl::Step(spk_solve::StepSolver::default()) +} + +fn resolvo_solver() -> SolverImpl { + SolverImpl::Resolvo(spk_solve::ResolvoSolver::default()) +} + #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_archive_io() { +async fn test_archive_io(#[case] solver: SolverImpl) { let rt = spfs_runtime().await; let spec = recipe!( { @@ -20,7 +31,7 @@ async fn test_archive_io() { } ); rt.tmprepo.publish_recipe(&spec).await.unwrap(); - let (spec, _) = BinaryPackageBuilder::from_recipe(spec) + let (spec, _) = BinaryPackageBuilder::from_recipe_with_solver(spec, solver) .with_source(BuildSource::LocalPath(".".into())) .build_and_publish(option_map! {}, &*rt.tmprepo) .await diff --git a/crates/spk-cli/group4/src/cmd_view.rs b/crates/spk-cli/group4/src/cmd_view.rs index 18d96a2201..3d169973f6 100644 --- a/crates/spk-cli/group4/src/cmd_view.rs +++ b/crates/spk-cli/group4/src/cmd_view.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/spkenv/spk +use std::any::Any; use std::borrow::Cow; use std::collections::BTreeMap; use std::sync::Arc; @@ -33,8 +34,8 @@ use spk_schema::{ Variant, VersionIdent, }; -use spk_solve::Recipe; use spk_solve::solution::{LayerPackageAndComponents, get_spfs_layers_to_packages}; +use spk_solve::{Recipe, Solver, SolverMut}; use spk_storage; use strum::{Display, EnumString, IntoEnumIterator, VariantNames}; @@ -89,9 +90,6 @@ pub struct View { #[clap(short = 'f', long)] pub format: Option, - #[clap(flatten)] - pub formatter_settings: flags::DecisionFormatterSettings, - /// Explicitly get info on a filepath #[clap(short = 'F', long)] filepath: Option, @@ -606,7 +604,7 @@ impl View { async fn get_package_versions( &self, name: &PkgNameBuf, - repos: &Vec>, + repos: &[std::sync::Arc], ) -> Result> { let mut versions = Vec::new(); for repo in repos { @@ -648,37 +646,44 @@ impl View { _ => bail!("Not a package request: {request:?}"), }; - let mut runtime = solver.run(); - - let formatter = self.formatter_settings.get_formatter(self.verbose)?; - - let result = formatter.run_and_print_decisions(&mut runtime).await; - let solution = match result { - Ok((s, _)) => s, - Err(err) => { - println!("{}", err.to_string().red()); - match self.verbose { - 0 => eprintln!("{}", "try '--verbose' for more info".yellow().dimmed(),), - v if v < 2 => { - eprintln!("{}", "try '-vv' for even more info".yellow().dimmed(),) - } - _v => { - let graph = runtime.graph(); - let graph = graph.read().await; - // Iter much? - let mut graph_walk = graph.walk(); - let walk_iter = graph_walk.iter().map(Ok); - let mut decision_iter = formatter.formatted_decisions_iter(walk_iter); - let iter = decision_iter.iter(); - tokio::pin!(iter); - while let Some(line) = iter.try_next().await? { - println!("{line}"); + let solution = if let Some(solver) = + (&solver as &dyn Any).downcast_ref::() + { + let mut runtime = solver.run(); + let formatter = self + .solver + .decision_formatter_settings + .get_formatter(self.verbose)?; + let result = formatter.run_and_print_decisions(&mut runtime).await; + match result { + Ok((s, _)) => s, + Err(err) => { + println!("{}", err.to_string().red()); + match self.verbose { + 0 => eprintln!("{}", "try '--verbose' for more info".yellow().dimmed(),), + v if v < 2 => { + eprintln!("{}", "try '-vv' for even more info".yellow().dimmed(),) + } + _v => { + let graph = runtime.graph(); + let graph = graph.read().await; + // Iter much? + let mut graph_walk = graph.walk(); + let walk_iter = graph_walk.iter().map(Ok); + let mut decision_iter = formatter.formatted_decisions_iter(walk_iter); + let iter = decision_iter.iter(); + tokio::pin!(iter); + while let Some(line) = iter.try_next().await? { + println!("{line}"); + } } } - } - return Ok(1); + return Ok(1); + } } + } else { + solver.solve().await? }; for item in solution.items() { diff --git a/crates/spk-exec/src/exec_test.rs b/crates/spk-exec/src/exec_test.rs index bb085ef1c4..59f8604e45 100644 --- a/crates/spk-exec/src/exec_test.rs +++ b/crates/spk-exec/src/exec_test.rs @@ -9,7 +9,7 @@ use rstest::{fixture, rstest}; use spk_cmd_build::build_package; use spk_schema::foundation::fixtures::*; use spk_schema::ident::build_ident; -use spk_solve::{DecisionFormatterBuilder, StepSolver}; +use spk_solve::{DecisionFormatterBuilder, SolverExt, SolverMut, StepSolver}; use spk_solve_macros::request; use spk_storage::fixtures::*; @@ -23,10 +23,15 @@ fn solver() -> StepSolver { /// If two layers contribute files to the same subdirectory, the Manifest is /// expected to contain both files. #[rstest] +#[case::cli("cli")] +#[case::checks("checks")] +#[case::resolvo("resolvo")] #[tokio::test] async fn get_environment_filesystem_merges_directories( tmpdir: tempfile::TempDir, + // TODO: test with all solvers mut solver: StepSolver, + #[case] solver_to_run: &str, ) { let rt = spfs_runtime().await; @@ -42,6 +47,7 @@ build: - mkdir "$PREFIX"/subdir - touch "$PREFIX"/subdir/one.txt "#, + solver_to_run ); build_package!( @@ -56,6 +62,7 @@ build: - mkdir "$PREFIX"/subdir - touch "$PREFIX"/subdir/two.txt "#, + solver_to_run ); let formatter = DecisionFormatterBuilder::default() @@ -66,7 +73,7 @@ build: solver.add_request(request!("one")); solver.add_request(request!("two")); - let (solution, _) = formatter.run_and_log_resolve(&solver).await.unwrap(); + let solution = solver.run_and_log_resolve(&formatter).await.unwrap(); let resolved_layers = solution_to_resolved_runtime_layers(&solution).unwrap(); diff --git a/crates/spk-schema/crates/foundation/src/ident_build/build.rs b/crates/spk-schema/crates/foundation/src/ident_build/build.rs index fa2c0c012d..4b4f1daca4 100644 --- a/crates/spk-schema/crates/foundation/src/ident_build/build.rs +++ b/crates/spk-schema/crates/foundation/src/ident_build/build.rs @@ -130,6 +130,10 @@ impl Build { } } + pub fn is_buildid(&self) -> bool { + matches!(self, Build::BuildId(_)) + } + pub fn is_source(&self) -> bool { matches!(self, Build::Source) } @@ -177,6 +181,14 @@ impl std::fmt::Display for Build { } } +impl TryFrom for Build { + type Error = super::Error; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + impl FromStr for Build { type Err = super::Error; diff --git a/crates/spk-schema/crates/foundation/src/name/mod.rs b/crates/spk-schema/crates/foundation/src/name/mod.rs index 901fbb4c8f..1a962ee9e3 100644 --- a/crates/spk-schema/crates/foundation/src/name/mod.rs +++ b/crates/spk-schema/crates/foundation/src/name/mod.rs @@ -31,7 +31,7 @@ mod name_test; /// ``` #[macro_export] macro_rules! pkg_name { - ($name:literal) => { + ($name:expr) => { $crate::name::PkgName::new($name).unwrap() }; } diff --git a/crates/spk-schema/crates/foundation/src/version/mod.rs b/crates/spk-schema/crates/foundation/src/version/mod.rs index 6e27423822..b7ef793551 100644 --- a/crates/spk-schema/crates/foundation/src/version/mod.rs +++ b/crates/spk-schema/crates/foundation/src/version/mod.rs @@ -554,6 +554,14 @@ impl TryFrom<&str> for Version { } } +impl TryFrom for Version { + type Error = Error; + + fn try_from(value: String) -> Result { + parse_version(value) + } +} + impl FromStr for Version { type Err = Error; diff --git a/crates/spk-schema/crates/ident/Cargo.toml b/crates/spk-schema/crates/ident/Cargo.toml index 44e72b5e4e..c8e28fb572 100644 --- a/crates/spk-schema/crates/ident/Cargo.toml +++ b/crates/spk-schema/crates/ident/Cargo.toml @@ -31,6 +31,7 @@ spk-schema-foundation = { workspace = true } tap = { workspace = true } thiserror = { workspace = true } miette = { workspace = true } +variantly = { workspace = true } [dev-dependencies] data-encoding = "2.3" diff --git a/crates/spk-schema/crates/ident/src/ident_build.rs b/crates/spk-schema/crates/ident/src/ident_build.rs index f9dd39aee5..2eda97b46e 100644 --- a/crates/spk-schema/crates/ident/src/ident_build.rs +++ b/crates/spk-schema/crates/ident/src/ident_build.rs @@ -6,7 +6,7 @@ use std::fmt::Write; use std::str::FromStr; use relative_path::RelativePathBuf; -use spk_schema_foundation::ident_build::Build; +use spk_schema_foundation::ident_build::{Build, EmbeddedSourcePackage}; use spk_schema_foundation::ident_ops::parsing::IdentPartsBuf; use spk_schema_foundation::ident_ops::{MetadataPath, TagPath}; use spk_schema_foundation::name::{PkgName, PkgNameBuf, RepositoryNameBuf}; @@ -30,6 +30,34 @@ pub type BuildIdent = Ident; crate::ident_version::version_ident_methods!(BuildIdent, .base); +impl TryFrom for BuildIdent { + type Error = Error; + + fn try_from(value: EmbeddedSourcePackage) -> std::result::Result { + let IdentPartsBuf { + repository_name: _, + pkg_name, + version_str: Some(version), + build_str: Some(build), + } = value.ident + else { + return if value.ident.build_str.is_some() { + Err(Error::String( + "EmbeddedSourcePackage missing version".to_string(), + )) + } else { + Err(Error::String( + "EmbeddedSourcePackage missing build".to_string(), + )) + }; + }; + Ok(Self::new( + VersionIdent::new(pkg_name.try_into()?, version.try_into()?), + build.try_into()?, + )) + } +} + macro_rules! build_ident_methods { ($Ident:ty $(, .$($access:ident).+)?) => { impl $Ident { diff --git a/crates/spk-schema/crates/ident/src/ident_located.rs b/crates/spk-schema/crates/ident/src/ident_located.rs index 4f924c4adc..98121e809d 100644 --- a/crates/spk-schema/crates/ident/src/ident_located.rs +++ b/crates/spk-schema/crates/ident/src/ident_located.rs @@ -86,3 +86,9 @@ impl TagPath for LocatedBuildIdent { .join(self.build().verbatim_tag_path()) } } + +impl std::fmt::Display for LocatedBuildIdent { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}/{}", self.base(), self.target()) + } +} diff --git a/crates/spk-schema/crates/ident/src/request.rs b/crates/spk-schema/crates/ident/src/request.rs index 2a25da91e7..96f7532fb3 100644 --- a/crates/spk-schema/crates/ident/src/request.rs +++ b/crates/spk-schema/crates/ident/src/request.rs @@ -39,6 +39,7 @@ use spk_schema_foundation::version_range::{ VersionFilter, }; use tap::Tap; +use variantly::Variantly; use super::AnyIdent; use crate::{BuildIdent, Error, RangeIdent, Result, Satisfy, VersionIdent}; @@ -176,7 +177,7 @@ impl<'de> Deserialize<'de> for PinPolicy { } /// Represents a constraint added to a resolved environment. -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Variantly)] #[serde(untagged)] pub enum Request { Pkg(PkgRequest), @@ -192,30 +193,6 @@ impl spk_schema_foundation::spec_ops::Named for Request { } } -impl Request { - pub fn is_pkg(&self) -> bool { - matches!(self, Self::Pkg(_)) - } - - pub fn into_pkg(self) -> Option { - match self { - Self::Pkg(p) => Some(p), - _ => None, - } - } - - pub fn is_var(&self) -> bool { - matches!(self, Self::Var(_)) - } - - pub fn into_var(self) -> Option { - match self { - Self::Var(v) => Some(v), - _ => None, - } - } -} - impl std::fmt::Display for Request { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/spk-schema/crates/ident/src/request_test.rs b/crates/spk-schema/crates/ident/src/request_test.rs index 4433f9061f..240d6a7bb9 100644 --- a/crates/spk-schema/crates/ident/src/request_test.rs +++ b/crates/spk-schema/crates/ident/src/request_test.rs @@ -73,11 +73,11 @@ fn test_prerelease_policy_restricts( ) { let mut a = serde_yaml::from_str::(request_a) .unwrap() - .into_pkg() + .pkg() .expect("expected pkg request"); let b = serde_yaml::from_str::(request_b) .unwrap() - .into_pkg() + .pkg() .expect("expected pkg request"); a.restrict(&b).unwrap(); @@ -149,11 +149,11 @@ fn test_prerelease_policy_contains( ) { let a = serde_yaml::from_str::(request_a) .unwrap() - .into_pkg() + .pkg() .expect("expected pkg request"); let b = serde_yaml::from_str::(request_b) .unwrap() - .into_pkg() + .pkg() .expect("expected pkg request"); let compat = a.contains(&b); @@ -164,11 +164,11 @@ fn test_prerelease_policy_contains( fn test_inclusion_policy() { let mut a = serde_yaml::from_str::("{pkg: something, include: IfAlreadyPresent}") .unwrap() - .into_pkg() + .pkg() .expect("expected pkg request"); let b = serde_yaml::from_str::("{pkg: something, include: Always}") .unwrap() - .into_pkg() + .pkg() .expect("expected pkg request"); a.restrict(&b).unwrap(); @@ -182,11 +182,11 @@ fn test_inclusion_policy() { fn test_compat_and_equals_restrict() { let mut a = serde_yaml::from_str::("{pkg: something/Binary:1.2.3}") .unwrap() - .into_pkg() + .pkg() .expect("expected pkg request"); let b = serde_yaml::from_str::("{pkg: something/=1.2.3}") .unwrap() - .into_pkg() + .pkg() .expect("expected pkg request"); a.restrict(&b).unwrap(); @@ -247,14 +247,8 @@ fn test_inclusion_policy_and_merge( #[case] expected_policy: InclusionPolicy, #[case] expected_merged_range: Option<&str>, ) { - let mut a = serde_yaml::from_str::(a) - .unwrap() - .into_pkg() - .unwrap(); - let b = serde_yaml::from_str::(b) - .unwrap() - .into_pkg() - .unwrap(); + let mut a = serde_yaml::from_str::(a).unwrap().pkg().unwrap(); + let b = serde_yaml::from_str::(b).unwrap().pkg().unwrap(); let r = a.restrict(&b); match expected_merged_range { @@ -306,7 +300,7 @@ fn test_var_request_pinned_roundtrip() { "should be able to round-trip serialize a var request with pin" ); assert!( - res.unwrap().into_var().unwrap().value.is_from_build_env(), + res.unwrap().var().unwrap().value.is_from_build_env(), "should preserve pin value" ); } @@ -347,7 +341,7 @@ fn test_pkg_request_pin_rendering( ) { let req = serde_yaml::from_str::(&format!("{{pkg: test, fromBuildEnv: {pin}}}")) .unwrap() - .into_pkg() + .pkg() .expect("expected package request"); let version = parse_build_ident(format!("test/{version}/src")).unwrap(); let res = req @@ -473,7 +467,7 @@ fn test_deserialize_pkg_pin_string_or_bool() { let reqs = Vec::::from_yaml(YAML).expect("expected yaml parsing to succeed"); let pins: Vec<_> = reqs .into_iter() - .map(|r| r.into_pkg().expect("expected a pkg request").pin) + .map(|r| r.pkg().expect("expected a pkg request").pin) .collect(); assert_eq!( pins, diff --git a/crates/spk-solve/Cargo.toml b/crates/spk-solve/Cargo.toml index b6feaa292a..8b6be31bcf 100644 --- a/crates/spk-solve/Cargo.toml +++ b/crates/spk-solve/Cargo.toml @@ -35,30 +35,35 @@ console = { workspace = true } crossterm = "0.28.1" ctrlc = "3.2" dyn-clone = { workspace = true } +enum_dispatch = { workspace = true } futures = { workspace = true } itertools = { workspace = true } -once_cell = { workspace = true } -priority-queue = "1.2" +miette = { workspace = true } num-bigint = "0.4.3" num-format = { version = "0.4.4", features = ["with-num-bigint"] } -serde_json = { workspace = true } +once_cell = { workspace = true } +priority-queue = "1.2" +resolvo = { workspace = true, features = ["tokio"] } sentry = { workspace = true, optional = true } +serde_json = { workspace = true } signal-hook = "0.3" spfs = { workspace = true } spk-config = { workspace = true } +spk-schema = { workspace = true } spk-solve-graph = { workspace = true } spk-solve-package-iterator = { workspace = true } spk-solve-solution = { workspace = true } spk-solve-validation = { workspace = true } -spk-schema = { workspace = true } spk-storage = { workspace = true } statsd = { version = "0.15.0", optional = true } +strum = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt"] } tracing = { workspace = true } -miette = { workspace = true } +variantly = { workspace = true } [dev-dependencies] rstest = { workspace = true } spk-solve-macros = { workspace = true } strip-ansi-escapes = { workspace = true } +tap = { workspace = true } diff --git a/crates/spk-solve/crates/package-iterator/src/lib.rs b/crates/spk-solve/crates/package-iterator/src/lib.rs index b81bd47433..38aa8680e7 100644 --- a/crates/spk-solve/crates/package-iterator/src/lib.rs +++ b/crates/spk-solve/crates/package-iterator/src/lib.rs @@ -7,10 +7,12 @@ mod error; mod package_iterator; mod promotion_patterns; +pub use build_key::BuildKey; pub use error::{Error, Result}; pub use package_iterator::{ BUILD_SORT_TARGET, BuildIterator, + BuildToSortedOptName, EmptyBuildIterator, PackageIterator, RepositoryPackageIterator, diff --git a/crates/spk-solve/crates/package-iterator/src/package_iterator.rs b/crates/spk-solve/crates/package-iterator/src/package_iterator.rs index 3d4f1dd76d..15e14bcc3d 100644 --- a/crates/spk-solve/crates/package-iterator/src/package_iterator.rs +++ b/crates/spk-solve/crates/package-iterator/src/package_iterator.rs @@ -468,64 +468,17 @@ struct ChangeCounter { pub use_it: bool, } -impl SortedBuildIterator { - pub async fn new( - _options: OptionMap, - source: Arc>, - builds_with_impossible_requests: HashMap, - ) -> Result { - // Note: _options is unused in this implementation, it was used - // in the by_distance sorting implementation - let mut builds = VecDeque::::new(); - { - let mut source_lock = source.lock().await; - while let Some(item) = source_lock.next().await? { - builds.push_back(item); - } - } - - let mut sbi = SortedBuildIterator { builds }; - - sbi.sort_by_build_option_values(builds_with_impossible_requests) - .await; - Ok(sbi) - } - - /// Helper for making BuildKey structures used in the sorting in - /// sort_by_build_option_values() below - fn make_option_values_build_key( - spec: &Spec, - ordered_names: &Vec, - build_name_values: &HashMap, - makes_an_impossible_request: bool, - ) -> BuildKey { - let build_id = spec.ident(); - let empty = OptionMap::default(); - let name_values = match build_name_values.get(build_id) { - Some(nv) => nv, - None => &empty, - }; - BuildKey::new( - spec.ident(), - ordered_names, - name_values, - makes_an_impossible_request, - ) - } - - /// Sorts builds by keys based on ordered build option names and - /// differing values in those options - async fn sort_by_build_option_values( - &mut self, - builds_with_impossible_requests: HashMap, - ) { - let start = Instant::now(); +pub struct BuildToSortedOptName {} +impl BuildToSortedOptName { + pub fn sort_builds<'a>( + builds: impl Iterator>, + ) -> (Vec, HashMap) { let mut number_non_src_builds: u64 = 0; let mut build_name_values: HashMap = HashMap::default(); let mut changes: HashMap = HashMap::new(); - for (build, _) in self.builds.iter().flat_map(|hm| hm.values()) { + for build in builds { // Skip this if it's a '/src' build because '/src' builds // won't use the build option values in their key, they // don't need to be looked at. They have a type of key @@ -609,28 +562,88 @@ impl SortedBuildIterator { // names should be added to the configuration // BUILD_KEY_NAME_ORDER to ensure they fall in the correct // position for a site's spk setup. - let mut ordered_names = key_entry_names.clone(); - BUILD_KEY_NAME_ORDER.promote_names(ordered_names.as_mut_slice(), |n| n); + BUILD_KEY_NAME_ORDER.promote_names(key_entry_names.as_mut_slice(), |n| n); + + (key_entry_names, build_name_values) + } +} + +impl SortedBuildIterator { + pub async fn new( + _options: OptionMap, + source: Arc>, + builds_with_impossible_requests: HashMap, + ) -> Result { + // Note: _options is unused in this implementation, it was used + // in the by_distance sorting implementation + let mut builds = VecDeque::::new(); + { + let mut source_lock = source.lock().await; + while let Some(item) = source_lock.next().await? { + builds.push_back(item); + } + } + + let mut sbi = SortedBuildIterator { builds }; + + sbi.sort_by_build_option_values(builds_with_impossible_requests) + .await; + Ok(sbi) + } + + /// Helper for making BuildKey structures used in the sorting in + /// sort_by_build_option_values() below + pub fn make_option_values_build_key( + spec: &Spec, + ordered_names: &Vec, + build_name_values: &HashMap, + makes_an_impossible_request: bool, + ) -> BuildKey { + let build_id = spec.ident(); + let empty = OptionMap::default(); + let name_values = match build_name_values.get(build_id) { + Some(nv) => nv, + None => &empty, + }; + BuildKey::new( + spec.ident(), + ordered_names, + name_values, + makes_an_impossible_request, + ) + } + + /// Sorts builds by keys based on ordered build option names and + /// differing values in those options + async fn sort_by_build_option_values( + &mut self, + builds_with_impossible_requests: HashMap, + ) { + let start = Instant::now(); + + let (key_entry_names, build_name_values) = BuildToSortedOptName::sort_builds( + self.builds + .iter() + .flat_map(|hm| hm.values().map(|(spec, _src)| spec)), + ); // Sort the builds by their generated keys generated from the // ordered names and values worth including. self.builds.make_contiguous().sort_by_cached_key(|hm| { // Pull an arbitrary spec out from the hashmap let spec = &hm.iter().next().expect("non-empty hashmap").1.0; - SortedBuildIterator::make_option_values_build_key( + // Reverse the sort to get the build with the highest + // "numbers" in the earlier parts of its key to come first, + // which also reverse sorts the text values, i.e. "on" will + // come before "off". + std::cmp::Reverse(SortedBuildIterator::make_option_values_build_key( spec, - &ordered_names, + &key_entry_names, &build_name_values, builds_with_impossible_requests.contains_key(&spec.ident().clone()), - ) + )) }); - // Reverse the sort to get the build with the highest - // "numbers" in the earlier parts of its key to come first, - // which also reverse sorts the text values, i.e. "on" will - // come before "off". - self.builds.make_contiguous().reverse(); - let duration: Duration = start.elapsed(); tracing::info!( target: BUILD_SORT_TARGET, @@ -641,7 +654,7 @@ impl SortedBuildIterator { tracing::debug!( target: BUILD_SORT_TARGET, "Keys by build option values: built from: [{}]", - ordered_names + key_entry_names .iter() .map(|n| n.as_str()) .collect::>() @@ -659,7 +672,7 @@ impl SortedBuildIterator { spec.ident(), SortedBuildIterator::make_option_values_build_key( spec, - &ordered_names, + &key_entry_names, &build_name_values, builds_with_impossible_requests.contains_key(&spec.ident().clone()), ), diff --git a/crates/spk-solve/crates/solution/src/solution.rs b/crates/spk-solve/crates/solution/src/solution.rs index d0ca5958e3..49bd1cbbcb 100644 --- a/crates/spk-solve/crates/solution/src/solution.rs +++ b/crates/spk-solve/crates/solution/src/solution.rs @@ -112,7 +112,7 @@ impl PartialOrd for PackageSource { } /// Represents a package request that has been resolved. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct SolvedRequest { pub request: PkgRequest, pub spec: Arc, @@ -255,6 +255,28 @@ impl SolvedRequest { } } +impl std::fmt::Debug for SolvedRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SolvedRequest") + .field("request", &self.request.to_string()) + .field("spec", &format!("{}", self.spec.ident())) + .field( + "source", + &match &self.source { + PackageSource::Repository { repo, .. } => { + format!("Repository={}", repo.name()) + } + PackageSource::BuildFromSource { recipe } => { + format!("BuildFromSource={}", recipe.ident()) + } + PackageSource::Embedded { parent, .. } => format!("Embedded={parent}"), + PackageSource::SpkInternalTest => "SpkInternalTest".to_string(), + }, + ) + .finish() + } +} + /// A pairing of a solved request and a list of the components (names) /// it provides. pub struct LayerPackageAndComponents<'a>(pub &'a SolvedRequest, pub Vec); diff --git a/crates/spk-solve/crates/validation/src/validation_test.rs b/crates/spk-solve/crates/validation/src/validation_test.rs index 25aec0322a..73c27e15f7 100644 --- a/crates/spk-solve/crates/validation/src/validation_test.rs +++ b/crates/spk-solve/crates/validation/src/validation_test.rs @@ -103,11 +103,11 @@ fn test_qualified_var_supersedes_unqualified() { vec![ Request::from_yaml("{var: debug/off}") .unwrap() - .into_var() + .var() .unwrap(), Request::from_yaml("{var: my-package.debug/on}") .unwrap() - .into_var() + .var() .unwrap(), ], vec![], diff --git a/crates/spk-solve/src/error.rs b/crates/spk-solve/src/error.rs index d4545a954f..47270af9d1 100644 --- a/crates/spk-solve/src/error.rs +++ b/crates/spk-solve/src/error.rs @@ -65,6 +65,8 @@ pub enum Error { SolverLogFileIOError(#[source] std::io::Error, PathBuf), #[error("Error: Flushing solver log file: {0}")] SolverLogFileFlushError(#[source] std::io::Error), + #[error("Failed to resolve: {0}")] + FailedToResolve(String), } impl From for Error { diff --git a/crates/spk-solve/src/io.rs b/crates/spk-solve/src/io.rs index e13620e628..1c95495d49 100644 --- a/crates/spk-solve/src/io.rs +++ b/crates/spk-solve/src/io.rs @@ -4,7 +4,7 @@ use std::cmp::max; use std::collections::VecDeque; -use std::fmt::{Display, Write}; +use std::fmt::Write; use std::fs::{File, OpenOptions}; use std::io::{BufWriter, ErrorKind, Write as IOWrite}; use std::path::{Path, PathBuf}; @@ -40,8 +40,9 @@ use spk_solve_graph::{ State, }; -use crate::solvers::{ErrorFreq, StepSolver, StepSolverRuntime}; -use crate::{Error, ResolverCallback, Result, Solution, StatusLine, show_search_space_stats}; +use crate::solvers::step::ErrorFreq; +use crate::solvers::{StepSolver, StepSolverRuntime}; +use crate::{Error, Result, Solution, Solver, StatusLine, show_search_space_stats}; #[cfg(feature = "statsd")] use crate::{ SPK_SOLUTION_PACKAGE_COUNT_METRIC, @@ -58,6 +59,7 @@ const BY_USER: &str = "by user"; const CLI_SOLVER: &str = "cli"; const IMPOSSIBLE_CHECKS_SOLVER: &str = "checks"; const ALL_SOLVERS: &str = "all"; +const RESOLVO_SOLVER: &str = "resolvo"; const UNABLE_TO_GET_OUTPUT_FILE_LOCK: &str = "Unable to get lock to write solver output to file"; const UNABLE_TO_WRITE_OUTPUT_MESSAGE: &str = "Unable to write solver output message to file"; @@ -1001,7 +1003,7 @@ impl OutputKind { } } -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub(crate) struct DecisionFormatterSettings { pub(crate) verbosity: u8, pub(crate) report_time: bool, @@ -1032,13 +1034,22 @@ enum LoopOutcome { Success, } -#[derive(PartialEq, Eq, Clone, Debug)] +#[derive(PartialEq, Eq, Clone, Debug, Default, strum::Display)] pub enum MultiSolverKind { + #[strum(to_string = "Unchanged")] Unchanged, + #[strum(to_string = "All Impossible Checks")] AllImpossibleChecks, // This isn't a solver on its own. It indicates: the run all the // solvers in parallel but show the output from the unchanged one. + // This runs all the solvers implemented in the original solver. At least + // for now, it is not possible to run both the original solver and the + // new solver in parallel. + #[default] + #[strum(to_string = "All")] All, + #[strum(to_string = "Resolvo")] + Resolvo, } impl MultiSolverKind { @@ -1053,6 +1064,7 @@ impl MultiSolverKind { MultiSolverKind::Unchanged => CLI_SOLVER, MultiSolverKind::AllImpossibleChecks => IMPOSSIBLE_CHECKS_SOLVER, MultiSolverKind::All => ALL_SOLVERS, + MultiSolverKind::Resolvo => RESOLVO_SOLVER, } } @@ -1080,17 +1092,6 @@ impl MultiSolverKind { } } -impl Display for MultiSolverKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let name = match self { - MultiSolverKind::Unchanged => "Unchanged", - MultiSolverKind::AllImpossibleChecks => "All Impossible Checks", - MultiSolverKind::All => "All", - }; - write!(f, "{name}") - } -} - struct SolverTaskSettings { solver: StepSolver, solver_kind: MultiSolverKind, @@ -1114,7 +1115,7 @@ struct SolverResult { pub(crate) result: Result<(Solution, Arc>)>, } -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct DecisionFormatter { pub(crate) settings: DecisionFormatterSettings, } @@ -1152,7 +1153,7 @@ impl DecisionFormatter { /// appropriate. This runs two solvers in parallel (one based on /// the given solver, one with additional options) and takes the /// result from the first to finish. - pub async fn run_and_print_resolve( + pub(crate) async fn run_and_print_resolve( &self, solver: &StepSolver, ) -> Result<(Solution, Arc>)> { @@ -1185,7 +1186,7 @@ impl DecisionFormatter { /// info-level event as appropriate. This runs two solvers in /// parallel (one based on the given solver, one with additional /// options) and takes the result from the first to finish. - pub async fn run_and_log_resolve( + pub(crate) async fn run_and_log_resolve( &self, solver: &StepSolver, ) -> Result<(Solution, Arc>)> { @@ -1193,27 +1194,6 @@ impl DecisionFormatter { self.run_multi_solve(solvers, OutputKind::Tracing).await } - /// Run the solver runtime to completion, logging each step as a - /// tracing info-level event as appropriate. This does not run - /// multiple solver and won't benefit from running solvers in - /// parallel. - pub async fn run_and_log_decisions( - &self, - runtime: &mut StepSolverRuntime, - ) -> Result<(Solution, Arc>)> { - // Note: this is not currently used directly. We may be able - // to remove this method. - let start = Instant::now(); - let loop_outcome = self.run_solver_loop(runtime, OutputKind::Tracing).await; - let solve_time = start.elapsed(); - - #[cfg(feature = "statsd")] - self.send_solver_end_metrics(solve_time); - - self.check_and_output_solver_results(loop_outcome, solve_time, runtime, OutputKind::Tracing) - .await - } - fn setup_solvers(&self, base_solver: &StepSolver) -> Vec { // Leave the first solver as is. let solver_with_no_change = base_solver.clone(); @@ -1247,6 +1227,7 @@ impl DecisionFormatter { ignore_failure: false, }, ]), + MultiSolverKind::Resolvo => unreachable!(), } } @@ -2115,23 +2096,3 @@ impl DecisionFormatter { out } } - -#[async_trait::async_trait] -impl ResolverCallback for &DecisionFormatter { - async fn solve<'s, 'a: 's>( - &'s self, - r: &'a StepSolver, - ) -> Result<(Solution, Arc>)> { - self.run_and_print_resolve(r).await - } -} - -#[async_trait::async_trait] -impl ResolverCallback for DecisionFormatter { - async fn solve<'s, 'a: 's>( - &'s self, - r: &'a StepSolver, - ) -> Result<(Solution, Arc>)> { - self.run_and_print_resolve(r).await - } -} diff --git a/crates/spk-solve/src/lib.rs b/crates/spk-solve/src/lib.rs index aadd40d278..933a5b3865 100644 --- a/crates/spk-solve/src/lib.rs +++ b/crates/spk-solve/src/lib.rs @@ -7,13 +7,11 @@ mod io; #[cfg(feature = "statsd")] mod metrics; mod search_space; +mod solver; mod solvers; mod status_line; -use std::sync::Arc; - pub use error::{Error, Result}; -use graph::Graph; pub use io::{ DEFAULT_SOLVER_RUN_FILE_PREFIX, DecisionFormatter, @@ -34,6 +32,9 @@ pub use metrics::{ get_metrics_client, }; pub(crate) use search_space::show_search_space_stats; +pub use solver::{Solver, SolverExt, SolverImpl, SolverMut}; +// Publicly exported ResolvoSolver to stop dead code warnings +pub use solvers::ResolvoSolver; pub use solvers::{StepSolver, StepSolverRuntime}; pub use spk_schema::foundation::ident_build::Build; pub use spk_schema::foundation::ident_component::Component; @@ -59,33 +60,3 @@ pub use { spk_solve_solution as solution, spk_solve_validation as validation, }; - -#[async_trait::async_trait] -pub trait ResolverCallback: Send + Sync { - /// Run a solve using the given [`crate::StepSolver`], - /// producing a [`crate::Solution`]. - async fn solve<'s, 'a: 's>( - &'s self, - r: &'a StepSolver, - ) -> Result<(Solution, Arc>)>; -} - -/// A no-frills implementation of [`ResolverCallback`]. -pub struct DefaultResolver {} - -#[async_trait::async_trait] -impl ResolverCallback for DefaultResolver { - async fn solve<'s, 'a: 's>( - &'s self, - r: &'a StepSolver, - ) -> Result<(Solution, Arc>)> { - let mut runtime = r.run(); - let solution = runtime.solution().await; - match solution { - Err(err) => Err(err), - Ok(s) => Ok((s, runtime.graph())), - } - } -} - -pub type BoxedResolverCallback<'a> = Box; diff --git a/crates/spk-solve/src/solver.rs b/crates/spk-solve/src/solver.rs new file mode 100644 index 0000000000..c03956df73 --- /dev/null +++ b/crates/spk-solve/src/solver.rs @@ -0,0 +1,203 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + +use std::borrow::Cow; +use std::sync::Arc; + +use enum_dispatch::enum_dispatch; +use spk_schema::ident::{PkgRequest, VarRequest}; +use spk_schema::{OptionMap, Recipe, Request}; +use spk_solve_solution::Solution; +use spk_storage::RepositoryHandle; +use variantly::Variantly; + +use crate::{DecisionFormatter, Result}; + +#[enum_dispatch(Solver, SolverExt, SolverMut)] +// Don't derive Default. If some code is generic on Solver and is given one of +// these, if it wants a "default" solver it needs to be given a new solver of +// the same variety and `SolverImpl::default()` can't do that. +#[derive(Clone, Variantly)] +pub enum SolverImpl { + Step(crate::StepSolver), + Resolvo(crate::solvers::ResolvoSolver), +} + +#[async_trait::async_trait] +#[enum_dispatch] +pub trait Solver { + /// Return the options that the solver is currently configured with. + /// + /// These are the options that have been set via + /// [`SolverMut::update_options`]. + fn get_options(&self) -> Cow<'_, OptionMap>; + + /// Return the PkgRequests added to the solver. + fn get_pkg_requests(&self) -> Vec; + + /// Return the VarRequests added to the solver. + fn get_var_requests(&self) -> Vec; + + /// Return a reference to the solver's list of repositories. + fn repositories(&self) -> &[Arc]; +} + +#[async_trait::async_trait] +#[enum_dispatch] +pub trait SolverMut: Solver { + /// Add a request to this solver. + fn add_request(&mut self, request: Request); + + /// Adds requests for all build requirements of the given recipe. + fn configure_for_build_environment(&mut self, recipe: &T) -> Result<()> { + let options = self.get_options(); + + let build_options = recipe.resolve_options(&*options)?; + for req in recipe + .get_build_requirements(&build_options)? + .iter() + .cloned() + { + self.add_request(req) + } + + Ok(()) + } + + /// Put this solver back into its default state + fn reset(&mut self); + + /// Run the solver as configured using the given formatter. + /// + /// "log" means that solver progress is output via tracing, as + /// configured by the formatter. + /// + /// The solution may also be printed, if found, as configured by the + /// formatter. + /// + /// Not all formatter settings may be supported by every solver. + async fn run_and_log_resolve(&mut self, formatter: &DecisionFormatter) -> Result; + + /// Run the solver as configured using the given formatter. + /// + /// "print" means that solver progress is printed to the console, as + /// configured by the formatter. + /// + /// The solution may also be printed, if found, as configured by the + /// formatter. + /// + /// Not all formatter settings may be supported by every solver. + async fn run_and_print_resolve(&mut self, formatter: &DecisionFormatter) -> Result; + + /// If true, only solve pre-built binary packages. + /// + /// When false, the solver may return packages where the build is not set. + /// These packages are known to have a source package available, and the requested + /// options are valid for a new build of that source package. + /// These packages are not actually built as part of the solver process but their + /// build environments are fully resolved and dependencies included + fn set_binary_only(&mut self, binary_only: bool); + + /// Run the solver as configured. + async fn solve(&mut self) -> Result; + + fn update_options(&mut self, options: OptionMap); +} + +impl Solver for &T +where + T: Solver, +{ + fn get_options(&self) -> Cow<'_, OptionMap> { + T::get_options(self) + } + + fn get_pkg_requests(&self) -> Vec { + T::get_pkg_requests(self) + } + + fn get_var_requests(&self) -> Vec { + T::get_var_requests(self) + } + + fn repositories(&self) -> &[Arc] { + T::repositories(self) + } +} + +impl Solver for &mut T +where + T: Solver, +{ + fn get_options(&self) -> Cow<'_, OptionMap> { + T::get_options(self) + } + + fn get_pkg_requests(&self) -> Vec { + T::get_pkg_requests(self) + } + + fn get_var_requests(&self) -> Vec { + T::get_var_requests(self) + } + + fn repositories(&self) -> &[Arc] { + T::repositories(self) + } +} + +#[async_trait::async_trait] +impl SolverMut for &mut T +where + T: SolverMut + Send + Sync, +{ + fn add_request(&mut self, request: Request) { + T::add_request(self, request) + } + + fn reset(&mut self) { + T::reset(self) + } + + async fn run_and_log_resolve(&mut self, formatter: &DecisionFormatter) -> Result { + T::run_and_log_resolve(self, formatter).await + } + + async fn run_and_print_resolve(&mut self, formatter: &DecisionFormatter) -> Result { + T::run_and_print_resolve(self, formatter).await + } + + fn set_binary_only(&mut self, binary_only: bool) { + T::set_binary_only(self, binary_only) + } + + async fn solve(&mut self) -> Result { + T::solve(self).await + } + + fn update_options(&mut self, options: OptionMap) { + T::update_options(self, options) + } +} + +#[async_trait::async_trait] +#[enum_dispatch] +pub trait SolverExt: Solver { + /// Add a repository where the solver can get packages. + fn add_repository(&mut self, repo: R) + where + R: Into>; +} + +impl SolverExt for &mut T +where + T: SolverExt + Sync, +{ + fn add_repository(&mut self, repo: R) + where + R: Into>, + { + T::add_repository(self, repo); + } +} diff --git a/crates/spk-solve/src/solvers/mod.rs b/crates/spk-solve/src/solvers/mod.rs index 3cd8193fa7..50d9b83b34 100644 --- a/crates/spk-solve/src/solvers/mod.rs +++ b/crates/spk-solve/src/solvers/mod.rs @@ -4,9 +4,11 @@ //! Spk package solver implementations. +pub(crate) mod resolvo; pub(crate) mod step; -pub use step::{ErrorFreq, Solver as StepSolver, SolverRuntime as StepSolverRuntime}; +pub use resolvo::Solver as ResolvoSolver; +pub use step::{Solver as StepSolver, SolverRuntime as StepSolverRuntime}; // Public to allow other tests to use its macros #[cfg(test)] diff --git a/crates/spk-solve/src/solvers/resolvo/mod.rs b/crates/spk-solve/src/solvers/resolvo/mod.rs new file mode 100644 index 0000000000..a8276561bf --- /dev/null +++ b/crates/spk-solve/src/solvers/resolvo/mod.rs @@ -0,0 +1,397 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + +//! A CDCL SAT solver for Spk. +//! +//! This solver uses [Resolvo](https://github.com/prefix-dev/resolvo) and is +//! able to handle more complex problems than the original Spk solver. However +//! the tradeoff is that it requires reading all the package metadata up front +//! so it can be slower than the original solver for small cases. +//! +//! When there is no solution, Resolvo provides a useful error message to help +//! explain the problem whereas the original solver requires reading the solver +//! log to deduce the real cause of the failure. + +mod pkg_request_version_set; +mod spk_provider; + +use std::borrow::Cow; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::sync::Arc; + +use pkg_request_version_set::{SpkSolvable, SyntheticComponent}; +use spk_provider::SpkProvider; +use spk_schema::ident::{ + InclusionPolicy, + LocatedBuildIdent, + PinPolicy, + PkgRequest, + RangeIdent, + VarRequest, +}; +use spk_schema::ident_component::Component; +use spk_schema::prelude::{HasVersion, Named, Versioned}; +use spk_schema::version_range::VersionFilter; +use spk_schema::{OptionMap, Package, Request}; +use spk_solve_solution::{PackageSource, Solution}; +use spk_solve_validation::{Validators, default_validators}; +use spk_storage::RepositoryHandle; + +use crate::solver::Solver as SolverTrait; +use crate::{DecisionFormatter, Error, Result, SolverExt, SolverMut}; + +#[cfg(test)] +#[path = "resolvo_tests.rs"] +mod resolvo_tests; + +#[derive(Clone, Default)] +pub struct Solver { + repos: Vec>, + requests: Vec, + options: OptionMap, + binary_only: bool, + _validators: Cow<'static, [Validators]>, + build_from_source_trail: HashSet, +} + +impl Solver { + pub fn new(repos: Vec>, validators: Cow<'static, [Validators]>) -> Self { + Self { + repos, + requests: Vec::new(), + options: Default::default(), + binary_only: true, + _validators: validators, + build_from_source_trail: HashSet::new(), + } + } + + pub(crate) fn set_build_from_source_trail(&mut self, trail: HashSet) { + self.build_from_source_trail = trail; + } + + pub async fn solve(&self) -> Result { + let repos = self.repos.clone(); + let requests = self.requests.clone(); + let options = self.options.clone(); + let binary_only = self.binary_only; + let build_from_source_trail = self.build_from_source_trail.clone(); + // Use a blocking thread so resolvo can call `block_on` on the runtime. + let solvables = tokio::task::spawn_blocking(move || { + let mut provider = Some(SpkProvider::new( + repos.clone(), + binary_only, + build_from_source_trail, + )); + let mut loop_counter = 0; + let (solver, solved) = loop { + loop_counter += 1; + let mut this_iter_provider = provider.take().expect("provider is always Some"); + let pkg_requirements = this_iter_provider.root_pkg_requirements(&requests); + let mut var_requirements = this_iter_provider.var_requirements(&requests); + // XXX: Not sure if this will result in the desired precedence + // when options and var requests for the same thing exist. + var_requirements + .extend(this_iter_provider.var_requirements_from_options(options.clone())); + let mut solver = resolvo::Solver::new(this_iter_provider) + .with_runtime(tokio::runtime::Handle::current()); + let problem = resolvo::Problem::new() + .requirements(pkg_requirements) + .constraints(var_requirements); + match solver.solve(problem) { + Ok(solved) => break (solver, solved), + Err(resolvo::UnsolvableOrCancelled::Cancelled(msg)) => { + let msg = msg.downcast_ref::(); + provider = Some(solver.provider().reset()); + tracing::info!( + "Solver retry {loop_counter}: {msg:?}", + msg = msg.map_or("unknown", |v| v) + ); + continue; + } + Err(resolvo::UnsolvableOrCancelled::Unsolvable(conflict)) => { + // Edge case: a need to retry was detected but the + // solver arrived at a decision before it noticed it + // needs to cancel (unknown if this ever happens). + if solver.provider().is_canceled() { + provider = Some(solver.provider().reset()); + tracing::info!("Solver retry {loop_counter}"); + continue; + } + return Err(Error::FailedToResolve(format!( + "{}", + conflict.display_user_friendly(&solver) + ))); + } + } + }; + + let pool = &solver.provider().pool; + Ok(solved + .into_iter() + .filter_map(|solvable_id| { + let solvable = pool.resolve_solvable(solvable_id); + if let SpkSolvable::LocatedBuildIdentWithComponent( + located_build_ident_with_component, + ) = &solvable.record + { + Some(located_build_ident_with_component.clone()) + } else { + None + } + }) + .collect::>()) + }) + .await + .map_err(|err| Error::String(format!("Tokio panicked? {err}")))??; + + let mut solution_options = OptionMap::default(); + let mut solution_adds = Vec::with_capacity(solvables.len()); + // Keep track of the index of each package added to `solution_adds` in + // order to merge components. Components of a package come out of the + // solver as separate solvables. The solver logic guarantees that any + // two entries in this list with the same package name are for the same + // package and this merging of components is valid. + let mut seen_packages = HashMap::new(); + for located_build_ident_with_component in solvables { + let SyntheticComponent::Actual(solvable_component) = + &located_build_ident_with_component.component + else { + continue; + }; + + if let Some(existing_index) = + seen_packages.get(located_build_ident_with_component.ident.name()) + { + if let Some(( + PkgRequest { + pkg: RangeIdent { components, .. }, + .. + }, + _, + _, + )) = solution_adds.get_mut(*existing_index) + { + // If we visit a solvable for the "All" component, the + // solver guarantees that we will have all the components. + if !components.contains(&Component::All) { + components.insert(solvable_component.clone()); + } else if solvable_component.is_all() { + *components = BTreeSet::from([Component::All]); + } + } + continue; + } + + let pkg_request = PkgRequest { + pkg: RangeIdent { + repository_name: None, + name: located_build_ident_with_component.ident.name().to_owned(), + components: BTreeSet::from_iter([solvable_component.clone()]), + version: VersionFilter::default(), + build: None, + }, + prerelease_policy: None, + inclusion_policy: InclusionPolicy::default(), + pin: None, + pin_policy: PinPolicy::default(), + required_compat: None, + requested_by: BTreeMap::new(), + }; + let repo = self + .repos + .iter() + .find(|repo| { + repo.name() == located_build_ident_with_component.ident.repository_name() + }) + .expect("Expected solved package's repository to be in the list of repositories"); + let package = repo + .read_package(located_build_ident_with_component.ident.target()) + .await?; + let rendered_version = package.compat().render(package.version()); + solution_options.insert(package.name().as_opt_name().to_owned(), rendered_version); + for option in package.get_build_options() { + match option { + spk_schema::Opt::Pkg(pkg_opt) => { + if let Some(value) = pkg_opt.get_value(None) { + solution_options.insert( + format!("{}.{}", package.name(), pkg_opt.pkg).try_into().expect("Two packages names separated by a period is a valid option name"), + value, + ); + } + } + spk_schema::Opt::Var(var_opt) => { + if let Some(value) = var_opt.get_value(None) { + if var_opt.var.namespace().is_none() { + solution_options.insert( + format!("{}.{}", package.name(), var_opt.var).try_into().expect("A package name, a period, and a non-namespaced option name is a valid option name"), + value, + ); + } else { + solution_options.insert(var_opt.var.clone(), value); + } + } + } + } + } + let next_index = solution_adds.len(); + seen_packages.insert( + located_build_ident_with_component.ident.name().to_owned(), + next_index, + ); + solution_adds.push((pkg_request, package, { + match located_build_ident_with_component.ident.build() { + spk_schema::ident_build::Build::Source + if located_build_ident_with_component.requires_build_from_source => + { + PackageSource::BuildFromSource { + recipe: repo + .read_recipe( + &located_build_ident_with_component.ident.to_version_ident(), + ) + .await?, + } + } + spk_schema::ident_build::Build::Source => { + // Not building this from source but just adding the + // source build to the Solution. + PackageSource::Repository { + repo: Arc::clone(repo), + // XXX: Why is this needed? + components: repo + .read_components(located_build_ident_with_component.ident.target()) + .await?, + } + } + spk_schema::ident_build::Build::Embedded(embedded_source) => { + match embedded_source { + spk_schema::ident_build::EmbeddedSource::Package( + embedded_source_package, + ) => { + PackageSource::Embedded { + parent: (**embedded_source_package).clone().try_into()?, + // XXX: Why is this needed? + components: repo + .read_components( + located_build_ident_with_component.ident.target(), + ) + .await? + .keys() + .cloned() + .collect(), + } + } + spk_schema::ident_build::EmbeddedSource::Unknown => todo!(), + } + } + spk_schema::ident_build::Build::BuildId(_build_id) => { + PackageSource::Repository { + repo: Arc::clone(repo), + // XXX: Why is this needed? + components: repo + .read_components(located_build_ident_with_component.ident.target()) + .await?, + } + } + } + })); + } + let mut solution = Solution::new(solution_options); + for (pkg_request, package, source) in solution_adds { + solution.add(pkg_request, package, source); + } + Ok(solution) + } +} + +impl SolverTrait for Solver { + fn get_options(&self) -> Cow<'_, OptionMap> { + Cow::Borrowed(&self.options) + } + + fn get_pkg_requests(&self) -> Vec { + self.requests + .iter() + .filter_map(|r| r.pkg_ref()) + .cloned() + .collect() + } + + fn get_var_requests(&self) -> Vec { + self.requests + .iter() + .filter_map(|r| r.var_ref()) + .cloned() + .collect() + } + + fn repositories(&self) -> &[Arc] { + &self.repos + } +} + +#[async_trait::async_trait] +impl SolverMut for Solver { + fn add_request(&mut self, mut request: Request) { + if let Request::Pkg(request) = &mut request { + if request.pkg.components.is_empty() { + if request.pkg.is_source() { + request.pkg.components.insert(Component::Source); + } else { + request.pkg.components.insert(Component::default_for_run()); + } + } + } + self.requests.push(request); + } + + fn reset(&mut self) { + self.repos.truncate(0); + self.requests.truncate(0); + self._validators = Cow::from(default_validators()); + } + + async fn run_and_log_resolve(&mut self, formatter: &DecisionFormatter) -> Result { + // This solver doesn't currently support tracing. + self.run_and_print_resolve(formatter).await + } + + async fn run_and_print_resolve(&mut self, formatter: &DecisionFormatter) -> Result { + let solution = self.solve().await?; + let output = solution + .format_solution_with_highest_versions( + formatter.settings.verbosity, + self.repositories(), + // the order coming out of resolvo is ... random? + true, + ) + .await?; + if formatter.settings.show_solution { + println!("{output}"); + } + Ok(solution) + } + + fn set_binary_only(&mut self, binary_only: bool) { + self.binary_only = binary_only; + } + + async fn solve(&mut self) -> Result { + Solver::solve(self).await + } + + fn update_options(&mut self, options: OptionMap) { + self.options.extend(options); + } +} + +#[async_trait::async_trait] +impl SolverExt for Solver { + fn add_repository(&mut self, repo: R) + where + R: Into>, + { + self.repos.push(repo.into()); + } +} diff --git a/crates/spk-solve/src/solvers/resolvo/pkg_request_version_set.rs b/crates/spk-solve/src/solvers/resolvo/pkg_request_version_set.rs new file mode 100644 index 0000000000..3bed5c4c79 --- /dev/null +++ b/crates/spk-solve/src/solvers/resolvo/pkg_request_version_set.rs @@ -0,0 +1,194 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + +use std::sync::Arc; + +use resolvo::utils::VersionSet; +use spk_schema::Request; +use spk_schema::ident::{LocatedBuildIdent, PkgRequest, PreReleasePolicy, RangeIdent, RequestedBy}; +use spk_schema::ident_component::Component; +use spk_schema::name::OptNameBuf; + +/// This allows for storing strings of different types but hash and compare by +/// the underlying strings. +#[derive(Clone, Debug)] +pub(crate) enum VarValue { + ArcStr(Arc), + Owned(String), +} + +impl VarValue { + #[inline] + fn as_str(&self) -> &str { + match self { + VarValue::ArcStr(a) => a, + VarValue::Owned(a) => a.as_str(), + } + } +} + +impl std::hash::Hash for VarValue { + fn hash(&self, state: &mut H) { + self.as_str().hash(state) + } +} + +impl Eq for VarValue {} + +impl Ord for VarValue { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl PartialOrd for VarValue { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for VarValue { + fn eq(&self, other: &Self) -> bool { + self.as_str() == other.as_str() + } +} + +impl PartialEq> for VarValue { + fn eq(&self, other: &Arc) -> bool { + self.as_str() == &**other + } +} + +impl PartialEq for Arc { + fn eq(&self, other: &VarValue) -> bool { + other.as_str() == &**self + } +} + +impl std::fmt::Display for VarValue { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.as_str().fmt(f) + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) enum RequestVS { + SpkRequest(Request), + GlobalVar { key: OptNameBuf, value: VarValue }, +} + +impl std::fmt::Display for RequestVS { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + RequestVS::SpkRequest(req) => write!(f, "{req}"), + RequestVS::GlobalVar { key, value } => write!(f, "GlobalVar({key}={value})"), + } + } +} + +/// The component portion of a package that can exist in a solution. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) enum SyntheticComponent { + /// This represents the "parent" of any components of a package, used to + /// prevent components from different versions of a package from getting + /// into the same solution. + Base, + /// Real components. + Actual(Component), +} + +impl SyntheticComponent { + #[inline] + pub(crate) fn is_all(&self) -> bool { + matches!(self, SyntheticComponent::Actual(Component::All)) + } +} + +// The `requires_build_from_source` field is ignored for hashing and equality +// purposes. +#[derive(Clone, Debug)] +pub(crate) struct LocatedBuildIdentWithComponent { + pub(crate) ident: LocatedBuildIdent, + pub(crate) component: SyntheticComponent, + pub(crate) requires_build_from_source: bool, +} + +impl std::hash::Hash for LocatedBuildIdentWithComponent { + fn hash(&self, state: &mut H) { + self.ident.hash(state); + self.component.hash(state); + } +} + +impl Eq for LocatedBuildIdentWithComponent {} + +impl PartialEq for LocatedBuildIdentWithComponent { + fn eq(&self, other: &Self) -> bool { + self.ident == other.ident && self.component == other.component + } +} + +impl LocatedBuildIdentWithComponent { + /// Create a request that will match this ident but with a different + /// component name. + pub(crate) fn as_request_with_components( + &self, + components: impl IntoIterator, + ) -> Request { + let mut range_ident = RangeIdent::double_equals(&self.ident.to_any_ident(), components); + range_ident.repository_name = Some(self.ident.repository_name().to_owned()); + + let mut pkg_request = PkgRequest::new( + range_ident, + RequestedBy::BinaryBuild(self.ident.target().clone()), + ); + // Since we're using double_equals, is it safe to always enable + // prereleases? If self represents a prerelease, then the Request + // needs to allow it. + pkg_request.prerelease_policy = Some(PreReleasePolicy::IncludeAll); + + Request::Pkg(pkg_request) + } +} + +impl PartialEq for Component { + fn eq(&self, other: &SyntheticComponent) -> bool { + match other { + SyntheticComponent::Base => false, + SyntheticComponent::Actual(other) => self == other, + } + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) enum SpkSolvable { + LocatedBuildIdentWithComponent(LocatedBuildIdentWithComponent), + GlobalVar { key: OptNameBuf, value: VarValue }, +} + +impl std::fmt::Display for SpkSolvable { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + SpkSolvable::LocatedBuildIdentWithComponent(located_build_ident_with_component) => { + write!(f, "{located_build_ident_with_component}") + } + SpkSolvable::GlobalVar { key, value } => write!(f, "GlobalVar({key}={value})"), + } + } +} + +impl VersionSet for RequestVS { + type V = SpkSolvable; +} + +impl std::fmt::Display for LocatedBuildIdentWithComponent { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &self.component { + SyntheticComponent::Base => self.ident.fmt(f), + SyntheticComponent::Actual(component) => { + write!(f, "{}:{component}", self.ident) + } + } + } +} diff --git a/crates/spk-solve/src/solvers/resolvo/resolvo_tests.rs b/crates/spk-solve/src/solvers/resolvo/resolvo_tests.rs new file mode 100644 index 0000000000..d8768d9b07 --- /dev/null +++ b/crates/spk-solve/src/solvers/resolvo/resolvo_tests.rs @@ -0,0 +1,224 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + +use std::borrow::Cow; + +use rstest::rstest; +use spk_schema::prelude::HasVersion; +use spk_schema::{Package, opt_name}; +use spk_solve_macros::{make_repo, request}; +use tap::TapFallible; + +use super::Solver; +use crate::SolverMut; + +#[rstest] +#[tokio::test] +async fn basic() { + let repo = make_repo!( + [ + {"pkg": "basic/1.0.0"}, + ] + ); + + let mut solver = Solver::new(vec![repo.into()], Cow::Borrowed(&[])); + solver.add_request(request!("basic")); + let solution = solver.solve().await.unwrap(); + assert_eq!(solution.len(), 1); +} + +#[rstest] +#[tokio::test] +async fn two_choices() { + let repo = make_repo!( + [ + {"pkg": "basic/2.0.0"}, + {"pkg": "basic/1.0.0"}, + ] + ); + + let mut solver = Solver::new(vec![repo.into()], Cow::Borrowed(&[])); + solver.add_request(request!("basic")); + let solution = solver.solve().await.unwrap(); + assert_eq!(solution.len(), 1); + // All things being equal it should pick the higher version + assert_eq!( + solution.items().next().unwrap().spec.version().to_string(), + "2.0.0" + ); +} + +#[rstest] +#[tokio::test] +async fn two_choices_request_lower() { + let repo = make_repo!( + [ + {"pkg": "basic/2.0.0"}, + {"pkg": "basic/1.0.0"}, + ] + ); + + let mut solver = Solver::new(vec![repo.into()], Cow::Borrowed(&[])); + solver.add_request(request!("basic/1.0.0")); + let solution = solver.solve().await.unwrap(); + assert_eq!(solution.len(), 1); + assert_eq!( + solution.items().next().unwrap().spec.version().to_string(), + "1.0.0" + ); +} + +#[rstest] +#[tokio::test] +async fn two_choices_request_missing() { + let repo = make_repo!( + [ + {"pkg": "basic/3.0.0"}, + {"pkg": "basic/2.0.0"}, + ] + ); + + let mut solver = Solver::new(vec![repo.into()], Cow::Borrowed(&[])); + solver.add_request(request!("basic/1.0.0")); + let _solution = solver.solve().await.expect_err("Nothing satisfies 1.0.0"); +} + +#[rstest] +#[tokio::test] +async fn package_with_dependency() { + let repo = make_repo!( + [ + {"pkg": "dep/1.0.0"}, + {"pkg": "needs-dep/1.0.0", + "install": { + "requirements": [ + {"pkg": "dep"} + ] + } + }, + ] + ); + + let mut solver = Solver::new(vec![repo.into()], Cow::Borrowed(&[])); + solver.add_request(request!("needs-dep/1.0.0")); + let solution = solver.solve().await.tap_err(|e| eprintln!("{e}")).unwrap(); + assert_eq!(solution.len(), 2); +} + +#[rstest] +#[case::expect_blue("dep.color/blue", "blue")] +#[case::expect_red("dep.color/red", "red")] +#[should_panic] +#[case::expect_green("dep.color/green", "green")] +#[tokio::test] +async fn package_with_dependency_on_variant( + #[case] color_spec: &str, + #[case] expected_color: &str, +) { + let repo = make_repo!( + [ + {"pkg": "dep/1.0.0", + "build": { + "options": [ + {"var": "color/blue"} + ] + } + }, + {"pkg": "dep/1.0.0", + "build": { + "options": [ + {"var": "color/red"} + ] + } + }, + {"pkg": "needs-dep/1.0.0", + "install": { + "requirements": [ + {"pkg": "dep"}, + {"var": color_spec}, + ] + } + }, + ] + ); + + let mut solver = Solver::new(vec![repo.into()], Cow::Borrowed(&[])); + solver.add_request(request!("needs-dep/1.0.0")); + let solution = solver.solve().await.unwrap(); + assert_eq!(solution.len(), 2); + let dep = solution.get("dep").unwrap(); + assert_eq!( + dep.spec.option_values().get(opt_name!("color")).unwrap(), + expected_color + ); +} + +#[rstest] +#[case::expect_blue("color/blue", "blue")] +#[case::expect_red("color/red", "red")] +#[should_panic] +#[case::expect_green("color/green", "green")] +#[tokio::test] +async fn global_vars(#[case] global_spec: &str, #[case] expected_color: &str) { + let repo = make_repo!( + [ + {"pkg": "dep/1.0.0", + "build": { + "options": [ + {"var": "color/blue"} + ] + } + }, + {"pkg": "dep/1.0.0", + "build": { + "options": [ + {"var": "color/red"} + ] + } + }, + {"pkg": "needs-dep/1.0.0", + "install": { + "requirements": [ + {"pkg": "dep"}, + {"var": global_spec}, + ] + } + }, + ] + ); + + let mut solver = Solver::new(vec![repo.into()], Cow::Borrowed(&[])); + solver.add_request(request!("needs-dep/1.0.0")); + let solution = solver.solve().await.unwrap(); + assert_eq!(solution.len(), 2); + let dep = solution.get("dep").unwrap(); + assert_eq!( + dep.spec.option_values().get(opt_name!("color")).unwrap(), + expected_color + ); +} + +#[rstest] +#[tokio::test] +async fn package_with_source_build() { + let repo = make_repo!( + [ + {"pkg": "dep/1.0.0/src"}, + {"pkg": "needs-dep/1.0.0", + "install": { + "requirements": [ + {"pkg": "dep"} + ] + } + }, + ] + ); + + let mut solver = Solver::new(vec![repo.into()], Cow::Borrowed(&[])); + solver.add_request(request!("needs-dep/1.0.0")); + solver + .solve() + .await + .expect_err("src build should not satisfy dependency"); +} diff --git a/crates/spk-solve/src/solvers/resolvo/spk_provider.rs b/crates/spk-solve/src/solvers/resolvo/spk_provider.rs new file mode 100644 index 0000000000..59ee4a7925 --- /dev/null +++ b/crates/spk-solve/src/solvers/resolvo/spk_provider.rs @@ -0,0 +1,1734 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::ops::Not; +use std::sync::Arc; + +use itertools::Itertools; +use resolvo::utils::Pool; +use resolvo::{ + Candidates, + Dependencies, + DependencyProvider, + Interner, + KnownDependencies, + NameId, + Requirement, + SolvableId, + SolverCache, + StringId, + VersionSetId, + VersionSetUnionId, +}; +use spk_schema::ident::{ + LocatedBuildIdent, + PinnableValue, + PkgRequest, + RangeIdent, + RequestedBy, + Satisfy, + VarRequest, +}; +use spk_schema::ident_build::{Build, EmbeddedSource, EmbeddedSourcePackage}; +use spk_schema::ident_component::Component; +use spk_schema::name::{OptNameBuf, PkgNameBuf}; +use spk_schema::prelude::{HasVersion, Named}; +use spk_schema::version_range::{DoubleEqualsVersion, Ranged, VersionFilter, parse_version_range}; +use spk_schema::{ + BuildIdent, + Deprecate, + Opt, + OptionMap, + Package, + Recipe, + Request, + Spec, + VersionIdent, +}; +use spk_solve_package_iterator::{BuildKey, BuildToSortedOptName, SortedBuildIterator}; +use spk_storage::RepositoryHandle; +use tracing::{Instrument, debug_span}; + +use super::pkg_request_version_set::{ + LocatedBuildIdentWithComponent, + RequestVS, + SpkSolvable, + SyntheticComponent, + VarValue, +}; +use crate::SolverMut; + +// Using just the package name as a Resolvo "package name" prevents multiple +// components from the same package from existing in the same solution, since +// we consider the different components to be different "solvables". Instead, +// treat different components of a package as separate packages. There needs to +// be a relationship between every component of a package and a "base" +// component, to prevent a solve containing a mix of components from different +// versions of the same package. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) struct PkgNameBufWithComponent { + pub(crate) name: PkgNameBuf, + pub(crate) component: SyntheticComponent, +} + +impl std::fmt::Display for PkgNameBufWithComponent { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &self.component { + SyntheticComponent::Base => write!(f, "{}", self.name), + SyntheticComponent::Actual(component) => write!(f, "{}:{component}", self.name), + } + } +} + +#[derive(Clone, Eq, Hash, PartialEq)] +pub(crate) enum ResolvoPackageName { + GlobalVar(OptNameBuf), + PkgNameBufWithComponent(PkgNameBufWithComponent), +} + +impl ResolvoPackageName { + async fn get_candidates(&self, name: NameId, provider: &SpkProvider) -> Option { + match self { + ResolvoPackageName::GlobalVar(key) => { + provider + .queried_global_var_values + .borrow_mut() + .insert(key.clone()); + + if let Some(values) = provider.known_global_var_values.borrow().get(key) { + let mut candidates = Candidates { + candidates: Vec::with_capacity(values.len()), + ..Default::default() + }; + for value in values { + let solvable_id = *provider + .interned_solvables + .borrow_mut() + .entry(SpkSolvable::GlobalVar { + key: key.clone(), + value: value.clone(), + }) + .or_insert_with(|| { + provider.pool.intern_solvable( + name, + SpkSolvable::GlobalVar { + key: key.clone(), + value: value.clone(), + }, + ) + }); + candidates.candidates.push(solvable_id); + } + return Some(candidates); + } + None + } + ResolvoPackageName::PkgNameBufWithComponent(pkg_name) => { + // Prevent duplicate solvables by using a set. Ties (same build + // but "requires_build_from_source" differs) are resolved with + // "requires_build_from_source" being true. + let mut located_builds = HashSet::new(); + + let root_pkg_request = provider.global_pkg_requests.get(&pkg_name.name); + + for repo in &provider.repos { + let versions = repo + .list_package_versions(&pkg_name.name) + .await + .unwrap_or_default(); + for version in versions.iter() { + // Skip versions known to be excluded to avoid the + // overhead of reading all the builds and their package + // specs. The tradeoff is that resolvo doesn't learn + // these builds exist to use them when constructing + // error messages. However from observation solver + // errors already don't mention versions that aren't + // applicable to root requests. + if let Some(pkg_request) = root_pkg_request { + if !pkg_request.pkg.version.is_applicable(version).is_ok() { + continue; + } + } + + // TODO: We need a borrowing version of this to avoid cloning. + let pkg_version = + VersionIdent::new(pkg_name.name.clone(), (**version).clone()); + + let builds = repo + .list_package_builds(&pkg_version) + .await + .unwrap_or_default(); + + for build in builds { + let located_build_ident = + LocatedBuildIdent::new(repo.name().to_owned(), build.clone()); + if let SyntheticComponent::Actual(pkg_name_component) = + &pkg_name.component + { + let components = + if let Build::Embedded(EmbeddedSource::Package(_parent)) = + build.build() + { + // Does this embedded stub contain the component + // being requested? For whatever reason, + // list_build_components returns an empty list for + // embedded stubs. + itertools::Either::Right( + if let Ok(stub) = repo.read_embed_stub(&build).await { + itertools::Either::Right( + stub.components() + .iter() + .map(|component_spec| { + component_spec.name.clone() + }) + .collect::>() + .into_iter(), + ) + } else { + itertools::Either::Left(std::iter::empty()) + }, + ) + } else { + itertools::Either::Left( + repo.list_build_components(&build) + .await + .unwrap_or_default(), + ) + }; + for component in components.into_iter().chain( + // A build representing the All component is included so + // when a request for it is found it can act as a + // surrogate that depends on all the individual + // components. + { + if !build.is_source() { + itertools::Either::Left([Component::All].into_iter()) + } else { + // XXX: Unclear if this is the right + // approach but without this special + // case the Solution can incorrectly + // end up with a src build marked as + // requires_build_from_source for + // requests that are asking for + // :src. + itertools::Either::Right([].into_iter()) + } + }, + ) { + let requires_build_from_source = build.is_source() + && (component != *pkg_name_component + || pkg_name_component.is_all()); + + if requires_build_from_source && provider.binary_only { + // Deny anything that requires build + // from source when binary_only is + // enabled. + continue; + } + + if (!requires_build_from_source || !build.is_source()) + && component != *pkg_name_component + { + // Deny components that don't match + // unless it is possible to build from + // source. + continue; + } + + let new_entry = LocatedBuildIdentWithComponent { + ident: located_build_ident.clone(), + component: pkg_name.component.clone(), + requires_build_from_source, + }; + + if requires_build_from_source { + // _replace_ any existing entry, which + // might have + // requires_build_from_source == false, + // so it now is true. + located_builds.replace(new_entry); + } else { + // _insert_ to not overwrite any + // existing entry that might have + // requires_build_from_source == true. + located_builds.insert(new_entry); + } + } + } else { + located_builds.insert(LocatedBuildIdentWithComponent { + ident: located_build_ident, + component: SyntheticComponent::Base, + requires_build_from_source: false, + }); + } + } + } + } + + if located_builds.is_empty() { + return None; + } + + let mut candidates = Candidates { + candidates: Vec::with_capacity(located_builds.len()), + ..Default::default() + }; + + for build in located_builds { + // What we need from build before it is moved into the pool. + let ident = build.ident.clone(); + let requires_build_from_source = build.requires_build_from_source; + + let solvable_id = *provider + .interned_solvables + .borrow_mut() + .entry(SpkSolvable::LocatedBuildIdentWithComponent(build.clone())) + .or_insert_with(|| { + provider.pool.intern_solvable( + name, + SpkSolvable::LocatedBuildIdentWithComponent(build), + ) + }); + + // Filter builds that don't conform to global options + // XXX: This find runtime will add up. + let repo = provider + .repos + .iter() + .find(|repo| repo.name() == ident.repository_name()) + .expect("Expected solved package's repository to be in the list of repositories"); + + if requires_build_from_source { + match provider.can_build_from_source(&ident).await { + CanBuildFromSource::Yes => { + candidates.candidates.push(solvable_id); + } + CanBuildFromSource::No(reason) => { + candidates.excluded.push((solvable_id, reason)); + } + } + continue; + } + + match repo.read_package(ident.target()).await { + Ok(package) => { + // Filter builds that don't satisfy global var requests + if let Some(VarRequest { + value: PinnableValue::Pinned(expected_version), + .. + }) = provider.global_var_requests.get(ident.name().as_opt_name()) + { + if let Ok(expected_version) = parse_version_range(expected_version) + { + if let spk_schema::version::Compatibility::Incompatible( + incompatible_reason, + ) = expected_version.is_applicable(package.version()) + { + candidates.excluded.push(( + solvable_id, + provider.pool.intern_string(format!( + "build version does not satisfy global var request: {incompatible_reason}" + )), + )); + continue; + } + } + } + + // XXX: `package.check_satisfies_request` walks the + // package's build options, so is it better to do this loop + // over `option_values` here, or loop over all the + // global_var_requests instead? + for (opt_name, _value) in package.option_values() { + if let Some(request) = provider.global_var_requests.get(&opt_name) { + if let spk_schema::version::Compatibility::Incompatible( + incompatible_reason, + ) = package.check_satisfies_request(request) + { + candidates.excluded.push(( + solvable_id, + provider.pool.intern_string(format!( + "build option {opt_name} does not satisfy global var request: {incompatible_reason}" + )), + )); + continue; + } + } + } + + candidates.candidates.push(solvable_id); + } + Err(err) => { + candidates + .excluded + .push((solvable_id, provider.pool.intern_string(err.to_string()))); + } + } + } + + Some(candidates) + } + } + } +} + +impl std::fmt::Display for ResolvoPackageName { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ResolvoPackageName::GlobalVar(name) => write!(f, "{name}"), + ResolvoPackageName::PkgNameBufWithComponent(name) => write!(f, "{name}"), + } + } +} + +enum CanBuildFromSource { + Yes, + No(StringId), +} + +/// An iterator that yields slices of items that fall into the same partition. +/// +/// The partition is determined by the key function. +/// The items must be already sorted in ascending order by the key function. +struct PartitionIter<'a, I, F, K> +where + F: for<'i> Fn(&'i I) -> K, + K: PartialOrd, +{ + slice: &'a [I], + key_fn: F, +} + +impl<'a, I, F, K> PartitionIter<'a, I, F, K> +where + F: for<'i> Fn(&'i I) -> K, + K: PartialOrd, +{ + fn new(slice: &'a [I], key_fn: F) -> Self { + Self { slice, key_fn } + } +} + +impl<'a, I, F, K> Iterator for PartitionIter<'a, I, F, K> +where + F: for<'i> Fn(&'i I) -> K, + K: PartialOrd, +{ + type Item = &'a [I]; + + fn next(&mut self) -> Option { + let element = self.slice.first()?; + + // Is a binary search overkill? + let partition_key = (self.key_fn)(element); + // No need to check the first element again. + let p = + 1 + self.slice[1..].partition_point(|element| (self.key_fn)(element) <= partition_key); + + let part = &self.slice[..p]; + self.slice = &self.slice[p..]; + Some(part) + } +} + +pub(crate) struct SpkProvider { + pub(crate) pool: Pool, + repos: Vec>, + /// Global package requests. These can be used to constrain the candidates + /// returned for these packages. + global_pkg_requests: HashMap, + /// Global options, like what might be specified with `--opt` to `spk env`. + /// Indexed by name. If multiple requests happen to exist with the same + /// name, the last one is kept. + global_var_requests: HashMap>, + interned_solvables: RefCell>, + /// Track all the global var keys and values that have been witnessed while + /// solving. + known_global_var_values: RefCell>>, + /// Track which global var candidates have been queried by the solver. Once + /// queried, it is no longer possible to add more possible values without + /// restarting the solve. + queried_global_var_values: RefCell>, + cancel_solving: RefCell>, + binary_only: bool, + /// When recursively exploring building packages from source, track chain + /// of packages to detect cycles. + build_from_source_trail: RefCell>, +} + +impl SpkProvider { + /// Return if the given solvable is buildable from source considering the + /// existing requests. + async fn can_build_from_source(&self, ident: &LocatedBuildIdent) -> CanBuildFromSource { + if self.build_from_source_trail.borrow().contains(ident) { + return CanBuildFromSource::No( + self.pool + .intern_string(format!("cycle detected while building {ident} from source")), + ); + } + + // Get solver requirements from the recipe. + let recipe = match self + .repos + .iter() + .find(|repo| repo.name() == ident.repository_name()) + { + Some(repo) => match repo.read_recipe(&ident.clone().to_version_ident()).await { + Ok(recipe) if recipe.is_deprecated() => { + return CanBuildFromSource::No( + self.pool + .intern_string(format!("recipe for {ident} is deprecated")), + ); + } + Ok(recipe) => recipe, + Err(err) => { + return CanBuildFromSource::No( + self.pool + .intern_string(format!("failed to read recipe: {err}")), + ); + } + }, + None => { + return CanBuildFromSource::No( + self.pool + .intern_string("package's repository is not in the list of repositories"), + ); + } + }; + + // Do we try all the variants in the recipe? + let variants = recipe.default_variants( + // XXX: What should really go here? + &Default::default(), + ); + + let mut solve_errors = Vec::new(); + + for variant in variants.iter() { + let mut solver = super::Solver::new(self.repos.clone(), Cow::Borrowed(&[])); + solver.set_binary_only(false); + solver.set_build_from_source_trail(HashSet::from_iter( + self.build_from_source_trail + .borrow() + .iter() + .cloned() + .chain([ident.clone()]), + )); + + let build_requirements = match recipe.get_build_requirements(&variant) { + Ok(build_requirements) => build_requirements, + Err(err) => { + return CanBuildFromSource::No( + self.pool + .intern_string(format!("failed to get build requirements: {err}")), + ); + } + }; + + for request in build_requirements.iter() { + solver.add_request(request.clone()); + } + + // These are last to take priority over the requests in the recipe. + for request in self.global_var_requests.values() { + solver.add_request(Request::Var(request.clone())); + } + + match solver + .solve() + .instrument(debug_span!( + "recursive solve", + ident = ident.to_string(), + variant = variant.to_string() + )) + .await + { + Ok(_solution) => return CanBuildFromSource::Yes, + Err(err) => solve_errors.push(err), + }; + } + + CanBuildFromSource::No( + self.pool + .intern_string(format!("failed to build from source: {solve_errors:?}")), + ) + } + + pub fn new( + repos: Vec>, + binary_only: bool, + build_from_source_trail: HashSet, + ) -> Self { + Self { + pool: Pool::new(), + repos, + global_pkg_requests: Default::default(), + global_var_requests: Default::default(), + interned_solvables: Default::default(), + known_global_var_values: Default::default(), + queried_global_var_values: Default::default(), + cancel_solving: Default::default(), + binary_only, + build_from_source_trail: RefCell::new(build_from_source_trail), + } + } + + fn pkg_request_to_known_dependencies(&self, pkg_request: &PkgRequest) -> KnownDependencies { + let mut components = pkg_request.pkg.components.iter().peekable(); + let iter = if components.peek().is_some() { + itertools::Either::Right(components.cloned()) + } else { + itertools::Either::Left( + // A request with no components is assumed to be a request for + // the default_for_run (or source) component. + if pkg_request + .pkg + .build + .as_ref() + .map(|build| build.is_source()) + .unwrap_or(false) + { + std::iter::once(Component::Source) + } else { + std::iter::once(Component::default_for_run()) + }, + ) + }; + let mut known_deps = KnownDependencies { + requirements: Vec::new(), + constrains: Vec::new(), + }; + for component in iter { + let dep_name = + self.pool + .intern_package_name(ResolvoPackageName::PkgNameBufWithComponent( + PkgNameBufWithComponent { + name: pkg_request.pkg.name().to_owned(), + component: SyntheticComponent::Actual(component.clone()), + }, + )); + let mut pkg_request_with_component = pkg_request.clone(); + pkg_request_with_component.pkg.components = BTreeSet::from_iter([component]); + let dep_vs = self.pool.intern_version_set( + dep_name, + RequestVS::SpkRequest(Request::Pkg(pkg_request_with_component)), + ); + match pkg_request.inclusion_policy { + spk_schema::ident::InclusionPolicy::Always => { + known_deps.requirements.push(dep_vs.into()); + } + spk_schema::ident::InclusionPolicy::IfAlreadyPresent => { + known_deps.constrains.push(dep_vs); + } + } + } + known_deps + } + + /// Add any package requests found in the given requests to the global + /// package requests, returning a list of Requirement. + pub(crate) fn root_pkg_requirements(&mut self, requests: &[Request]) -> Vec { + self.global_pkg_requests.reserve(requests.len()); + requests + .iter() + .filter_map(|req| match req { + Request::Pkg(pkg) => Some(pkg), + _ => None, + }) + .flat_map(|req| { + self.global_pkg_requests + .insert(req.pkg.name().to_owned(), req.clone()); + self.pkg_request_to_known_dependencies(req).requirements + }) + .collect() + } + + /// Return a list of requirements for all the package requests found in the + /// given requests. + fn dep_pkg_requirements(&self, requests: &[Request]) -> Vec { + requests + .iter() + .filter_map(|req| match req { + Request::Pkg(pkg) => Some(pkg), + _ => None, + }) + .flat_map(|req| self.pkg_request_to_known_dependencies(req).requirements) + .collect() + } + + pub fn is_canceled(&self) -> bool { + self.cancel_solving.borrow().is_some() + } + + /// Return an iterator that yields slices of builds that are from the same + /// package version. + /// + /// The provided builds must already be sorted otherwise the behavior is + /// undefined. + fn find_version_runs<'a>( + builds: &'a [(SolvableId, &'a LocatedBuildIdentWithComponent, Arc)], + ) -> impl Iterator)]> + { + PartitionIter::new(builds, |(_, ident, _)| { + // partition by (name, version) ignoring repository + (ident.ident.name(), ident.ident.version()) + }) + } + + fn request_to_known_dependencies(&self, requirement: &Request) -> KnownDependencies { + let mut known_deps = KnownDependencies::default(); + match requirement { + Request::Pkg(pkg_request) => { + let kd = self.pkg_request_to_known_dependencies(pkg_request); + known_deps.requirements.extend(kd.requirements); + known_deps.constrains.extend(kd.constrains); + } + Request::Var(var_request) => { + match &var_request.value { + spk_schema::ident::PinnableValue::FromBuildEnv => todo!(), + spk_schema::ident::PinnableValue::FromBuildEnvIfPresent => todo!(), + spk_schema::ident::PinnableValue::Pinned(value) => { + let dep_name = match var_request.var.namespace() { + Some(pkg_name) => self.pool.intern_package_name( + ResolvoPackageName::PkgNameBufWithComponent( + PkgNameBufWithComponent { + name: pkg_name.to_owned(), + component: SyntheticComponent::Base, + }, + ), + ), + None => { + // Since we will be adding constraints for + // global vars we need to add the pseudo-package + // to the dependency list so it will influence + // decisions. + if self + .known_global_var_values + .borrow_mut() + .entry(var_request.var.without_namespace().to_owned()) + .or_default() + .insert(VarValue::ArcStr(Arc::clone(value))) + && self + .queried_global_var_values + .borrow() + .contains(var_request.var.without_namespace()) + { + // Seeing a new value for a var that has + // already locked in the list of candidates. + *self.cancel_solving.borrow_mut() = Some(format!( + "Saw new value for global var: {}/{value}", + var_request.var.without_namespace() + )); + } + let dep_name = + self.pool.intern_package_name(ResolvoPackageName::GlobalVar( + var_request.var.without_namespace().to_owned(), + )); + known_deps.requirements.push( + self.pool + .intern_version_set( + dep_name, + RequestVS::GlobalVar { + key: var_request.var.without_namespace().to_owned(), + value: VarValue::ArcStr(Arc::clone(value)), + }, + ) + .into(), + ); + dep_name + } + }; + // If we end up adding pkg_name to the solve, it needs + // to satisfy this var request. + known_deps.constrains.push(self.pool.intern_version_set( + dep_name, + RequestVS::SpkRequest(requirement.clone()), + )); + } + } + } + } + known_deps + } + + /// Return a new provider to restart the solve, preserving what was learned + /// about global variables. + pub fn reset(&self) -> Self { + Self { + pool: Pool::new(), + repos: self.repos.clone(), + global_pkg_requests: self.global_pkg_requests.clone(), + global_var_requests: self.global_var_requests.clone(), + interned_solvables: Default::default(), + known_global_var_values: RefCell::new(self.known_global_var_values.take()), + queried_global_var_values: Default::default(), + cancel_solving: Default::default(), + binary_only: self.binary_only, + build_from_source_trail: self.build_from_source_trail.clone(), + } + } + + /// Order two builds based on which should be preferred to include in a + /// solve as a candidate. + /// + /// Generally this means a build with newer dependencies is ordered first. + fn sort_builds( + &self, + build_key_index: &HashMap, + a: (SolvableId, &LocatedBuildIdentWithComponent), + b: (SolvableId, &LocatedBuildIdentWithComponent), + ) -> std::cmp::Ordering { + // This function should _not_ return `std::cmp::Ordering::Equal` unless + // `a` and `b` are the same build (in practice this function will never + // be called when that is true). + + // Embedded stubs are always ordered last. + match (a.1.ident.is_embedded(), b.1.ident.is_embedded()) { + (true, false) => return std::cmp::Ordering::Greater, + (false, true) => return std::cmp::Ordering::Less, + _ => {} + }; + + match (build_key_index.get(&a.0), build_key_index.get(&b.0)) { + (Some(a_key), Some(b_key)) => { + // BuildKey orders in reverse order from what is needed here. + return b_key.cmp(a_key); + } + (Some(_), None) => return std::cmp::Ordering::Less, + (None, Some(_)) => return std::cmp::Ordering::Greater, + _ => {} + }; + + // If neither build has a key, both packages failed to load? + // Add debug assert to see if this ever happens. + debug_assert!(false, "builds without keys {a:?} {b:?}"); + + a.1.ident.cmp(&b.1.ident) + } + + pub fn var_requirements(&mut self, requests: &[Request]) -> Vec { + self.global_var_requests.reserve(requests.len()); + requests + .iter() + .filter_map(|req| match req { + Request::Var(var) => Some(var), + _ => None, + }) + .filter_map(|req| match req.var.namespace() { + Some(pkg_name) => { + // A global request applicable to a specific package. + let dep_name = + self.pool + .intern_package_name(ResolvoPackageName::PkgNameBufWithComponent( + PkgNameBufWithComponent { + name: pkg_name.to_owned(), + component: SyntheticComponent::Base, + }, + )); + Some(self.pool.intern_version_set( + dep_name, + RequestVS::SpkRequest(Request::Var(req.clone())), + )) + } + None => { + // A global request affecting all packages. + self.global_var_requests + .insert(req.var.without_namespace().to_owned(), req.clone()); + match &req.value { + PinnableValue::FromBuildEnv => {} + PinnableValue::FromBuildEnvIfPresent => {} + PinnableValue::Pinned(arc) => { + self.known_global_var_values + .borrow_mut() + .entry(req.var.without_namespace().to_owned()) + .or_default() + .insert(VarValue::ArcStr(Arc::clone(arc))); + } + }; + None + } + }) + .collect() + } + + pub fn var_requirements_from_options(&mut self, options: OptionMap) -> Vec { + self.global_var_requests.reserve(options.len()); + options + .into_iter() + .filter_map(|(var, value)| { + let var_value = VarValue::Owned(value.clone()); + let req = VarRequest::new_with_value(var, value); + match req.var.namespace() { + Some(pkg_name) => { + // A global request applicable to a specific package. + let dep_name = self.pool.intern_package_name( + ResolvoPackageName::PkgNameBufWithComponent(PkgNameBufWithComponent { + name: pkg_name.to_owned(), + component: SyntheticComponent::Base, + }), + ); + Some( + self.pool.intern_version_set( + dep_name, + RequestVS::SpkRequest(Request::Var(req)), + ), + ) + } + None => { + // A global request affecting all packages. + self.global_var_requests + .insert(req.var.without_namespace().to_owned(), req.clone()); + self.known_global_var_values + .borrow_mut() + .entry(req.var.without_namespace().to_owned()) + .or_default() + .insert(var_value); + None + } + } + }) + .collect() + } +} + +impl DependencyProvider for SpkProvider { + async fn filter_candidates( + &self, + candidates: &[SolvableId], + version_set: VersionSetId, + inverse: bool, + ) -> Vec { + let mut selected = Vec::with_capacity(candidates.len()); + let request_vs = self.pool.resolve_version_set(version_set); + for candidate in candidates { + let solvable = self.pool.resolve_solvable(*candidate); + match &request_vs { + RequestVS::SpkRequest(Request::Pkg(pkg_request)) => { + let SpkSolvable::LocatedBuildIdentWithComponent( + located_build_ident_with_component, + ) = &solvable.record + else { + if inverse { + selected.push(*candidate); + } + continue; + }; + + let compatible = pkg_request + .is_version_applicable(located_build_ident_with_component.ident.version()); + if compatible.is_ok() { + tracing::trace!(pkg_request = %pkg_request.pkg, version = %located_build_ident_with_component.ident.version(), "version applicable"); + let is_source = + located_build_ident_with_component.ident.build().is_source(); + + // If build from source is enabled, any source build is + // a candidate. Source builds that can't be built from + // source are filtered out in `get_candidates`. + if located_build_ident_with_component.requires_build_from_source { + // However, building from source is not a suitable + // candidate for a request for a specific component + // of an existing build, such as when finding the + // members of the :all component of a build. + if pkg_request + .pkg + .build + .as_ref() + .is_some_and(|b| b.is_buildid()) + { + if inverse { + selected.push(*candidate); + } + continue; + } + + if !inverse { + selected.push(*candidate); + } + continue; + } + + // Only select source builds for requests of source builds. + if is_source { + if pkg_request + .pkg + .build + .as_ref() + .map(|build| build.is_source()) + .unwrap_or(false) + ^ inverse + { + selected.push(*candidate); + } + continue; + } + + // Only select All component for requests of All + // component. + if located_build_ident_with_component.component.is_all() { + // This can disqualify but not qualify; version + // compatibility check is still required. + if !pkg_request.pkg.components.contains(&Component::All) { + if inverse { + selected.push(*candidate); + } + continue; + } + } else { + // Only the All component can satisfy requests for All. + if pkg_request.pkg.components.contains(&Component::All) { + if inverse { + selected.push(*candidate); + } + continue; + } + + // Only the x component can satisfy requests for x. + let mut at_least_one_request_matched_this_solvable = None; + for component in pkg_request.pkg.components.iter() { + if component.is_all() { + continue; + } + if component == &located_build_ident_with_component.component { + at_least_one_request_matched_this_solvable = Some(true); + break; + } else { + at_least_one_request_matched_this_solvable = Some(false); + } + } + + match at_least_one_request_matched_this_solvable { + Some(true) => { + if inverse { + continue; + } + } + Some(false) => { + // The request is for specific components but + // this solvable doesn't match any of them. + if inverse { + selected.push(*candidate); + continue; + } + } + None => { + // TODO: if at_least_one_request_matched_this_solvable + // is None it means the request didn't specify a + // component. Decide which specific component this + // should match. + } + } + } + + // XXX: This find runtime will add up. + let repo = self + .repos + .iter() + .find(|repo| repo.name() == located_build_ident_with_component.ident.repository_name()) + .expect( + "Expected solved package's repository to be in the list of repositories", + ); + if let Ok(package) = repo + .read_package(located_build_ident_with_component.ident.target()) + .await + { + if pkg_request.is_satisfied_by(&package).is_ok() ^ inverse { + tracing::trace!(pkg_request = %pkg_request.pkg, package = %package.ident(), %inverse, "satisfied by"); + selected.push(*candidate); + } + } else if inverse { + // If reading the package failed but inverse is true, should + // we include the package as a candidate? Unclear. + selected.push(*candidate); + } + } else if inverse { + selected.push(*candidate); + } + } + RequestVS::SpkRequest(Request::Var(var_request)) => { + match var_request.var.namespace() { + Some(pkg_name) => { + let SpkSolvable::LocatedBuildIdentWithComponent( + located_build_ident_with_component, + ) = &solvable.record + else { + if inverse { + selected.push(*candidate); + } + continue; + }; + // Will this ever not match? + debug_assert_eq!( + pkg_name, + located_build_ident_with_component.ident.name() + ); + // XXX: This find runtime will add up. + let repo = self + .repos + .iter() + .find(|repo| repo.name() == located_build_ident_with_component.ident.repository_name()) + .expect( + "Expected solved package's repository to be in the list of repositories", + ); + if let Ok(package) = repo + .read_package(located_build_ident_with_component.ident.target()) + .await + { + if var_request.is_satisfied_by(&package).is_ok() ^ inverse { + selected.push(*candidate); + } + } else if inverse { + // If reading the package failed but inverse is true, should + // we include the package as a candidate? Unclear. + selected.push(*candidate); + } + } + None => match &var_request.value { + spk_schema::ident::PinnableValue::FromBuildEnv => todo!(), + spk_schema::ident::PinnableValue::FromBuildEnvIfPresent => todo!(), + spk_schema::ident::PinnableValue::Pinned(value) => { + let SpkSolvable::GlobalVar { + key: record_key, + value: record_value, + } = &solvable.record + else { + if inverse { + selected.push(*candidate); + } + continue; + }; + if (var_request.var.without_namespace() == record_key + && value == record_value) + ^ inverse + { + selected.push(*candidate); + } + } + }, + } + } + RequestVS::GlobalVar { key, value } => { + let SpkSolvable::GlobalVar { + key: record_key, + value: record_value, + } = &solvable.record + else { + if inverse { + selected.push(*candidate); + } + continue; + }; + if (key == record_key && value == record_value) ^ inverse { + selected.push(*candidate); + } + } + } + } + selected + } + + async fn get_candidates(&self, name: NameId) -> Option { + let resolvo_package_name = self.pool.resolve_package_name(name); + resolvo_package_name.get_candidates(name, self).await + } + + async fn sort_candidates(&self, _solver: &SolverCache, solvables: &mut [SolvableId]) { + // Goal: Create a `BuildKey` for each build in `solvables`. + // The `BuildKey` factory needs as input the output from + // `BuildToSortedOptName::sort_builds`. + // `BuildToSortedOptName::sort_builds` needs to be fed builds from the + // same version. + // `solvables` can be builds from various versions so they need to be + // grouped by version. + let build_solvables = solvables + .iter() + .filter_map(|solvable_id| { + let solvable = self.pool.resolve_solvable(*solvable_id); + match &solvable.record { + SpkSolvable::LocatedBuildIdentWithComponent( + located_build_ident_with_component, + ) => + // sorting the source build (if any) is handled + // elsewhere; skip source builds. + { + located_build_ident_with_component + .ident + .is_source() + .not() + .then_some((*solvable_id, located_build_ident_with_component)) + } + _ => None, + } + }) + .sorted_by( + |(_, LocatedBuildIdentWithComponent { ident: a, .. }), + (_, LocatedBuildIdentWithComponent { ident: b, .. })| { + // build_solvables will be ordered by (pkg, version, build). + a.target().cmp(b.target()) + }, + ) + .collect::>(); + + // `BuildToSortedOptName::sort_builds` will need the package specs. + let mut build_solvables_and_specs = Vec::with_capacity(build_solvables.len()); + for build_solvable in build_solvables { + let (solvable_id, located_build_ident_with_component) = build_solvable; + let repo = self + .repos + .iter() + .find(|repo| { + repo.name() == located_build_ident_with_component.ident.repository_name() + }) + .expect("Expected solved package's repository to be in the list of repositories"); + let Ok(package) = repo + .read_package(located_build_ident_with_component.ident.target()) + .await + else { + // Any builds that can't load the spec will be sorted to the + // end. In most cases the package spec would already be loaded + // in cache at this point. + continue; + }; + build_solvables_and_specs.push(( + solvable_id, + located_build_ident_with_component, + package, + )); + } + + let mut build_key_index = HashMap::new(); + build_key_index.reserve(build_solvables_and_specs.len()); + + // Find runs of the same package version. + for version_run in SpkProvider::find_version_runs(&build_solvables_and_specs) { + let (ordered_names, build_name_values) = + BuildToSortedOptName::sort_builds(version_run.iter().map(|(_, _, spec)| spec)); + + for (solvable_id, _, spec) in version_run { + let build_key = SortedBuildIterator::make_option_values_build_key( + spec, + &ordered_names, + &build_name_values, + false, + ); + build_key_index.insert(*solvable_id, build_key); + } + } + + // TODO: The ordering should take component names into account, so + // the run component or the build component is tried first in the + // appropriate situations. + solvables.sort_by(|solvable_id_a, solvable_id_b| { + let a = self.pool.resolve_solvable(*solvable_id_a); + let b = self.pool.resolve_solvable(*solvable_id_b); + match (&a.record, &b.record) { + ( + SpkSolvable::LocatedBuildIdentWithComponent(a), + SpkSolvable::LocatedBuildIdentWithComponent(b), + ) => { + // Sort source packages last to prefer using any existing + // build of whatever version over building from source. + match (a.ident.build(), b.ident.build()) { + (Build::Source, Build::Source) => {} + (Build::Source, _) => return std::cmp::Ordering::Greater, + (_, Build::Source) => return std::cmp::Ordering::Less, + _ => {} + }; + // Sort embedded packages second last, even if an embedded + // package has the highest version. + match (a.ident.build(), b.ident.build()) { + (Build::Embedded(_), Build::Embedded(_)) => {} + (Build::Embedded(_), _) => return std::cmp::Ordering::Greater, + (_, Build::Embedded(_)) => return std::cmp::Ordering::Less, + _ => {} + }; + // Then prefer higher versions... + match b.ident.version().cmp(a.ident.version()) { + std::cmp::Ordering::Equal => { + // Sort source builds last + match (a.ident.build(), b.ident.build()) { + (Build::Source, Build::Source) => {} + (Build::Source, _) => return std::cmp::Ordering::Greater, + (_, Build::Source) => return std::cmp::Ordering::Less, + _ => {} + }; + self.sort_builds( + &build_key_index, + (*solvable_id_a, a), + (*solvable_id_b, b), + ) + } + ord => ord, + } + } + ( + SpkSolvable::GlobalVar { + key: a_key, + value: a_value, + }, + SpkSolvable::GlobalVar { + key: b_key, + value: b_value, + }, + ) => { + if a_key == b_key { + a_value.cmp(b_value) + } else { + a_key.cmp(b_key) + } + } + (SpkSolvable::LocatedBuildIdentWithComponent(_), SpkSolvable::GlobalVar { .. }) => { + std::cmp::Ordering::Less + } + (SpkSolvable::GlobalVar { .. }, SpkSolvable::LocatedBuildIdentWithComponent(_)) => { + std::cmp::Ordering::Greater + } + } + }); + } + + async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies { + let solvable = self.pool.resolve_solvable(solvable); + let SpkSolvable::LocatedBuildIdentWithComponent(located_build_ident_with_component) = + &solvable.record + else { + return Dependencies::Known(KnownDependencies::default()); + }; + let actual_component = match &located_build_ident_with_component.component { + SyntheticComponent::Base => { + // Base can't depend on anything because we don't know what + // components actually exist or if requests exist for whatever it + // was we picked if we were to pick a component to depend on. + return Dependencies::Known(KnownDependencies::default()); + } + SyntheticComponent::Actual(component) => component, + }; + // XXX: This find runtime will add up. + let repo = self + .repos + .iter() + .find(|repo| repo.name() == located_build_ident_with_component.ident.repository_name()) + .expect("Expected solved package's repository to be in the list of repositories"); + match repo + .read_package(located_build_ident_with_component.ident.target()) + .await + { + Ok(package) => { + let mut known_deps = KnownDependencies { + requirements: Vec::with_capacity(package.runtime_requirements().len()), + // This is where IfAlreadyPresent constraints would go. + constrains: Vec::with_capacity(package.get_build_options().len()), + }; + if located_build_ident_with_component.component.is_all() { + // The only dependencies of the All component are the other + // components defined in the package. + for component_spec in package.components().iter() { + let dep_name = self.pool.intern_package_name( + ResolvoPackageName::PkgNameBufWithComponent(PkgNameBufWithComponent { + name: located_build_ident_with_component.ident.name().to_owned(), + component: SyntheticComponent::Actual(component_spec.name.clone()), + }), + ); + known_deps.requirements.push( + self.pool + .intern_version_set( + dep_name, + RequestVS::SpkRequest( + located_build_ident_with_component + .as_request_with_components([component_spec + .name + .clone()]), + ), + ) + .into(), + ); + } + return Dependencies::Known(known_deps); + } else { + // For any non-All/non-Base component, add a dependency on + // the base to ensure all components come from the same + // base version. + let dep_name = + self.pool + .intern_package_name(ResolvoPackageName::PkgNameBufWithComponent( + PkgNameBufWithComponent { + name: located_build_ident_with_component + .ident + .name() + .to_owned(), + component: SyntheticComponent::Base, + }, + )); + known_deps.requirements.push( + self.pool + .intern_version_set( + dep_name, + RequestVS::SpkRequest( + located_build_ident_with_component + .as_request_with_components([]), + ), + ) + .into(), + ); + // Also add dependencies on any components that this + // component "uses" and its install requirements. + if let Some(component_spec) = package + .components() + .iter() + .find(|component_spec| component_spec.name == *actual_component) + { + component_spec.uses.iter().for_each(|uses| { + let dep_name = self.pool.intern_package_name( + ResolvoPackageName::PkgNameBufWithComponent( + PkgNameBufWithComponent { + name: located_build_ident_with_component + .ident + .name() + .to_owned(), + component: SyntheticComponent::Actual(uses.clone()), + }, + ), + ); + known_deps.requirements.push( + self.pool + .intern_version_set( + dep_name, + RequestVS::SpkRequest( + located_build_ident_with_component + .as_request_with_components([uses.clone()]), + ), + ) + .into(), + ); + }); + known_deps + .requirements + .extend(self.dep_pkg_requirements(&component_spec.requirements)); + } + } + // Also add dependencies on any packages embedded in this + // component. + for embedded in package.embedded().iter() { + // If this embedded package is configured to exist in + // specific components, then skip it if this solvable's + // component is not one of those. + let components_where_this_embedded_package_exists = package + .components() + .iter() + .filter_map(|component_spec| { + if component_spec.embedded.iter().any(|embedded_package| { + embedded_package.pkg.name() == embedded.name() + && embedded_package + .pkg + .target() + .as_ref() + .map(|version| version == embedded.version()) + .unwrap_or(true) + }) { + Some(component_spec.name.clone()) + } else { + None + } + }) + .collect::>(); + if !components_where_this_embedded_package_exists.is_empty() + && !components_where_this_embedded_package_exists.contains(actual_component) + { + continue; + } + + let dep_name = + self.pool + .intern_package_name(ResolvoPackageName::PkgNameBufWithComponent( + PkgNameBufWithComponent { + name: embedded.name().to_owned(), + component: located_build_ident_with_component.component.clone(), + }, + )); + known_deps.requirements.push( + self.pool + .intern_version_set( + dep_name, + RequestVS::SpkRequest(Request::Pkg(PkgRequest::new( + RangeIdent { + repository_name: Some( + located_build_ident_with_component + .ident + .repository_name() + .to_owned(), + ), + name: embedded.name().to_owned(), + components: Default::default(), + version: VersionFilter::single( + DoubleEqualsVersion::version_range( + embedded.version().clone(), + ), + ), + // This needs to match the build of + // the stub for get_candidates to like + // it. Stub parents are always the Run + // component. + build: Some(Build::Embedded(EmbeddedSource::Package( + Box::new(EmbeddedSourcePackage { + ident: package.ident().into(), + components: BTreeSet::from_iter([Component::Run]), + }), + ))), + }, + RequestedBy::Embedded( + located_build_ident_with_component.ident.target().clone(), + ), + ))), + ) + .into(), + ); + // Any install requirements of components inside embedded + // packages with the same name as this component also + // become dependencies. + for embedded_component_requirement in embedded + .components() + .iter() + .filter(|embedded_component| embedded_component.name == *actual_component) + .flat_map(|embedded_component| embedded_component.requirements.iter()) + { + let kd = self.request_to_known_dependencies(embedded_component_requirement); + known_deps.requirements.extend(kd.requirements); + known_deps.constrains.extend(kd.constrains); + } + } + // If this solvable is an embedded stub and it is + // representing that it provides a component that lives in a + // component of the parent, then that parent component needs + // to be included in the solution. + if let Build::Embedded(EmbeddedSource::Package(parent)) = + located_build_ident_with_component.ident.build() + { + let parent_ident: BuildIdent = match (**parent).clone().try_into() { + Ok(ident) => ident, + Err(err) => { + let msg = self.pool.intern_string(format!( + "failed to get valid parent ident for '{}': {err}", + located_build_ident_with_component.ident + )); + return Dependencies::Unknown(msg); + } + }; + let parent = match repo.read_package(&parent_ident).await { + Ok(spec) => spec, + Err(err) => { + let msg = self.pool.intern_string(format!( + "failed to read parent package for '{}': {err}", + located_build_ident_with_component.ident + )); + return Dependencies::Unknown(msg); + } + }; + // Look through the components of the parent to see + // if one (or more?) of them embeds this component. + let mut found = false; + for parent_component in parent.components().iter() { + parent_component + .embedded + .iter() + .filter(|embedded_package| { + embedded_package.pkg.name() + == located_build_ident_with_component.ident.name() + && embedded_package + .pkg + .target() + .as_ref() + .map(|version| { + version + == located_build_ident_with_component + .ident + .version() + }) + .unwrap_or(true) + && embedded_package.components().contains(actual_component) + }) + .for_each(|_embedded_package| { + found = true; + let dep_name = self.pool.intern_package_name( + ResolvoPackageName::PkgNameBufWithComponent( + PkgNameBufWithComponent { + name: parent_ident.name().to_owned(), + component: SyntheticComponent::Actual( + parent_component.name.clone(), + ), + }, + ), + ); + known_deps.requirements.push( + self.pool + .intern_version_set( + dep_name, + RequestVS::SpkRequest(Request::Pkg(PkgRequest::new( + RangeIdent { + repository_name: Some( + located_build_ident_with_component + .ident + .repository_name() + .to_owned(), + ), + name: parent_ident.name().to_owned(), + components: BTreeSet::from_iter([ + parent_component.name.clone(), + ]), + version: VersionFilter::single( + DoubleEqualsVersion::version_range( + parent_ident.version().clone(), + ), + ), + build: Some(parent_ident.build().clone()), + }, + RequestedBy::Embedded( + located_build_ident_with_component + .ident + .target() + .clone(), + ), + ))), + ) + .into(), + ); + }); + } + if !found { + // In the event that no owning component was found, + // this stub must still bring in at least one + // component from the parent. By convention, bring + // in the Run component of the parent. + let dep_name = self.pool.intern_package_name( + ResolvoPackageName::PkgNameBufWithComponent(PkgNameBufWithComponent { + name: parent_ident.name().to_owned(), + component: SyntheticComponent::Actual(Component::Run), + }), + ); + let located_parent = LocatedBuildIdentWithComponent { + ident: parent_ident.clone().to_located( + located_build_ident_with_component + .ident + .repository_name() + .to_owned(), + ), + // as_request_with_components does not make use + // of the component field, assigning Base here + // does not imply anything. + component: SyntheticComponent::Base, + requires_build_from_source: false, + }; + known_deps.requirements.push( + self.pool + .intern_version_set( + dep_name, + RequestVS::SpkRequest( + located_parent.as_request_with_components([Component::Run]), + ), + ) + .into(), + ); + } + } + for option in package.get_build_options() { + let Opt::Var(var_opt) = option else { + continue; + }; + if var_opt.var.namespace().is_some() { + continue; + } + let Some(value) = var_opt.get_value(None) else { + continue; + }; + if self + .known_global_var_values + .borrow_mut() + .entry(var_opt.var.without_namespace().to_owned()) + .or_default() + .insert(VarValue::Owned(value.clone())) + && self + .queried_global_var_values + .borrow() + .contains(var_opt.var.without_namespace()) + { + // Seeing a new value for a var that has already locked + // in the list of candidates. + *self.cancel_solving.borrow_mut() = Some(format!( + "Saw new value for global var: {}/{value}", + var_opt.var.without_namespace() + )); + } + let dep_name = self.pool.intern_package_name(ResolvoPackageName::GlobalVar( + var_opt.var.without_namespace().to_owned(), + )); + // Add a constraint not a dependency because the package + // is targeting a specific global var value but there may + // not be a request for that var of a specific value. + known_deps.constrains.push(self.pool.intern_version_set( + dep_name, + RequestVS::GlobalVar { + key: var_opt.var.without_namespace().to_owned(), + value: VarValue::Owned(value), + }, + )); + } + for requirement in package.runtime_requirements().iter() { + let kd = self.request_to_known_dependencies(requirement); + known_deps.requirements.extend(kd.requirements); + known_deps.constrains.extend(kd.constrains); + } + Dependencies::Known(known_deps) + } + Err(err) => { + let msg = self.pool.intern_string(err.to_string()); + Dependencies::Unknown(msg) + } + } + } + + fn should_cancel_with_value(&self) -> Option> { + if let Some(msg) = self.cancel_solving.borrow().as_ref() { + // Eventually there will be more than one reason the solve is + // cancelled... + Some(Box::new(msg.clone())) + } else { + None + } + } +} + +impl Interner for SpkProvider { + fn display_solvable(&self, solvable: SolvableId) -> impl std::fmt::Display + '_ { + let solvable = self.pool.resolve_solvable(solvable); + format!("{}", solvable.record) + } + + fn display_name(&self, name: NameId) -> impl std::fmt::Display + '_ { + self.pool.resolve_package_name(name) + } + + fn display_version_set(&self, version_set: VersionSetId) -> impl std::fmt::Display + '_ { + self.pool.resolve_version_set(version_set) + } + + fn display_string(&self, string_id: StringId) -> impl std::fmt::Display + '_ { + self.pool.resolve_string(string_id) + } + + fn version_set_name(&self, version_set: VersionSetId) -> NameId { + self.pool.resolve_version_set_package_name(version_set) + } + + fn solvable_name(&self, solvable: SolvableId) -> NameId { + self.pool.resolve_solvable(solvable).name + } + + fn version_sets_in_union( + &self, + version_set_union: VersionSetUnionId, + ) -> impl Iterator { + self.pool.resolve_version_set_union(version_set_union) + } +} diff --git a/crates/spk-solve/src/solvers/solver_test.rs b/crates/spk-solve/src/solvers/solver_test.rs index 0e716d9b4b..553704857d 100644 --- a/crates/spk-solve/src/solvers/solver_test.rs +++ b/crates/spk-solve/src/solvers/solver_test.rs @@ -19,17 +19,19 @@ use spk_schema::ident::{ parse_ident_range, version_ident, }; -use spk_schema::ident_build::{Build, BuildId, EmbeddedSource}; +use spk_schema::ident_build::{Build, BuildId}; use spk_schema::prelude::*; use spk_schema::{recipe, v0}; use spk_solve_macros::{make_build, make_build_and_components, make_package, make_repo, request}; use spk_solve_solution::PackageSource; use spk_storage::RepositoryHandle; use spk_storage::fixtures::*; +use tap::prelude::*; use crate::io::DecisionFormatterBuilder; +use crate::solver::{SolverExt, SolverImpl, SolverMut}; use crate::solvers::step::{ErrorDetails, ErrorFreq}; -use crate::{Error, Result, StepSolver, option_map, spec}; +use crate::{Error, ResolvoSolver, Result, Solution, StepSolver, option_map, spec}; #[fixture] fn solver() -> StepSolver { @@ -43,12 +45,15 @@ fn solver() -> StepSolver { /// of resolved components, or the specific build of the package. macro_rules! assert_resolved { ($solution:ident, $pkg:literal, $version:literal) => { - assert_resolved!($solution, $pkg, $version, "wrong package version was resolved") + assert_resolved!($solution, $pkg, version = $version, "wrong package version was resolved") + }; + ($solution:ident, $pkg:literal, version = $version:expr) => { + assert_resolved!($solution, $pkg, version = $version, "wrong package version was resolved") }; ($solution:ident, $pkg:literal, $version:literal, $message:literal) => { assert_resolved!($solution, $pkg, version = $version, $message) }; - ($solution:ident, $pkg:literal, version = $version:literal, $message:literal) => {{ + ($solution:ident, $pkg:literal, version = $version:expr, $message:literal) => {{ let pkg = $solution .get($pkg) .expect("expected package to be in solution"); @@ -65,6 +70,16 @@ macro_rules! assert_resolved { assert_eq!(pkg.spec.ident().build(), &$build, $message); }}; + ($solution:ident, $pkg:literal, build =~ $build:pat) => { + assert_resolved!($solution, $pkg, build =~ $build, "wrong package build was resolved") + }; + ($solution:ident, $pkg:literal, build =~ $build:pat, $message:literal) => {{ + let pkg = $solution + .get($pkg) + .expect("expected package to be in solution"); + assert!(matches!(pkg.spec.ident().build(), $build), $message); + }}; + ($solution:ident, $pkg:literal, components = [$($component:literal),+ $(,)?]) => {{ let mut resolved = std::collections::HashSet::::new(); let pkg = $solution @@ -106,35 +121,61 @@ macro_rules! assert_not_resolved { /// Runs the given solver, printing the output with reasonable output settings /// for unit test debugging and inspection. -async fn run_and_print_resolve_for_tests(solver: &StepSolver) -> Result { - let formatter = DecisionFormatterBuilder::default() - .with_verbosity(100) - .build(); +async fn run_and_print_resolve_for_tests(solver: &mut SolverImpl) -> Result { + match solver { + SolverImpl::Step(solver) => { + let formatter = DecisionFormatterBuilder::default() + .with_verbosity(100) + .build(); + + let (solution, _) = formatter.run_and_print_resolve(solver).await?; + Ok(solution) + } - let (solution, _) = formatter.run_and_print_resolve(solver).await?; - Ok(solution) + SolverImpl::Resolvo(solver) => solver.solve().await, + } } /// Runs the given solver, logging the output with reasonable output settings /// for unit test debugging and inspection. -async fn run_and_log_resolve_for_tests(solver: &StepSolver) -> Result { - let formatter = DecisionFormatterBuilder::default() - .with_verbosity(100) - .build(); +async fn run_and_log_resolve_for_tests(solver: &mut SolverImpl) -> Result { + match solver { + SolverImpl::Step(solver) => { + let formatter = DecisionFormatterBuilder::default() + .with_verbosity(100) + .build(); + + let (solution, _) = formatter.run_and_log_resolve(solver).await?; + Ok(solution) + } + SolverImpl::Resolvo(solver) => solver.solve().await, + } +} + +fn step_solver() -> SolverImpl { + SolverImpl::Step(StepSolver::default()) +} - let (solution, _) = formatter.run_and_log_resolve(solver).await?; - Ok(solution) +fn resolvo_solver() -> SolverImpl { + SolverImpl::Resolvo(ResolvoSolver::default()) } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_no_requests(mut solver: StepSolver) { +async fn test_solver_no_requests(#[case] mut solver: SolverImpl) { solver.solve().await.unwrap(); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_package_with_no_recipe(mut solver: StepSolver, random_build_id: BuildId) { +async fn test_solver_package_with_no_recipe( + #[case] mut solver: SolverImpl, + random_build_id: BuildId, +) { let repo = RepositoryHandle::new_mem(); let options = option_map! {}; @@ -156,7 +197,7 @@ async fn test_solver_package_with_no_recipe(mut solver: StepSolver, random_build solver.add_request(request!("my-pkg")); // Test - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!( res.is_ok(), "'{res:?}' should be an Ok(_) solution not an error.')" @@ -164,9 +205,11 @@ async fn test_solver_package_with_no_recipe(mut solver: StepSolver, random_build } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] async fn test_solver_package_with_no_recipe_and_impossible_initial_checks( - mut solver: StepSolver, + #[case] mut solver: SolverImpl, random_build_id: BuildId, ) { init_logging(); @@ -184,13 +227,16 @@ async fn test_solver_package_with_no_recipe_and_impossible_initial_checks( solver.update_options(options); solver.add_repository(Arc::new(repo)); solver.add_request(request!("my-pkg")); - solver.set_initial_request_impossible_checks(true); + if let SolverImpl::Step(ref mut solver) = solver { + solver.set_initial_request_impossible_checks(true); + } // Test - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; if cfg!(feature = "migration-to-components") { match res { - Err(Error::InitialRequestsContainImpossibleError(_)) => { + Err(Error::InitialRequestsContainImpossibleError(_)) + | Err(Error::FailedToResolve(_)) => { // Success, when the 'migration-to-components' feature // is enabled because the initial checks for // impossible requests fail because the package does @@ -216,8 +262,10 @@ async fn test_solver_package_with_no_recipe_and_impossible_initial_checks( } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_package_with_no_recipe_from_cmd_line(mut solver: StepSolver) { +async fn test_solver_package_with_no_recipe_from_cmd_line(#[case] mut solver: SolverImpl) { let repo = RepositoryHandle::new_mem(); let spec = spec!({"pkg": "my-pkg/1.0.0/4OYMIQUY"}); @@ -240,7 +288,7 @@ async fn test_solver_package_with_no_recipe_from_cmd_line(mut solver: StepSolver solver.add_request(req); // Test - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!( res.is_ok(), "'{res:?}' should be an Ok(_) solution not an error.')" @@ -248,9 +296,11 @@ async fn test_solver_package_with_no_recipe_from_cmd_line(mut solver: StepSolver } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] async fn test_solver_package_with_no_recipe_from_cmd_line_and_impossible_initial_checks( - mut solver: StepSolver, + #[case] mut solver: SolverImpl, ) { init_logging(); let repo = RepositoryHandle::new_mem(); @@ -270,10 +320,12 @@ async fn test_solver_package_with_no_recipe_from_cmd_line_and_impossible_initial RequestedBy::CommandLine, )); solver.add_request(req); - solver.set_initial_request_impossible_checks(true); + if let SolverImpl::Step(ref mut solver) = solver { + solver.set_initial_request_impossible_checks(true); + } // Test - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; if cfg!(feature = "migration-to-components") { // with the 'migration-to-components' feature and impossible // request initial checks will fail because the feature turns @@ -281,7 +333,11 @@ async fn test_solver_package_with_no_recipe_from_cmd_line_and_impossible_initial // :build and a :run component to pass and it only has a :run // component assert!( - matches!(res, Err(Error::InitialRequestsContainImpossibleError(_))), + matches!( + res, + Err(Error::InitialRequestsContainImpossibleError(_)) + | Err(Error::FailedToResolve(_)) + ), "'{res:?}' should be a Error::String('Initial requests contain 1 impossible request.')", ); } else { @@ -297,8 +353,10 @@ async fn test_solver_package_with_no_recipe_from_cmd_line_and_impossible_initial } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_single_package_no_deps(mut solver: StepSolver) { +async fn test_solver_single_package_no_deps(#[case] mut solver: SolverImpl) { let options = option_map! {}; let repo = make_repo!([{"pkg": "my-pkg/1.0.0"}], options=options.clone()); @@ -306,7 +364,7 @@ async fn test_solver_single_package_no_deps(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("my-pkg")); - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_eq!(packages.len(), 1, "expected one resolved package"); let resolved = packages.get("my-pkg").unwrap(); assert_eq!(&resolved.spec.version().to_string(), "1.0.0"); @@ -314,8 +372,10 @@ async fn test_solver_single_package_no_deps(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_single_package_simple_deps(mut solver: StepSolver) { +async fn test_solver_single_package_simple_deps(#[case] mut solver: SolverImpl) { let options = option_map! {}; let repo = make_repo!( [ @@ -333,15 +393,17 @@ async fn test_solver_single_package_simple_deps(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("pkg-b/1.1")); - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_eq!(packages.len(), 2, "expected two resolved packages"); assert_resolved!(packages, "pkg-a", "1.2.1"); assert_resolved!(packages, "pkg-b", "1.1.0"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_dependency_abi_compat(mut solver: StepSolver) { +async fn test_solver_dependency_abi_compat(#[case] mut solver: SolverImpl) { let options = option_map! {}; let repo = make_repo!( [ @@ -362,15 +424,17 @@ async fn test_solver_dependency_abi_compat(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("pkg-b/1.1")); - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_eq!(packages.len(), 2, "expected two resolved packages"); assert_resolved!(packages, "pkg-a", "1.1.1"); assert_resolved!(packages, "pkg-b", "1.1.0"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_dependency_incompatible(mut solver: StepSolver) { +async fn test_solver_dependency_incompatible(#[case] mut solver: SolverImpl) { // test what happens when a dependency is added which is incompatible // with an existing request in the stack let repo = make_repo!( @@ -389,14 +453,16 @@ async fn test_solver_dependency_incompatible(mut solver: StepSolver) { // this one is incompatible with requirements of my-plugin but the solver doesn't know it yet solver.add_request(request!("maya/2019")); - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!(res.is_err()); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_dependency_incompatible_stepback(mut solver: StepSolver) { +async fn test_solver_dependency_incompatible_stepback(#[case] mut solver: SolverImpl) { // test what happens when a dependency is added which is incompatible // with an existing request in the stack - in this case we want the solver // to successfully step back into an older package version with @@ -421,15 +487,17 @@ async fn test_solver_dependency_incompatible_stepback(mut solver: StepSolver) { // this one is incompatible with requirements of my-plugin/1.1.0 but not my-plugin/1.0 solver.add_request(request!("maya/2019")); - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(packages, "my-plugin", "1.0.0"); assert_resolved!(packages, "maya", "2019.0.0"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_dependency_already_satisfied(mut solver: StepSolver) { +async fn test_solver_dependency_already_satisfied(#[case] mut solver: SolverImpl) { // test what happens when a dependency is added which represents // a package which has already been resolved // - and the resolved version satisfies the request @@ -452,15 +520,19 @@ async fn test_solver_dependency_already_satisfied(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("pkg-top")); - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(packages, ["pkg-top", "dep-1", "dep-2"]); assert_resolved!(packages, "dep-1", "1.0.0"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_dependency_already_satisfied_conflicting_components(mut solver: StepSolver) { +async fn test_solver_dependency_already_satisfied_conflicting_components( + #[case] mut solver: SolverImpl, +) { // like test_solver_dependency_already_satisfied but with conflicting components let repo = make_repo!( @@ -497,14 +569,16 @@ async fn test_solver_dependency_already_satisfied_conflicting_components(mut sol // How can this test code verify that the solver is actually hitting // that code path? - run_and_print_resolve_for_tests(&solver) + run_and_print_resolve_for_tests(&mut solver) .await .expect_err("solve should fail"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_dependency_reopen_solvable(mut solver: StepSolver) { +async fn test_solver_dependency_reopen_solvable(#[case] mut solver: SolverImpl) { // test what happens when a dependency is added which represents // a package which has already been resolved // - and the resolved version does not satisfy the request @@ -532,14 +606,16 @@ async fn test_solver_dependency_reopen_solvable(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("my-plugin")); - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(packages, ["my-plugin", "some-library", "maya"]); assert_resolved!(packages, "maya", "2019.0.0"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_dependency_reiterate(mut solver: StepSolver) { +async fn test_solver_dependency_reiterate(#[case] mut solver: SolverImpl) { // test what happens when a package iterator must be run through twice // - walking back up the solve graph should reset the iterator to where it was @@ -566,14 +642,16 @@ async fn test_solver_dependency_reiterate(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("my-plugin")); - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(packages, ["my-plugin", "some-library", "maya"]); assert_resolved!(packages, "maya", "2019.0.0"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_dependency_reopen_unsolvable(mut solver: StepSolver) { +async fn test_solver_dependency_reopen_unsolvable(#[case] mut solver: SolverImpl) { // test what happens when a dependency is added which represents // a package which has already been resolved // - and the resolved version does not satisfy the request @@ -599,13 +677,15 @@ async fn test_solver_dependency_reopen_unsolvable(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("pkg-top")); - let result = run_and_print_resolve_for_tests(&solver).await; + let result = run_and_print_resolve_for_tests(&mut solver).await; assert!(result.is_err()); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_pre_release_config(mut solver: StepSolver) { +async fn test_solver_pre_release_config(#[case] mut solver: SolverImpl) { let repo = make_repo!( [ {"pkg": "my-pkg/0.9.0"}, @@ -619,7 +699,7 @@ async fn test_solver_pre_release_config(mut solver: StepSolver) { solver.add_repository(repo.clone()); solver.add_request(request!("my-pkg")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!( solution, "my-pkg", @@ -631,13 +711,15 @@ async fn test_solver_pre_release_config(mut solver: StepSolver) { solver.add_repository(repo); solver.add_request(request!({"pkg": "my-pkg", "prereleasePolicy": "IncludeAll"})); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(solution, "my-pkg", "1.0.0-pre.2"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_constraint_only(mut solver: StepSolver) { +async fn test_solver_constraint_only(#[case] mut solver: SolverImpl) { // test what happens when a dependency is marked as a constraint/optional // and no other request is added // - the constraint is noted @@ -658,13 +740,15 @@ async fn test_solver_constraint_only(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("vnp3")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert!(solution.get("python").is_none()); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_constraint_and_request(mut solver: StepSolver) { +async fn test_solver_constraint_and_request(#[case] mut solver: SolverImpl) { // test what happens when a dependency is marked as a constraint/optional // and also requested by another package // - the constraint is noted @@ -691,14 +775,19 @@ async fn test_solver_constraint_and_request(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("my-tool")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(solution, "python", "3.7.3"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_option_compatibility(mut solver: StepSolver) { +async fn test_solver_option_compatibility( + #[case] mut solver: SolverImpl, + #[values("~2.0", "~2.7", "~2.7.5", "2,<3", "2.7,<3", "3", "3.7", "3.7.3")] pyver: &str, +) { // test what happens when an option is given in the solver // - the options for each build are checked // - the resolved build must have used the option @@ -738,48 +827,44 @@ async fn test_solver_option_compatibility(mut solver: StepSolver) { // added to some of the version ranges below force the solver to // work through the ordered builds until it finds an appropriate // 2.x.y values to both solve and pass the test. - for pyver in [ - // Uncomment this, when the '2,<3' parsing bug: https://github.com/spkenv/spk/issues/322 has been fixed - //"~2.0", "~2.7", "~2.7.5", "2,<3", "2.7,<3", "3", "3.7", "3.7.3", - "~2.0", "~2.7", "~2.7.5", "3", "3.7", "3.7.3", - ] { - solver.reset(); - solver.add_repository(repo.clone()); - solver.add_request(request!("vnp3")); - solver.add_request( - VarRequest { - var: opt_name!("python").to_owned(), - value: pyver.into(), - description: None, - } - .into(), - ); + solver.reset(); + solver.add_repository(repo.clone()); + solver.add_request(request!("vnp3")); + solver.add_request( + VarRequest { + var: opt_name!("python").to_owned(), + value: pyver.into(), + description: None, + } + .into(), + ); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); - - let resolved = solution.get("vnp3").unwrap(); - let value = resolved - .spec - .option_values() - .remove(opt_name!("python")) - .unwrap(); - - // Check the first digit component of the pyver value - let expected = if pyver.starts_with('~') { - format!("~{}", pyver.chars().nth(1).unwrap()).to_string() - } else { - format!("~{}", pyver.chars().next().unwrap()).to_string() - }; - assert!( - value.starts_with(&expected), - "{value} should start with ~{expected} to be valid for {pyver}" - ); - } + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); + + let resolved = solution.get("vnp3").unwrap(); + let value = resolved + .spec + .option_values() + .remove(opt_name!("python")) + .unwrap(); + + // Check the first digit component of the pyver value + let expected = if pyver.starts_with('~') { + format!("~{}", pyver.chars().nth(1).unwrap()).to_string() + } else { + format!("~{}", pyver.chars().next().unwrap()).to_string() + }; + assert!( + value.starts_with(&expected), + "{value} should start with {expected} to be valid for {pyver}" + ); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_option_injection(mut solver: StepSolver) { +async fn test_solver_option_injection(#[case] mut solver: SolverImpl) { // test the options that are defined when a package is resolved // - options are namespaced and added to the environment init_logging(); @@ -810,7 +895,7 @@ async fn test_solver_option_injection(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("vnp3")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); let mut opts = solution.options().clone(); assert_eq!(opts.remove(opt_name!("vnp3")), Some("~2.0.0".to_string())); @@ -831,8 +916,10 @@ async fn test_solver_option_injection(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_build_from_source(mut solver: StepSolver) { +async fn test_solver_build_from_source(#[case] mut solver: SolverImpl) { init_logging(); // test when no appropriate build exists but the source is available // - the build is skipped @@ -861,7 +948,10 @@ async fn test_solver_build_from_source(mut solver: StepSolver) { solver.add_request(request!({"var": "debug/on"})); solver.add_request(request!("my-tool")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver) + .await + .tap_err(|e| eprintln!("{e}")) + .unwrap(); let resolved = solution.get("my-tool").unwrap(); assert!( @@ -877,15 +967,18 @@ async fn test_solver_build_from_source(mut solver: StepSolver) { solver.set_binary_only(true); // Should fail when binary-only is specified - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!(res.is_err()); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_build_from_source_unsolvable(mut solver: StepSolver) { +async fn test_solver_build_from_source_unsolvable(#[case] mut solver: SolverImpl) { let log = init_logging(); + // test when no appropriate build exists but the source is available // - if the requested pkg cannot resolve a build environment // - this is flagged by the solver as impossible @@ -915,34 +1008,44 @@ async fn test_solver_build_from_source_unsolvable(mut solver: StepSolver) { repo.publish_recipe(&recipe).await.unwrap(); solver.add_repository(Arc::new(repo)); + solver.set_binary_only(false); // the new option value should disqualify the existing build // and there is no 6.3 that can be resolved for this request solver.add_request(request!({"var": "gcc/6.3"})); solver.add_request(request!("my-tool:run")); - let res = run_and_log_resolve_for_tests(&solver).await; + let res = run_and_log_resolve_for_tests(&mut solver).await; assert!(res.is_err(), "should fail to resolve"); - let log = log.lock(); - let event = log.all_events().find(|e| { - let Some(msg) = e.message() else { - return false; - }; - let msg = strip_ansi_escapes::strip(msg); - let msg = String::from_utf8_lossy(&msg); - msg.ends_with( - "TRY my-tool/1.2.0/src - cannot resolve build env for source build: Failed to resolve: there is no solution for these requests using the available packages", - ) - }); - assert!( - event.is_some(), - "should block because of failed build env resolve" - ); + + // This additional assert is specific to the output of the step solver. + // XXX The log isn't cleared between test cases, so this code would + // sometimes pass while testing the resolvo solver. There is no obvious way + // to clear the log. + if let SolverImpl::Step(_) = solver { + let log = log.lock(); + let event = log.all_events().find(|e| { + let Some(msg) = e.message() else { + return false; + }; + let msg = strip_ansi_escapes::strip(msg); + let msg = String::from_utf8_lossy(&msg); + msg.ends_with( + "TRY my-tool/1.2.0/src - cannot resolve build env for source build: Failed to resolve: there is no solution for these requests using the available packages", + ) + }); + assert!( + event.is_some(), + "should block because of failed build env resolve" + ); + } } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_build_from_source_dependency(mut solver: StepSolver) { +async fn test_solver_build_from_source_dependency(#[case] mut solver: SolverImpl) { // test when no appropriate build exists but the source is available // - the existing build is skipped // - the source package is checked for current options @@ -986,10 +1089,10 @@ async fn test_solver_build_from_source_dependency(mut solver: StepSolver) { // but a new one should be generated for this set of options solver.update_options(option_map! {"debug" => "on"}); solver.add_repository(Arc::new(repo)); - solver.add_request(request!("my-tool")); solver.set_binary_only(false); + solver.add_request(request!("my-tool")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert!( solution.get("my-tool").unwrap().is_source_build(), @@ -998,8 +1101,10 @@ async fn test_solver_build_from_source_dependency(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_deprecated_build(mut solver: StepSolver) { +async fn test_solver_deprecated_build(#[case] mut solver: SolverImpl) { let deprecated = make_build!({"pkg": "my-pkg/1.0.0", "deprecated": true}); let deprecated_build = deprecated.ident().clone(); let repo = make_repo!([ @@ -1012,7 +1117,7 @@ async fn test_solver_deprecated_build(mut solver: StepSolver) { solver.add_repository(repo.clone()); solver.add_request(request!("my-pkg")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!( solution, "my-pkg", @@ -1030,7 +1135,7 @@ async fn test_solver_deprecated_build(mut solver: StepSolver) { .into(), ); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!( solution, "my-pkg", @@ -1040,8 +1145,10 @@ async fn test_solver_deprecated_build(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_deprecated_version(mut solver: StepSolver) { +async fn test_solver_deprecated_version(#[case] mut solver: SolverImpl) { let deprecated = make_build!({"pkg": "my-pkg/1.0.0", "deprecated": true}); let repo = make_repo!( [{"pkg": "my-pkg/0.9.0"}, {"pkg": "my-pkg/1.0.0", "deprecated": true}, deprecated] @@ -1051,7 +1158,7 @@ async fn test_solver_deprecated_version(mut solver: StepSolver) { solver.add_repository(repo.clone()); solver.add_request(request!("my-pkg")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!( solution, "my-pkg", @@ -1069,7 +1176,7 @@ async fn test_solver_deprecated_version(mut solver: StepSolver) { .into(), ); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!( solution, "my-pkg", @@ -1079,8 +1186,10 @@ async fn test_solver_deprecated_version(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_build_from_source_deprecated(mut solver: StepSolver) { +async fn test_solver_build_from_source_deprecated(#[case] mut solver: SolverImpl) { // test when no appropriate build exists and the main package // has been deprecated, no source build should be allowed @@ -1106,13 +1215,17 @@ async fn test_solver_build_from_source_deprecated(mut solver: StepSolver) { repo.force_publish_recipe(&spec).await.unwrap(); solver.add_repository(Arc::new(repo)); + solver.set_binary_only(false); solver.add_request(request!({"var": "debug/on"})); solver.add_request(request!("my-tool")); - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; match res { + // step solver's error Err(Error::GraphError(ref graph_err)) if matches!(&**graph_err, spk_solve_graph::Error::FailedToResolve(_)) => {} + // resolvo solver's error + Err(Error::FailedToResolve(_)) => {} Err(err) => { panic!("expected solver spk_solver_graph::Error::FailedToResolve, got: '{err:?}'") } @@ -1121,9 +1234,11 @@ async fn test_solver_build_from_source_deprecated(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] async fn test_solver_build_from_source_deprecated_and_impossible_initial_checks( - mut solver: StepSolver, + #[case] mut solver: SolverImpl, ) { // test when no appropriate build exists and the main package // has been deprecated, no source build should be allowed @@ -1150,11 +1265,14 @@ async fn test_solver_build_from_source_deprecated_and_impossible_initial_checks( repo.force_publish_recipe(&spec).await.unwrap(); solver.add_repository(Arc::new(repo)); + solver.set_binary_only(false); solver.add_request(request!({"var": "debug/on"})); solver.add_request(request!("my-tool")); - solver.set_initial_request_impossible_checks(true); + if let SolverImpl::Step(ref mut solver) = solver { + solver.set_initial_request_impossible_checks(true); + } - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; match res { Err(Error::GraphError(ref graph_err)) if matches!(&**graph_err, spk_solve_graph::Error::FailedToResolve(_)) => @@ -1167,6 +1285,9 @@ async fn test_solver_build_from_source_deprecated_and_impossible_initial_checks( // recipe is deprecated and refuses to build a binary from // the source package. } + Err(Error::FailedToResolve(_)) => { + // Success; same as above, but for the resolvo solver. + } Err(Error::InitialRequestsContainImpossibleError(_)) => { // Success, when the 'migration-to-components' feature is // disabled because: the initial checks for impossible @@ -1182,8 +1303,10 @@ async fn test_solver_build_from_source_deprecated_and_impossible_initial_checks( } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_embedded_package_adds_request(mut solver: StepSolver) { +async fn test_solver_embedded_package_adds_request(#[case] mut solver: SolverImpl) { // test when there is an embedded package // - the embedded package is added to the solution // - the embedded package is also added as a request in the resolve @@ -1201,24 +1324,29 @@ async fn test_solver_embedded_package_adds_request(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("maya")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver) + .await + .tap_err(|e| eprintln!("{e}")) + .unwrap(); assert_resolved!( solution, "qt", - build = Build::Embedded(EmbeddedSource::Unknown) + build =~ Build::Embedded(_) ); assert_resolved!(solution, "qt", "5.12.6"); assert_resolved!( solution, "qt", - build = Build::Embedded(EmbeddedSource::Unknown) + build =~ Build::Embedded(_) ); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_embedded_package_solvable(mut solver: StepSolver) { +async fn test_solver_embedded_package_solvable(#[case] mut solver: SolverImpl) { // test when there is an embedded package // - the embedded package is added to the solution // - the embedded package resolves existing requests @@ -1242,19 +1370,21 @@ async fn test_solver_embedded_package_solvable(mut solver: StepSolver) { solver.add_request(request!("qt")); solver.add_request(request!("maya")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(solution, "qt", "5.12.6"); assert_resolved!( solution, "qt", - build = Build::Embedded(EmbeddedSource::Unknown) + build =~ Build::Embedded(_) ); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_embedded_package_unsolvable(mut solver: StepSolver) { +async fn test_solver_embedded_package_unsolvable(#[case] mut solver: SolverImpl) { // test when there is an embedded package // - the embedded package is added to the solution // - the embedded package conflicts with existing requests @@ -1281,13 +1411,15 @@ async fn test_solver_embedded_package_unsolvable(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("my-plugin")); - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!(res.is_err()); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_embedded_package_replaces_real_package(mut solver: StepSolver) { +async fn test_solver_embedded_package_replaces_real_package(#[case] mut solver: SolverImpl) { // test when there is an embedded package // - the embedded package is added to the solution // - any dependencies from the "real" package aren't part of the solution @@ -1330,7 +1462,7 @@ async fn test_solver_embedded_package_replaces_real_package(mut solver: StepSolv // "unwanted-dep" is added to solution. solver.add_request(request!("thing-needs-plugin")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); // At time of writing, this is a point where "unwanted-dep" is part of the // solution: @@ -1340,15 +1472,17 @@ async fn test_solver_embedded_package_replaces_real_package(mut solver: StepSolv assert_resolved!( solution, "qt", - build = Build::Embedded(EmbeddedSource::Unknown) + build =~ Build::Embedded(_) ); assert_not_resolved!(solution, "unwanted-dep"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] async fn test_solver_initial_request_impossible_masks_embedded_package_solution( - mut solver: StepSolver, + #[case] mut solver: SolverImpl, ) { // test when an embedded package and its parent package are // requested and impossible checks are enabled for initial @@ -1378,15 +1512,17 @@ async fn test_solver_initial_request_impossible_masks_embedded_package_solution( // requests work correctly. solver.add_request(request!("qt/5.12.6")); solver.add_request(request!("maya")); - solver.set_initial_request_impossible_checks(true); + if let SolverImpl::Step(ref mut solver) = solver { + solver.set_initial_request_impossible_checks(true); + } - match run_and_print_resolve_for_tests(&solver).await { + match run_and_print_resolve_for_tests(&mut solver).await { Ok(solution) => { assert_resolved!(solution, "qt", "5.12.6"); assert_resolved!( solution, "qt", - build = Build::Embedded(EmbeddedSource::Unknown) + build =~ Build::Embedded(_) ); } Err(err) => { @@ -1396,9 +1532,11 @@ async fn test_solver_initial_request_impossible_masks_embedded_package_solution( } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] async fn test_solver_impossible_request_but_embedded_package_makes_solvable( - mut solver: StepSolver, + #[case] mut solver: SolverImpl, ) { // test when there is an embedded package // - the initial request depends on the same package as the embedded package @@ -1446,7 +1584,9 @@ async fn test_solver_impossible_request_but_embedded_package_makes_solvable( solver.add_repository(Arc::new(repo)); solver.add_request(request!("needs")); - solver.set_resolve_validation_impossible_checks(true); + if let SolverImpl::Step(ref mut solver) = solver { + solver.set_resolve_validation_impossible_checks(true); + } // The solutions is: needs/1.0.0 -> something/2.4.0 -> maya/2019.2 (embeds qt/5.12.6) // -> somethingelse/3.2.1 ----------------------^ @@ -1458,13 +1598,13 @@ async fn test_solver_impossible_request_but_embedded_package_makes_solvable( // that point because the solver does not process all unresolved // requests before stopping and this is not an embedded package // cache for it to check. - match run_and_print_resolve_for_tests(&solver).await { + match run_and_print_resolve_for_tests(&mut solver).await { Ok(solution) => { assert_resolved!(solution, "qt", "5.12.6"); assert_resolved!( solution, "qt", - build = Build::Embedded(EmbeddedSource::Unknown) + build =~ Build::Embedded(_) ); } Err(err) => { @@ -1477,9 +1617,11 @@ async fn test_solver_impossible_request_but_embedded_package_makes_solvable( /// When multiple packages try to embed the same package the solver doesn't /// panic. #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] async fn test_multiple_packages_embed_same_package( - mut solver: StepSolver, + #[case] mut solver: SolverImpl, #[values(true, false)] resolve_validation_impossible_checks: bool, ) { init_logging(); @@ -1517,11 +1659,16 @@ async fn test_multiple_packages_embed_same_package( solver.add_repository(Arc::new(repo)); solver.add_request(request!("top-level")); - solver.set_resolve_validation_impossible_checks(resolve_validation_impossible_checks); + if let SolverImpl::Step(ref mut solver) = solver { + solver.set_resolve_validation_impossible_checks(resolve_validation_impossible_checks); + } - match run_and_print_resolve_for_tests(&solver).await { + match run_and_print_resolve_for_tests(&mut solver).await { + // step solver's error Err(Error::GraphError(ref graph_err)) if matches!(&**graph_err, spk_solve_graph::Error::FailedToResolve(_)) => {} + // resolvo solver's error + Err(Error::FailedToResolve(_)) => {} Ok(_) => { panic!("No solution expected"); } @@ -1532,8 +1679,10 @@ async fn test_multiple_packages_embed_same_package( } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_with_impossible_checks_in_build_keys(mut solver: StepSolver) { +async fn test_solver_with_impossible_checks_in_build_keys(#[case] mut solver: SolverImpl) { let options1 = option_map! {"dep" => "1.0.0"}; let options2 = option_map! {"dep" => "2.0.0"}; @@ -1563,16 +1712,20 @@ async fn test_solver_with_impossible_checks_in_build_keys(mut solver: StepSolver solver.add_request(request!("pkg-top")); // This is to exercise the check. The missing dep2 package will // ensure that the package that depends on dep1 is chosen. - solver.set_build_key_impossible_checks(true); + if let SolverImpl::Step(ref mut solver) = solver { + solver.set_build_key_impossible_checks(true); + } - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(packages, "pkg-a", "1.0.0"); assert_resolved!(packages, "dep", "1.0.0"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_some_versions_conflicting_requests(mut solver: StepSolver) { +async fn test_solver_some_versions_conflicting_requests(#[case] mut solver: SolverImpl) { // test when there is a package with some version that have a conflicting dependency // - the solver passes over the one with conflicting // - the solver logs compat info for versions with conflicts @@ -1602,14 +1755,16 @@ async fn test_solver_some_versions_conflicting_requests(mut solver: StepSolver) solver.add_repository(Arc::new(repo)); solver.add_request(request!("my-lib")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(solution, "dep", "2.0.0"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_embedded_request_invalidates(mut solver: StepSolver) { +async fn test_solver_embedded_request_invalidates(#[case] mut solver: SolverImpl) { // test when a package is resolved with an incompatible embedded pkg // - the solver tries to resolve the package // - there is a conflict in the embedded request @@ -1636,14 +1791,16 @@ async fn test_solver_embedded_request_invalidates(mut solver: StepSolver) { solver.add_request(request!("python")); solver.add_request(request!("my-lib")); - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!(res.is_err()); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_unknown_package_options(mut solver: StepSolver) { +async fn test_solver_unknown_package_options(#[case] mut solver: SolverImpl) { // test when a package is requested with specific options (eg: pkg.opt) // - the solver ignores versions that don't define the option // - the solver resolves versions that do define the option @@ -1656,19 +1813,21 @@ async fn test_solver_unknown_package_options(mut solver: StepSolver) { solver.add_request(request!({"var": "my-lib.something/value"})); solver.add_request(request!("my-lib")); - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!(res.is_err()); // this time we don't request that option, and it should be ok solver.reset(); solver.add_repository(repo); solver.add_request(request!("my-lib")); - run_and_print_resolve_for_tests(&solver).await.unwrap(); + run_and_print_resolve_for_tests(&mut solver).await.unwrap(); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_var_requirements(mut solver: StepSolver) { +async fn test_solver_var_requirements(#[case] mut solver: SolverImpl) { // test what happens when a dependency is added which is incompatible // with an existing request in the stack let repo = make_repo!( @@ -1700,7 +1859,7 @@ async fn test_solver_var_requirements(mut solver: StepSolver) { solver.add_repository(repo.clone()); solver.add_request(request!("my-app/2")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(solution, "my-app", "2.0.0"); assert_resolved!(solution, "python", "3.7.3"); @@ -1710,14 +1869,16 @@ async fn test_solver_var_requirements(mut solver: StepSolver) { solver.add_repository(repo); solver.add_request(request!("my-app/1")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(solution, "python", "2.7.5"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_var_requirements_unresolve(mut solver: StepSolver) { +async fn test_solver_var_requirements_unresolve(#[case] mut solver: SolverImpl) { // test when a package is resolved that conflicts in var requirements // - the solver should unresolve the solved package // - the solver should resolve a new version of the package with the right version @@ -1751,7 +1912,7 @@ async fn test_solver_var_requirements_unresolve(mut solver: StepSolver) { // the addition of this app constrains the python.abi to 2.7 solver.add_request(request!("my-app/1")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(solution, "my-app", "1.0.0"); assert_resolved!(solution, "python", "2.7.5", "should re-resolve python"); @@ -1763,15 +1924,17 @@ async fn test_solver_var_requirements_unresolve(mut solver: StepSolver) { // the addition of this app constrains the global abi to 2.7 solver.add_request(request!("my-app/2")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!(solution, "my-app", "2.0.0"); assert_resolved!(solution, "python", "2.7.5", "should re-resolve python"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_build_options_dont_affect_compat(mut solver: StepSolver) { +async fn test_solver_build_options_dont_affect_compat(#[case] mut solver: SolverImpl) { // test when a package is resolved with some build option // - that option can conflict with another packages build options // - as long as there is no explicit requirement on that option's value @@ -1803,7 +1966,7 @@ async fn test_solver_build_options_dont_affect_compat(mut solver: StepSolver) { // b is not affected and can still be resolved solver.add_request(request!("pkgb")); - run_and_print_resolve_for_tests(&solver).await.unwrap(); + run_and_print_resolve_for_tests(&mut solver).await.unwrap(); solver.reset(); solver.add_repository(repo.clone()); @@ -1813,13 +1976,15 @@ async fn test_solver_build_options_dont_affect_compat(mut solver: StepSolver) { // this time the explicit request will cause a failure solver.add_request(request!({"var": "build-dep/=1.0.0"})); - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!(res.is_err()); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_option_compat_intersection(mut solver: StepSolver) { +async fn test_solver_option_compat_intersection(#[case] mut solver: SolverImpl) { // A var option for spi-platform/~2022.4.1.4 should be able to resolve // with a build of openimageio that requires spi-platform/~2022.4.1.3. @@ -1851,12 +2016,14 @@ async fn test_solver_option_compat_intersection(mut solver: StepSolver) { solver.add_request(request!({"var": "spi-platform/~2022.4.1.4"})); solver.add_request(request!({"pkg": "openimageio"})); - let _ = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let _ = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); } #[rstest] +#[case::step(step_solver())] +// #[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_components(mut solver: StepSolver) { +async fn test_solver_components(#[case] mut solver: SolverImpl) { // test when a package is requested with specific components // - all the aggregated components are selected in the resolve // - the final build has published layers for each component @@ -1890,7 +2057,7 @@ async fn test_solver_components(mut solver: StepSolver) { solver.add_request(request!("pkga")); solver.add_request(request!("pkgb")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); let resolved = solution .get("python") @@ -1908,8 +2075,10 @@ async fn test_solver_components(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_components_interaction_with_embeds(mut solver: StepSolver) { +async fn test_solver_components_interaction_with_embeds(#[case] mut solver: SolverImpl) { // Test that a package can have a component that embeds a specific // component of some other package. This package must be included in a // solution to satisfy a request for that package+component combo. @@ -1970,7 +2139,10 @@ async fn test_solver_components_interaction_with_embeds(mut solver: StepSolver) solver.add_request(request!("fake-pkg:comp1")); solver.add_request(request!("victim")); - let Ok(solution) = run_and_print_resolve_for_tests(&solver).await else { + let Ok(solution) = run_and_print_resolve_for_tests(&mut solver) + .await + .tap_err(|e| eprintln!("{e}")) + else { panic!("Expected a valid solution"); }; @@ -1990,8 +2162,10 @@ async fn test_solver_components_interaction_with_embeds(mut solver: StepSolver) } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_components_when_no_components_requested(mut solver: StepSolver) { +async fn test_solver_components_when_no_components_requested(#[case] mut solver: SolverImpl) { // test when a package is requested with no components and the // package is one that has components // - the default component(s) should be the ones in the resolve @@ -2025,7 +2199,7 @@ async fn test_solver_components_when_no_components_requested(mut solver: StepSol solver.add_request(request!("pkga")); solver.add_request(request!("pkgb")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); let resolved = solution .get("python") @@ -2043,8 +2217,12 @@ async fn test_solver_components_when_no_components_requested(mut solver: StepSol } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_src_package_request_when_no_components_requested(mut solver: StepSolver) { +async fn test_solver_src_package_request_when_no_components_requested( + #[case] mut solver: SolverImpl, +) { // test when a /src package build is requested with no components // and a matching package with a /src package build exists in the repo // - the solver should resolve to the /src package build @@ -2063,7 +2241,7 @@ async fn test_solver_src_package_request_when_no_components_requested(mut solver let req = request!("mypkg/1.2.3/src"); solver.add_request(req); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); let resolved = solution.get("mypkg").unwrap().spec.ident().clone(); let expected = build_ident!("mypkg/1.2.3/src"); @@ -2071,8 +2249,10 @@ async fn test_solver_src_package_request_when_no_components_requested(mut solver } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_all_component(mut solver: StepSolver) { +async fn test_solver_all_component(#[case] mut solver: SolverImpl) { // test when a package is requested with the 'all' component // - all the specs components are selected in the resolve // - the final build has published layers for each component @@ -2096,7 +2276,7 @@ async fn test_solver_all_component(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("python:all")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); let resolved = solution.get("python").unwrap(); assert_eq!(resolved.request.pkg.components.len(), 1); @@ -2112,8 +2292,10 @@ async fn test_solver_all_component(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_component_availability(mut solver: StepSolver) { +async fn test_solver_component_availability(#[case] mut solver: SolverImpl) { // test when a package is requested with some component // - all the specs components are selected in the resolve // - the final build has published layers for each component @@ -2166,7 +2348,10 @@ async fn test_solver_component_availability(mut solver: StepSolver) { solver.add_repository(Arc::new(repo)); solver.add_request(request!("python:bin")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver) + .await + .tap_err(|e| eprintln!("{e}")) + .unwrap(); assert_resolved!( solution, @@ -2178,8 +2363,10 @@ async fn test_solver_component_availability(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_component_requirements(mut solver: StepSolver) { +async fn test_solver_component_requirements(#[case] mut solver: SolverImpl) { // test when a component has its own list of requirements // - the requirements are added to the existing set of requirements // - the additional requirements are resolved @@ -2208,7 +2395,7 @@ async fn test_solver_component_requirements(mut solver: StepSolver) { solver.add_repository(repo.clone()); solver.add_request(request!("mypkg:build")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); solution.get("dep").expect("should exist"); solution.get("depb").expect("should exist"); @@ -2218,7 +2405,7 @@ async fn test_solver_component_requirements(mut solver: StepSolver) { solver.add_repository(repo); solver.add_request(request!("mypkg:run")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); solution.get("dep").expect("should exist"); solution.get("depr").expect("should exist"); @@ -2226,8 +2413,10 @@ async fn test_solver_component_requirements(mut solver: StepSolver) { } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_component_requirements_extending(mut solver: StepSolver) { +async fn test_solver_component_requirements_extending(#[case] mut solver: SolverImpl) { // test when an additional component is requested after a package is resolved // - the new components requirements are still added and resolved @@ -2253,14 +2442,16 @@ async fn test_solver_component_requirements_extending(mut solver: StepSolver) { // has a new requirement on depc solver.add_request(request!("depb")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); solution.get("depc").expect("should exist"); } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_component_embedded(mut solver: StepSolver) { +async fn test_solver_component_embedded(#[case] mut solver: SolverImpl) { // test when a component has its own list of embedded packages // - the embedded package is immediately selected // - it must be compatible with any previous requirements @@ -2303,12 +2494,12 @@ async fn test_solver_component_embedded(mut solver: StepSolver) { solver.add_repository(repo.clone()); solver.add_request(request!("downstream1")); - let solution = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_resolved!( solution, "dep-e1", - build = Build::Embedded(EmbeddedSource::Unknown) + build =~ Build::Embedded(_) ); solver.reset(); @@ -2318,7 +2509,7 @@ async fn test_solver_component_embedded(mut solver: StepSolver) { // should fail because the one embedded package // does not meet the requirements in downstream spec - let res = run_and_print_resolve_for_tests(&solver).await; + let res = run_and_print_resolve_for_tests(&mut solver).await; assert!(res.is_err()); } @@ -2328,7 +2519,7 @@ async fn test_solver_component_embedded(mut solver: StepSolver) { #[case::comp2(&["mypkg:comp2", "dep-e1:comp2"], false)] #[tokio::test] async fn test_solver_component_embedded_component_requirements( - mut solver: StepSolver, + #[values(step_solver(), resolvo_solver())] mut solver: SolverImpl, #[case] packages_to_request: &[&str], #[case] expected_solve_result: bool, ) { @@ -2366,7 +2557,10 @@ async fn test_solver_component_embedded_component_requirements( solver.add_request(request!(package_to_request)); } - match run_and_print_resolve_for_tests(&solver).await { + match run_and_print_resolve_for_tests(&mut solver) + .await + .tap_err(|e| eprintln!("{e}")) + { Ok(solution) => { assert!(expected_solve_result, "expected solve to fail"); @@ -2384,7 +2578,7 @@ async fn test_solver_component_embedded_component_requirements( #[case::downstream3("downstream3", false)] #[tokio::test] async fn test_solver_component_embedded_multiple_versions( - mut solver: StepSolver, + #[values(step_solver(), resolvo_solver())] mut solver: SolverImpl, #[case] package_to_request: &str, #[case] expected_solve_result: bool, ) { @@ -2435,14 +2629,17 @@ async fn test_solver_component_embedded_multiple_versions( solver.add_repository(repo); solver.add_request(request!(package_to_request)); - match run_and_print_resolve_for_tests(&solver).await { + match run_and_print_resolve_for_tests(&mut solver) + .await + .tap_err(|e| eprintln!("{e}")) + { Ok(solution) => { assert!(expected_solve_result, "expected solve to fail"); assert_resolved!( solution, "dep-e1", - build = Build::Embedded(EmbeddedSource::Unknown) + build =~ Build::Embedded(_) ); } Err(_) => { @@ -2452,8 +2649,10 @@ async fn test_solver_component_embedded_multiple_versions( } #[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] #[tokio::test] -async fn test_solver_component_embedded_incompatible_requests(mut solver: StepSolver) { +async fn test_solver_component_embedded_incompatible_requests(#[case] mut solver: SolverImpl) { // test when different components of a package embedded packages that // make incompatible requests @@ -2484,7 +2683,7 @@ async fn test_solver_component_embedded_incompatible_requests(mut solver: StepSo solver.add_request(request!("mypkg:comp1")); solver.add_request(request!("mypkg:comp2")); - run_and_print_resolve_for_tests(&solver) + run_and_print_resolve_for_tests(&mut solver) .await .expect_err("expected solve to fail"); } @@ -2652,7 +2851,7 @@ fn test_problem_packages() { #[case::resolve_two_part_flavor("blue", "1.0")] #[tokio::test] async fn test_version_number_masking( - mut solver: StepSolver, + #[values(step_solver(), resolvo_solver())] mut solver: SolverImpl, #[case] color_to_solve_for: &str, #[case] expected_resolved_version: &str, #[values(RepoKind::Mem, RepoKind::Spfs)] repo: RepoKind, @@ -2723,7 +2922,7 @@ async fn test_version_number_masking( .into(), ); - let packages = run_and_print_resolve_for_tests(&solver).await.unwrap(); + let packages = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); assert_eq!(packages.len(), 1, "expected one resolved package"); let resolved = packages.get("my-pkg").unwrap(); assert_eq!( @@ -2732,3 +2931,30 @@ async fn test_version_number_masking( ); assert_ne!(resolved.spec.ident().build(), &Build::Source); } + +#[rstest] +#[case::step(step_solver())] +#[case::resolvo(resolvo_solver())] +#[tokio::test] +async fn request_for_all_component_picks_correct_version( + #[case] mut solver: SolverImpl, + #[values("1.0.0", "2.0.0", "3.0.0")] version: &str, +) { + // A request for :all component still controls for version compatibility + + let repo = make_repo!( + [ + { "pkg": "mypkg/1.0.0" }, + { "pkg": "mypkg/2.0.0" }, + { "pkg": "mypkg/3.0.0" }, + ] + ); + let repo = Arc::new(repo); + + solver.add_repository(repo); + let request_str = format!("mypkg:all/{version}"); + solver.add_request(request!(request_str)); + + let solution = run_and_print_resolve_for_tests(&mut solver).await.unwrap(); + assert_resolved!(solution, "mypkg", version = version); +} diff --git a/crates/spk-solve/src/solvers/step/solver.rs b/crates/spk-solve/src/solvers/step/solver.rs index 868472f812..29fc3b3a31 100644 --- a/crates/spk-solve/src/solvers/step/solver.rs +++ b/crates/spk-solve/src/solvers/step/solver.rs @@ -53,9 +53,10 @@ use spk_solve_validation::{ }; use spk_storage::RepositoryHandle; -use crate::error::OutOfOptions; +use crate::error::{self, OutOfOptions}; use crate::option_map::OptionMap; -use crate::{Error, Result, error}; +use crate::solver::Solver as SolverTrait; +use crate::{DecisionFormatter, Error, Result, SolverExt, SolverMut}; /// Structure to hold whether the three kinds of impossible checks are /// enabled or disabled in a solver. @@ -205,37 +206,6 @@ impl ErrorFreq { } impl Solver { - /// Add a request to this solver. - pub fn add_request(&mut self, request: Request) { - let request = match request { - Request::Pkg(mut request) => { - if request.pkg.components.is_empty() { - if request.pkg.is_source() { - request.pkg.components.insert(Component::Source); - } else { - request.pkg.components.insert(Component::default_for_run()); - } - } - Change::RequestPackage(RequestPackage::new(request)) - } - Request::Var(request) => Change::RequestVar(RequestVar::new(request)), - }; - self.initial_state_builders.push(request); - } - - /// Add a repository where the solver can get packages. - pub fn add_repository(&mut self, repo: R) - where - R: Into>, - { - self.repos.push(repo.into()); - } - - /// Return a reference to the solver's list of repositories. - pub fn repositories(&self) -> &Vec> { - &self.repos - } - pub fn get_initial_state(&self) -> Arc { let mut state = None; let base = State::default_state(); @@ -1049,63 +1019,11 @@ impl Solver { Ok(()) } - /// Put this solver back into its default state - pub fn reset(&mut self) { - self.repos.truncate(0); - self.initial_state_builders.truncate(0); - self.validators = Cow::from(default_validators()); - (*self.request_validator).reset(); - - self.number_of_steps = 0; - self.number_builds_skipped = 0; - self.number_incompat_versions = 0; - self.number_incompat_builds = 0; - self.number_total_builds = 0; - self.number_of_steps_back.store(0, Ordering::SeqCst); - self.error_frequency.clear(); - self.problem_packages.clear(); - } - /// Run this solver pub fn run(&self) -> SolverRuntime { SolverRuntime::new(self.clone()) } - /// If true, only solve pre-built binary packages. - /// - /// When false, the solver may return packages where the build is not set. - /// These packages are known to have a source package available, and the requested - /// options are valid for a new build of that source package. - /// These packages are not actually built as part of the solver process but their - /// build environments are fully resolved and dependencies included - pub fn set_binary_only(&mut self, binary_only: bool) { - self.request_validator.set_binary_only(binary_only); - - let has_binary_only = self - .validators - .iter() - .find_map(|v| match v { - Validators::BinaryOnly(_) => Some(true), - _ => None, - }) - .unwrap_or(false); - if !(has_binary_only ^ binary_only) { - return; - } - if binary_only { - // Add BinaryOnly validator because it was missing. - self.validators - .to_mut() - .insert(0, Validators::BinaryOnly(BinaryOnlyValidator {})) - } else { - // Remove all BinaryOnly validators because one was found. - self.validators = take(self.validators.to_mut()) - .into_iter() - .filter(|v| !matches!(v, Validators::BinaryOnly(_))) - .collect(); - } - } - /// Enable or disable running impossible checks on the initial requests /// before the solve starts pub fn set_initial_request_impossible_checks(&mut self, enabled: bool) { @@ -1132,43 +1050,12 @@ impl Solver { || self.impossible_checks.use_in_build_keys } - pub async fn solve(&mut self) -> Result { - let mut runtime = self.run(); - { - let iter = runtime.iter(); - tokio::pin!(iter); - while let Some(_step) = iter.try_next().await? {} - } - runtime.current_solution().await - } - - /// Adds requests for all build requirements - pub fn configure_for_build_environment(&mut self, recipe: &T) -> Result<()> { - let state = self.get_initial_state(); - - let build_options = recipe.resolve_options(state.get_option_map())?; - for req in recipe - .get_build_requirements(&build_options)? - .iter() - .cloned() - { - self.add_request(req) - } - - Ok(()) - } - /// Adds requests for all build requirements and solves pub async fn solve_build_environment(&mut self, recipe: &SpecRecipe) -> Result { self.configure_for_build_environment(recipe)?; self.solve().await } - pub fn update_options(&mut self, options: OptionMap) { - self.initial_state_builders - .push(Change::SetOptions(SetOptions::new(options))) - } - /// Get the number of steps (forward) taken in the solve pub fn get_number_of_steps(&self) -> usize { self.number_of_steps @@ -1200,6 +1087,131 @@ impl Solver { } } +impl SolverTrait for Solver { + fn get_options(&self) -> Cow<'_, OptionMap> { + Cow::Owned(self.get_initial_state().get_option_map().clone()) + } + + fn get_pkg_requests(&self) -> Vec { + self.get_initial_state() + .get_pkg_requests() + .iter() + .map(|pkg_request| (***pkg_request).clone()) + .collect() + } + + fn get_var_requests(&self) -> Vec { + self.get_initial_state() + .get_var_requests() + .iter() + .cloned() + .collect() + } + + fn repositories(&self) -> &[Arc] { + &self.repos + } +} + +#[async_trait::async_trait] +impl SolverMut for Solver { + fn add_request(&mut self, request: Request) { + let request = match request { + Request::Pkg(mut request) => { + if request.pkg.components.is_empty() { + if request.pkg.is_source() { + request.pkg.components.insert(Component::Source); + } else { + request.pkg.components.insert(Component::default_for_run()); + } + } + Change::RequestPackage(RequestPackage::new(request)) + } + Request::Var(request) => Change::RequestVar(RequestVar::new(request)), + }; + self.initial_state_builders.push(request); + } + + fn reset(&mut self) { + self.repos.truncate(0); + self.initial_state_builders.truncate(0); + self.validators = Cow::from(default_validators()); + (*self.request_validator).reset(); + + self.number_of_steps = 0; + self.number_builds_skipped = 0; + self.number_incompat_versions = 0; + self.number_incompat_builds = 0; + self.number_total_builds = 0; + self.number_of_steps_back.store(0, Ordering::SeqCst); + self.error_frequency.clear(); + self.problem_packages.clear(); + } + + async fn run_and_log_resolve(&mut self, formatter: &DecisionFormatter) -> Result { + let (solution, _graph) = formatter.run_and_log_resolve(self).await?; + Ok(solution) + } + + async fn run_and_print_resolve(&mut self, formatter: &DecisionFormatter) -> Result { + let (solution, _graph) = formatter.run_and_print_resolve(self).await?; + Ok(solution) + } + + fn set_binary_only(&mut self, binary_only: bool) { + self.request_validator.set_binary_only(binary_only); + + let has_binary_only = self + .validators + .iter() + .find_map(|v| match v { + Validators::BinaryOnly(_) => Some(true), + _ => None, + }) + .unwrap_or(false); + if !(has_binary_only ^ binary_only) { + return; + } + if binary_only { + // Add BinaryOnly validator because it was missing. + self.validators + .to_mut() + .insert(0, Validators::BinaryOnly(BinaryOnlyValidator {})) + } else { + // Remove all BinaryOnly validators because one was found. + self.validators = take(self.validators.to_mut()) + .into_iter() + .filter(|v| !matches!(v, Validators::BinaryOnly(_))) + .collect(); + } + } + + async fn solve(&mut self) -> Result { + let mut runtime = self.run(); + { + let iter = runtime.iter(); + tokio::pin!(iter); + while let Some(_step) = iter.try_next().await? {} + } + runtime.current_solution().await + } + + fn update_options(&mut self, options: OptionMap) { + self.initial_state_builders + .push(Change::SetOptions(SetOptions::new(options))) + } +} + +#[async_trait::async_trait] +impl SolverExt for Solver { + fn add_repository(&mut self, repo: R) + where + R: Into>, + { + self.repos.push(repo.into()); + } +} + // This is needed so `PriorityQueue` doesn't need to hash the node itself. struct NodeWrapper { pub(crate) node: Arc>>, diff --git a/cspell.json b/cspell.json index af53298b08..ad9ebab414 100644 --- a/cspell.json +++ b/cspell.json @@ -67,12 +67,14 @@ "builddir", "buildenv", "BUILDGST", + "buildid", "BUILDKEY", "buildroot", "buildvariabledescription", "canonicalize", "canonicalized", "CCAA", + "cdcl", "CFLAGS", "chdir", "CHDIR", @@ -303,6 +305,7 @@ "INLINES", "inodes", "insertcopying", + "interner", "intree", "inutes", "invalidapi", @@ -591,6 +594,7 @@ "repr", "reqby", "reqs", + "resolvo", "respecifying", "retpoline", "retryable", @@ -643,6 +647,7 @@ "SIGWINCH", "SIMD", "SNAPPROCESS", + "solvables", "somedata", "SOMEDIGEST", "somedir",