mirror of
https://codeberg.org/Toasterson/ips.git
synced 2026-04-10 13:20:42 +00:00
implement dependency solver based on resolvo
This commit is contained in:
parent
287b692920
commit
28a18c088a
4 changed files with 1050 additions and 0 deletions
171
Cargo.lock
generated
171
Cargo.lock
generated
|
|
@ -17,6 +17,19 @@ version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
|
@ -165,6 +178,18 @@ version = "2.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -316,6 +341,15 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "concurrent-queue"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-random"
|
name = "const-random"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
|
|
@ -370,6 +404,12 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy"
|
name = "crunchy"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|
@ -473,6 +513,15 @@ version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elsa"
|
||||||
|
version = "1.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
|
@ -498,12 +547,29 @@ dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener"
|
||||||
|
version = "5.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -544,6 +610,26 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "funty"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
|
@ -551,6 +637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -585,6 +672,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
|
@ -969,6 +1057,15 @@ version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
|
@ -1023,6 +1120,7 @@ dependencies = [
|
||||||
"redb",
|
"redb",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"resolvo",
|
||||||
"rust-ini",
|
"rust-ini",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1392,6 +1490,12 @@ version = "4.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
|
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
|
@ -1442,6 +1546,16 @@ dependencies = [
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "petgraph"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||||
|
dependencies = [
|
||||||
|
"fixedbitset",
|
||||||
|
"indexmap 2.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
|
@ -1580,6 +1694,12 @@ version = "5.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radium"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redb"
|
name = "redb"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
|
|
@ -1688,6 +1808,22 @@ dependencies = [
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "resolvo"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "014783b06e2d02bee01fe3c3247454fb34d0fc35765334e825034cdadec422fa"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"bitvec",
|
||||||
|
"elsa",
|
||||||
|
"event-listener",
|
||||||
|
"futures",
|
||||||
|
"itertools",
|
||||||
|
"petgraph",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
|
|
@ -2124,6 +2260,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tap"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.20.0"
|
version = "3.20.0"
|
||||||
|
|
@ -2935,6 +3077,15 @@ version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyz"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||||
|
dependencies = [
|
||||||
|
"tap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -2967,6 +3118,26 @@ dependencies = [
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ redb = "1.5.0"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
rust-ini = "0.21.2"
|
rust-ini = "0.21.2"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||||
|
resolvo = "0.7"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["redb-index"]
|
default = ["redb-index"]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub mod fmri;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
pub mod payload;
|
pub mod payload;
|
||||||
pub mod repository;
|
pub mod repository;
|
||||||
|
pub mod solver;
|
||||||
mod test_json_manifest;
|
mod test_json_manifest;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
877
libips/src/solver/mod.rs
Normal file
877
libips/src/solver/mod.rs
Normal file
|
|
@ -0,0 +1,877 @@
|
||||||
|
// This Source Code Form is subject to the terms of
|
||||||
|
// the Mozilla Public License, v. 2.0. If a copy of the
|
||||||
|
// MPL was not distributed with this file, You can
|
||||||
|
// obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
//! Minimal dependency resolution and planning over the ImageCatalog.
|
||||||
|
//! This module contains an MVP greedy resolver and a Catalog-backed provider
|
||||||
|
//! that is structured after resolvo's DependencyProvider. The provider encodes
|
||||||
|
//! IPS-specific selection rules so we can plug it into resolvo with minimal
|
||||||
|
//! changes later:
|
||||||
|
//! - Package identity is the IPS stem (name) and the publisher.
|
||||||
|
//! - Prefer the same publisher as the dependant; if not available, try the
|
||||||
|
//! publishers in the image search order; finally fall back to the image's
|
||||||
|
//! default publisher.
|
||||||
|
//! - Ignore obsolete packages when enumerating candidates.
|
||||||
|
//! - While resolving a dependency, restrict candidates to the same branch as
|
||||||
|
//! the dependant (required by IPS). If no candidate exists on that branch,
|
||||||
|
//! resolution fails with a missing dependency.
|
||||||
|
//! - When a dependency expression carries a version, we match by the main
|
||||||
|
//! release component of the version (the part before branch/build/timestamp).
|
||||||
|
//! - Candidate ordering picks the newest release first; if releases are equal,
|
||||||
|
//! the candidate with the newest timestamp wins.
|
||||||
|
//!
|
||||||
|
//! The public API resolve_install keeps returning an InstallPlan and internally
|
||||||
|
//! uses the provider to choose candidates. Swapping the greedy traversal for
|
||||||
|
//! resolvo::Solver will only require wiring this provider into resolvo.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
|
|
||||||
|
// Begin resolvo wiring imports (names discovered by compiler)
|
||||||
|
// We start broad and refine with compiler guidance.
|
||||||
|
use resolvo::{self, Candidates, Dependencies as RDependencies, DependencyProvider, Interner, KnownDependencies, Mapping, NameId, Problem as RProblem, Requirement as RRequirement, Solver as RSolver, SolverCache, SolvableId, StringId, UnsolvableOrCancelled, VersionSetId, VersionSetUnionId};
|
||||||
|
|
||||||
|
use miette::Diagnostic;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
use crate::actions::{Dependency, Manifest};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct PkgCand {
|
||||||
|
id: SolvableId,
|
||||||
|
name_id: NameId,
|
||||||
|
fmri: Fmri,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum VersionSetKind {
|
||||||
|
Any,
|
||||||
|
ReleaseEq(String),
|
||||||
|
BranchEq(String),
|
||||||
|
ReleaseAndBranch { release: String, branch: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IpsProvider<'a> {
|
||||||
|
image: &'a Image,
|
||||||
|
catalog: CatalogProvider<'a>,
|
||||||
|
// interner storages
|
||||||
|
names: Mapping<NameId, String>,
|
||||||
|
name_by_str: BTreeMap<String, NameId>,
|
||||||
|
strings: Mapping<StringId, String>,
|
||||||
|
solvables: Mapping<SolvableId, PkgCand>,
|
||||||
|
cands_by_name: HashMap<NameId, Vec<SolvableId>>,
|
||||||
|
// Version set storage needs interior mutability to allocate during async trait calls
|
||||||
|
version_sets: RefCell<Mapping<VersionSetId, VersionSetKind>>,
|
||||||
|
vs_name: RefCell<Mapping<VersionSetId, NameId>>,
|
||||||
|
unions: RefCell<Mapping<VersionSetUnionId, Vec<VersionSetId>>>,
|
||||||
|
// per-name publisher preference order; set by dependency processing or top-level specs
|
||||||
|
publisher_prefs: RefCell<HashMap<NameId, Vec<String>>>,
|
||||||
|
}
|
||||||
|
use crate::fmri::{Fmri, Version};
|
||||||
|
use crate::image::{catalog::PackageInfo, Image};
|
||||||
|
|
||||||
|
impl<'a> IpsProvider<'a> {
|
||||||
|
fn new(image: &'a Image) -> Result<Self, SolverError> {
|
||||||
|
let mut prov = IpsProvider {
|
||||||
|
image,
|
||||||
|
catalog: CatalogProvider::new(image)?,
|
||||||
|
names: Mapping::default(),
|
||||||
|
name_by_str: BTreeMap::new(),
|
||||||
|
strings: Mapping::default(),
|
||||||
|
solvables: Mapping::default(),
|
||||||
|
cands_by_name: HashMap::new(),
|
||||||
|
version_sets: RefCell::new(Mapping::default()),
|
||||||
|
vs_name: RefCell::new(Mapping::default()),
|
||||||
|
unions: RefCell::new(Mapping::default()),
|
||||||
|
publisher_prefs: RefCell::new(HashMap::new()),
|
||||||
|
};
|
||||||
|
prov.build_index();
|
||||||
|
Ok(prov)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_index(&mut self) {
|
||||||
|
// Take a snapshot of the catalog to avoid borrow conflicts while interning
|
||||||
|
let snapshot: Vec<(String, Vec<PackageInfo>)> = self
|
||||||
|
.catalog
|
||||||
|
.cache
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect();
|
||||||
|
for (stem, list) in snapshot {
|
||||||
|
let name_id = self.intern_name(&stem);
|
||||||
|
let mut ids: Vec<SolvableId> = Vec::new();
|
||||||
|
for pkg in &list {
|
||||||
|
// allocate next solvable id based on current len
|
||||||
|
let sid = SolvableId(self.solvables.len() as u32);
|
||||||
|
self.solvables.insert(
|
||||||
|
sid,
|
||||||
|
PkgCand {
|
||||||
|
id: sid,
|
||||||
|
name_id,
|
||||||
|
fmri: pkg.fmri.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ids.push(sid);
|
||||||
|
}
|
||||||
|
// Ensure deterministic initial order: newest first by IPS ordering
|
||||||
|
ids.sort_by(|a, b| {
|
||||||
|
let fa = &self.solvables.get(*a).unwrap().fmri;
|
||||||
|
let fb = &self.solvables.get(*b).unwrap().fmri;
|
||||||
|
version_order_desc(fa, fb)
|
||||||
|
});
|
||||||
|
self.cands_by_name.insert(name_id, ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intern_name(&mut self, name: &str) -> NameId {
|
||||||
|
if let Some(id) = self.name_by_str.get(name).copied() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
let id = NameId(self.names.len() as u32);
|
||||||
|
self.names.insert(id, name.to_string());
|
||||||
|
self.name_by_str.insert(name.to_string(), id);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_set_for(&self, name: NameId, kind: VersionSetKind) -> VersionSetId {
|
||||||
|
let vs_id = VersionSetId(self.version_sets.borrow().len() as u32);
|
||||||
|
self.version_sets.borrow_mut().insert(vs_id, kind);
|
||||||
|
self.vs_name.borrow_mut().insert(vs_id, name);
|
||||||
|
vs_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Interner for IpsProvider<'a> {
|
||||||
|
fn display_solvable(&self, solvable: SolvableId) -> impl std::fmt::Display + '_ {
|
||||||
|
let fmri = &self.solvables.get(solvable).unwrap().fmri;
|
||||||
|
fmri.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name(&self, name: NameId) -> impl std::fmt::Display + '_ {
|
||||||
|
self.names.get(name).cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_version_set(&self, version_set: VersionSetId) -> impl std::fmt::Display + '_ {
|
||||||
|
match self.version_sets.borrow().get(version_set) {
|
||||||
|
Some(VersionSetKind::Any) => "any".to_string(),
|
||||||
|
Some(VersionSetKind::ReleaseEq(r)) => format!("release={}", r),
|
||||||
|
Some(VersionSetKind::BranchEq(b)) => format!("branch={}", b),
|
||||||
|
Some(VersionSetKind::ReleaseAndBranch { release, branch }) =>
|
||||||
|
format!("release={}, branch={}", release, branch),
|
||||||
|
None => "<unknown>".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_string(&self, string_id: StringId) -> impl std::fmt::Display + '_ {
|
||||||
|
self.strings.get(string_id).cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_set_name(&self, version_set: VersionSetId) -> NameId {
|
||||||
|
*self.vs_name.borrow().get(version_set).expect("version set name present")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solvable_name(&self, solvable: SolvableId) -> NameId {
|
||||||
|
self.solvables.get(solvable).unwrap().name_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_sets_in_union(
|
||||||
|
&self,
|
||||||
|
version_set_union: VersionSetUnionId,
|
||||||
|
) -> impl Iterator<Item = VersionSetId> {
|
||||||
|
self.unions
|
||||||
|
.borrow()
|
||||||
|
.get(version_set_union)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to evaluate if a candidate FMRI matches a VersionSetKind constraint
|
||||||
|
fn fmri_matches_version_set(fmri: &Fmri, kind: &VersionSetKind) -> bool {
|
||||||
|
match kind {
|
||||||
|
VersionSetKind::Any => true,
|
||||||
|
VersionSetKind::ReleaseEq(req_rel) => fmri
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| &v.release == req_rel)
|
||||||
|
.unwrap_or(false),
|
||||||
|
VersionSetKind::BranchEq(req_branch) => fmri
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v.branch.as_ref())
|
||||||
|
.map(|b| b == req_branch)
|
||||||
|
.unwrap_or(false),
|
||||||
|
VersionSetKind::ReleaseAndBranch { release, branch } => fmri
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| &v.release == release)
|
||||||
|
.unwrap_or(false)
|
||||||
|
&& fmri
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v.branch.as_ref())
|
||||||
|
.map(|b| b == branch)
|
||||||
|
.unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
impl<'a> DependencyProvider for IpsProvider<'a> {
|
||||||
|
async fn filter_candidates(
|
||||||
|
&self,
|
||||||
|
candidates: &[SolvableId],
|
||||||
|
version_set: VersionSetId,
|
||||||
|
inverse: bool,
|
||||||
|
) -> Vec<SolvableId> {
|
||||||
|
let kind = self
|
||||||
|
.version_sets
|
||||||
|
.borrow()
|
||||||
|
.get(version_set)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(VersionSetKind::Any);
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|sid| {
|
||||||
|
let fmri = &self.solvables.get(*sid).unwrap().fmri;
|
||||||
|
let m = fmri_matches_version_set(fmri, &kind);
|
||||||
|
if inverse { !m } else { m }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_candidates(&self, name: NameId) -> Option<Candidates> {
|
||||||
|
let list = self.cands_by_name.get(&name)?;
|
||||||
|
Some(Candidates {
|
||||||
|
candidates: list.clone(),
|
||||||
|
favored: None,
|
||||||
|
locked: None,
|
||||||
|
hint_dependencies_available: vec![],
|
||||||
|
excluded: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sort_candidates(&self, _solver: &SolverCache<Self>, solvables: &mut [SolvableId]) {
|
||||||
|
// Determine publisher preference order for this name
|
||||||
|
let name_id = if solvables.is_empty() {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
self.solvable_name(solvables[0])
|
||||||
|
};
|
||||||
|
let prefs_opt = self.publisher_prefs.borrow().get(&name_id).cloned();
|
||||||
|
let pub_order = prefs_opt.unwrap_or_else(|| build_publisher_preference(None, self.image));
|
||||||
|
|
||||||
|
let idx_of = |pubname: &str| -> usize {
|
||||||
|
pub_order
|
||||||
|
.iter()
|
||||||
|
.position(|p| p == pubname)
|
||||||
|
.unwrap_or(usize::MAX)
|
||||||
|
};
|
||||||
|
|
||||||
|
solvables.sort_by(|a, b| {
|
||||||
|
let fa = &self.solvables.get(*a).unwrap().fmri;
|
||||||
|
let fb = &self.solvables.get(*b).unwrap().fmri;
|
||||||
|
// First: compare releases only
|
||||||
|
let rel_ord = cmp_release_desc(fa, fb);
|
||||||
|
if rel_ord != std::cmp::Ordering::Equal {
|
||||||
|
return rel_ord;
|
||||||
|
}
|
||||||
|
// If same release: prefer publisher order
|
||||||
|
let ia = fa.publisher.as_deref().map(idx_of).unwrap_or(usize::MAX);
|
||||||
|
let ib = fb.publisher.as_deref().map(idx_of).unwrap_or(usize::MAX);
|
||||||
|
if ia != ib {
|
||||||
|
return ia.cmp(&ib);
|
||||||
|
}
|
||||||
|
// Same publisher: prefer newest timestamp
|
||||||
|
version_order_desc(fa, fb)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_dependencies(&self, solvable: SolvableId) -> RDependencies {
|
||||||
|
let pkg = self.solvables.get(solvable).unwrap();
|
||||||
|
let fmri = &pkg.fmri;
|
||||||
|
let manifest_opt = match self.image.get_manifest_from_catalog(fmri) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
let Some(manifest) = manifest_opt else {
|
||||||
|
return RDependencies::Known(KnownDependencies::default());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build requirements for "require" deps
|
||||||
|
let mut reqs: Vec<RRequirement> = Vec::new();
|
||||||
|
let parent_branch = fmri
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v.branch.clone());
|
||||||
|
let parent_pub = fmri.publisher.as_deref();
|
||||||
|
|
||||||
|
for d in manifest.dependencies.iter().filter(|d| d.dependency_type == "require") {
|
||||||
|
if let Some(df) = &d.fmri {
|
||||||
|
let stem = df.stem().to_string();
|
||||||
|
let Some(child_name_id) = self.name_by_str.get(&stem).copied() else {
|
||||||
|
// If the dependency name isn't present in the catalog index, skip it
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// Create version set by release (from dep expr) and branch (from parent)
|
||||||
|
let vs_kind = match (&df.version, &parent_branch) {
|
||||||
|
(Some(ver), Some(branch)) => VersionSetKind::ReleaseAndBranch {
|
||||||
|
release: ver.release.clone(),
|
||||||
|
branch: branch.clone(),
|
||||||
|
},
|
||||||
|
(Some(ver), None) => VersionSetKind::ReleaseEq(ver.release.clone()),
|
||||||
|
(None, Some(branch)) => VersionSetKind::BranchEq(branch.clone()),
|
||||||
|
(None, None) => VersionSetKind::Any,
|
||||||
|
};
|
||||||
|
let vs_id = self.version_set_for(child_name_id, vs_kind);
|
||||||
|
reqs.push(RRequirement::from(vs_id));
|
||||||
|
|
||||||
|
// Set publisher preferences for the child to parent-first, then image order
|
||||||
|
let order = build_publisher_preference(parent_pub, self.image);
|
||||||
|
self.publisher_prefs
|
||||||
|
.borrow_mut()
|
||||||
|
.entry(child_name_id)
|
||||||
|
.or_insert(order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RDependencies::Known(KnownDependencies {
|
||||||
|
requirements: reqs,
|
||||||
|
constrains: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
#[error("Solver error: {message}")]
|
||||||
|
#[diagnostic(
|
||||||
|
code(ips::solver_error::generic),
|
||||||
|
help("Check package names and repository catalogs. Use 'pkg6 image catalog --dump' for debugging.")
|
||||||
|
)]
|
||||||
|
pub struct SolverError {
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SolverError {
|
||||||
|
fn new(msg: impl Into<String>) -> Self { Self { message: msg.into() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ResolvedPkg {
|
||||||
|
pub fmri: Fmri,
|
||||||
|
pub manifest: Manifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct InstallPlan {
|
||||||
|
pub add: Vec<ResolvedPkg>,
|
||||||
|
pub remove: Vec<ResolvedPkg>,
|
||||||
|
pub update: Vec<(ResolvedPkg, ResolvedPkg)>,
|
||||||
|
pub reasons: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Constraint {
|
||||||
|
pub stem: String,
|
||||||
|
// If present, this holds the main release component to match (e.g., "1.18.0" or "5.11").
|
||||||
|
// IPS dependency expressions should be matched by their main release, not the full
|
||||||
|
// branch/build/timestamp string.
|
||||||
|
pub version_req: Option<String>,
|
||||||
|
// Preferred publishers in order of priority. When multiple candidates have the same
|
||||||
|
// best release and timestamp, we pick the first matching publisher in this list.
|
||||||
|
pub preferred_publishers: Vec<String>,
|
||||||
|
// When resolving a dependency, enforce staying on the dependant's branch.
|
||||||
|
pub branch: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Catalog-backed provider for candidates. Filters out obsolete packages.
|
||||||
|
struct CatalogProvider<'a> {
|
||||||
|
image: &'a Image,
|
||||||
|
// cache: stem -> list of non-obsolete PackageInfo
|
||||||
|
cache: BTreeMap<String, Vec<PackageInfo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CatalogProvider<'a> {
|
||||||
|
fn new(image: &'a Image) -> Result<Self, SolverError> {
|
||||||
|
let mut prov = Self { image, cache: BTreeMap::new() };
|
||||||
|
prov.rebuild_cache()?;
|
||||||
|
Ok(prov)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_cache(&mut self) -> Result<(), SolverError> {
|
||||||
|
let pkgs = self.image
|
||||||
|
.query_catalog(None)
|
||||||
|
.map_err(|e| SolverError::new(format!("catalog query failed: {e}")))?;
|
||||||
|
let mut m: BTreeMap<String, Vec<PackageInfo>> = BTreeMap::new();
|
||||||
|
for p in pkgs.into_iter().filter(|p| !p.obsolete) {
|
||||||
|
m.entry(p.fmri.stem().to_string()).or_default().push(p);
|
||||||
|
}
|
||||||
|
// Sort each stem's candidates by version descending (highest first)
|
||||||
|
for v in m.values_mut() {
|
||||||
|
v.sort_by(|a, b| cmp_version_desc(&a.fmri, &b.fmri));
|
||||||
|
}
|
||||||
|
self.cache = m;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the best candidate PackageInfo for a constraint, applying IPS rules:
|
||||||
|
/// 1) Filter out candidates not on the required branch (if specified).
|
||||||
|
/// 2) If a version_req (release) is given, restrict to that release.
|
||||||
|
/// 3) Order by publisher preference (preferred_publishers) if provided.
|
||||||
|
/// 4) Choose newest release; if release equal, choose newest timestamp.
|
||||||
|
fn best_match(&self, c: &Constraint) -> Option<&PackageInfo> {
|
||||||
|
let list = self.cache.get(&c.stem)?;
|
||||||
|
|
||||||
|
// Filter by branch if specified
|
||||||
|
let mut filtered: Vec<&PackageInfo> = list
|
||||||
|
.iter()
|
||||||
|
.filter(|p| match (&c.branch, &p.fmri.version) {
|
||||||
|
(Some(branch_req), Some(ver)) => ver.branch.as_ref().map(|b| b == branch_req).unwrap_or(false),
|
||||||
|
(Some(_), None) => false,
|
||||||
|
(None, _) => true,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// If a version (release) is required, keep only matching releases
|
||||||
|
if let Some(release_req) = &c.version_req {
|
||||||
|
filtered.retain(|p| match &p.fmri.version {
|
||||||
|
Some(ver) => &ver.release == release_req,
|
||||||
|
None => false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if filtered.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by IPS ordering: newest release, then newest timestamp
|
||||||
|
filtered.sort_by(|a, b| version_order_desc(&a.fmri, &b.fmri));
|
||||||
|
|
||||||
|
// Apply publisher preference: find first candidate matching the preferred publishers order
|
||||||
|
if !c.preferred_publishers.is_empty() {
|
||||||
|
for pref in &c.preferred_publishers {
|
||||||
|
if let Some(pkg) = filtered.iter().find(|p| &p.publisher == pref) {
|
||||||
|
return Some(pkg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to the top candidate
|
||||||
|
filtered.first().copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_version_req<'a>(cands: Vec<&'a PackageInfo>, ver_req: &str) -> Option<&'a PackageInfo> {
|
||||||
|
// Deprecated: kept for compatibility in case external callers rely on it.
|
||||||
|
let req = ver_req.trim_start_matches('=');
|
||||||
|
for c in &cands {
|
||||||
|
if c.fmri.version() == req { return Some(*c); }
|
||||||
|
}
|
||||||
|
cands.first().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmp_version_desc(a: &Fmri, b: &Fmri) -> std::cmp::Ordering {
|
||||||
|
// Basic descending order by stringified version as a fallback for cache sorting.
|
||||||
|
a.version().cmp(&b.version()).reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IPS-specific comparison: newest release first; if equal, newest timestamp.
|
||||||
|
fn cmp_release_desc(a: &Fmri, b: &Fmri) -> std::cmp::Ordering {
|
||||||
|
let a_rel = a.version.as_ref();
|
||||||
|
let b_rel = b.version.as_ref();
|
||||||
|
match (a_rel, b_rel) {
|
||||||
|
(Some(va), Some(vb)) => match (va.release_to_semver(), vb.release_to_semver()) {
|
||||||
|
(Ok(ra), Ok(rb)) => ra.cmp(&rb).reverse(),
|
||||||
|
_ => va.release.cmp(&vb.release).reverse(),
|
||||||
|
},
|
||||||
|
(Some(_), None) => std::cmp::Ordering::Less,
|
||||||
|
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||||
|
(None, None) => std::cmp::Ordering::Equal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_order_desc(a: &Fmri, b: &Fmri) -> std::cmp::Ordering {
|
||||||
|
// Compare by release (semver padded) if possible
|
||||||
|
let a_rel = a.version.clone();
|
||||||
|
let b_rel = b.version.clone();
|
||||||
|
|
||||||
|
match (&a_rel, &b_rel) {
|
||||||
|
(Some(va), Some(vb)) => {
|
||||||
|
// Compare release using semver padded via Version::release_to_semver
|
||||||
|
let rel_cmp = match (va.release_to_semver(), vb.release_to_semver()) {
|
||||||
|
(Ok(ra), Ok(rb)) => ra.cmp(&rb).reverse(),
|
||||||
|
_ => va.release.cmp(&vb.release).reverse(),
|
||||||
|
};
|
||||||
|
if rel_cmp != std::cmp::Ordering::Equal {
|
||||||
|
return rel_cmp;
|
||||||
|
}
|
||||||
|
// Same release: compare timestamp (lexicographic works for YYYYMMDDThhmmssZ)
|
||||||
|
match (&va.timestamp, &vb.timestamp) {
|
||||||
|
(Some(ta), Some(tb)) => ta.cmp(tb).reverse(),
|
||||||
|
(Some(_), None) => std::cmp::Ordering::Less, // Some > None (newer preferred)
|
||||||
|
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||||
|
(None, None) => std::cmp::Ordering::Equal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(_), None) => std::cmp::Ordering::Less,
|
||||||
|
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||||
|
(None, None) => std::cmp::Ordering::Equal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve an install plan for the given constraints.
|
||||||
|
pub fn resolve_install(image: &Image, constraints: &[Constraint]) -> Result<InstallPlan, SolverError> {
|
||||||
|
// Build provider indexed from catalog
|
||||||
|
let mut provider = IpsProvider::new(image)?;
|
||||||
|
|
||||||
|
// Construct problem requirements from top-level constraints
|
||||||
|
let mut problem = RProblem::default();
|
||||||
|
|
||||||
|
// Augment publisher preferences for roots and create version sets
|
||||||
|
let image_pub_order: Vec<String> = image.publishers().iter().map(|p| p.name.clone()).collect();
|
||||||
|
let default_pub = image
|
||||||
|
.default_publisher()
|
||||||
|
.map(|p| p.name.clone())
|
||||||
|
.unwrap_or_else(|_| String::new());
|
||||||
|
|
||||||
|
for c in constraints.iter().cloned() {
|
||||||
|
// Intern name
|
||||||
|
let name_id = provider.intern_name(&c.stem);
|
||||||
|
|
||||||
|
// Store publisher preferences for this root
|
||||||
|
let mut prefs = c.preferred_publishers.clone();
|
||||||
|
if prefs.is_empty() {
|
||||||
|
prefs = image_pub_order.clone();
|
||||||
|
if !default_pub.is_empty() && !prefs.iter().any(|p| p == &default_pub) {
|
||||||
|
prefs.push(default_pub.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
provider
|
||||||
|
.publisher_prefs
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(name_id, prefs);
|
||||||
|
|
||||||
|
// Build version set: by release if provided; optionally by branch if present
|
||||||
|
let vs_kind = match (c.version_req, c.branch) {
|
||||||
|
(Some(release), Some(branch)) => VersionSetKind::ReleaseAndBranch { release, branch },
|
||||||
|
(Some(release), None) => VersionSetKind::ReleaseEq(release),
|
||||||
|
(None, Some(branch)) => VersionSetKind::BranchEq(branch),
|
||||||
|
(None, None) => VersionSetKind::Any,
|
||||||
|
};
|
||||||
|
let vs_id = provider.version_set_for(name_id, vs_kind);
|
||||||
|
problem.requirements.push(RRequirement::from(vs_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before moving provider into the solver, capture a map from solvable id to fmri
|
||||||
|
let mut sid_to_fmri: HashMap<SolvableId, Fmri> = HashMap::new();
|
||||||
|
for ids in provider.cands_by_name.values() {
|
||||||
|
for sid in ids {
|
||||||
|
let fmri = provider.solvables.get(*sid).unwrap().fmri.clone();
|
||||||
|
sid_to_fmri.insert(*sid, fmri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the solver
|
||||||
|
let mut solver = RSolver::new(provider);
|
||||||
|
let solution_ids = solver
|
||||||
|
.solve(problem)
|
||||||
|
.map_err(|e| SolverError::new(format!("dependency solving failed: {e:?}")))?;
|
||||||
|
|
||||||
|
// Build plan from solution
|
||||||
|
let image_ref = image;
|
||||||
|
let mut plan = InstallPlan::default();
|
||||||
|
for sid in solution_ids {
|
||||||
|
if let Some(fmri) = sid_to_fmri.get(&sid).cloned() {
|
||||||
|
let manifest = image_ref
|
||||||
|
.get_manifest_from_catalog(&fmri)
|
||||||
|
.map_err(|e| SolverError::new(format!("failed to load manifest for {}: {e}", fmri)))?
|
||||||
|
.ok_or_else(|| SolverError::new(format!("manifest not found in catalog for {}", fmri)))?;
|
||||||
|
plan.reasons.push(format!("selected {} via solver", fmri));
|
||||||
|
plan.add.push(ResolvedPkg { fmri, manifest });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_one(provider: &mut CatalogProvider, c: &Constraint, plan: &mut InstallPlan, visited: &mut BTreeSet<String>) -> Result<(), SolverError> {
|
||||||
|
if visited.contains(&c.stem) { return Ok(()); }
|
||||||
|
|
||||||
|
// Limit immutable borrow scope of provider through this block
|
||||||
|
let (selected_fmri, manifest, dep_constraints): (Fmri, Manifest, Vec<Constraint>) = {
|
||||||
|
let pkg = provider
|
||||||
|
.best_match(c)
|
||||||
|
.ok_or_else(|| SolverError::new(format!("no candidate found for {}", c.stem)))?;
|
||||||
|
|
||||||
|
trace!(stem = %c.stem, fmri = %pkg.fmri, "selected candidate");
|
||||||
|
|
||||||
|
// get manifest
|
||||||
|
let manifest = provider
|
||||||
|
.image
|
||||||
|
.get_manifest_from_catalog(&pkg.fmri)
|
||||||
|
.map_err(|e| SolverError::new(format!("failed to load manifest for {}: {e}", pkg.fmri)))?
|
||||||
|
.ok_or_else(|| SolverError::new(format!("manifest not found in catalog for {}", pkg.fmri)))?;
|
||||||
|
|
||||||
|
let selected_fmri = pkg.fmri.clone();
|
||||||
|
|
||||||
|
// collect dependency constraints now, before dropping the borrow on provider
|
||||||
|
let dep_constraints: Vec<Constraint> = manifest
|
||||||
|
.dependencies
|
||||||
|
.iter()
|
||||||
|
.filter(|d| d.dependency_type == "require")
|
||||||
|
.filter_map(|d| constraint_from_dependency(d, &selected_fmri, provider.image))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(selected_fmri, manifest, dep_constraints)
|
||||||
|
};
|
||||||
|
|
||||||
|
// add to plan
|
||||||
|
plan.add.push(ResolvedPkg { fmri: selected_fmri.clone(), manifest: manifest.clone() });
|
||||||
|
plan.reasons.push(format!("selected {} due to user request or dependency", selected_fmri));
|
||||||
|
visited.insert(c.stem.clone());
|
||||||
|
|
||||||
|
// traverse require dependencies
|
||||||
|
for dep_c in dep_constraints {
|
||||||
|
resolve_one(provider, &dep_c, plan, visited)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_publisher_preference(parent_pub: Option<&str>, image: &Image) -> Vec<String> {
|
||||||
|
let mut order: Vec<String> = Vec::new();
|
||||||
|
// 1) parent publisher first if provided
|
||||||
|
if let Some(p) = parent_pub {
|
||||||
|
order.push(p.to_string());
|
||||||
|
}
|
||||||
|
// 2) image publishers in configured order
|
||||||
|
for p in image.publishers() {
|
||||||
|
if !order.iter().any(|x| x == &p.name) {
|
||||||
|
order.push(p.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3) default publisher at the end if missing
|
||||||
|
if let Ok(def) = image.default_publisher() {
|
||||||
|
if !order.iter().any(|x| x == &def.name) {
|
||||||
|
order.push(def.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constraint_from_dependency(dep: &Dependency, parent: &Fmri, image: &Image) -> Option<Constraint> {
|
||||||
|
// We only support simple FMRI deps without complex predicates for MVP
|
||||||
|
if let Some(f) = &dep.fmri {
|
||||||
|
let stem = f.stem().to_string();
|
||||||
|
// The dependant's branch is enforced for dependencies
|
||||||
|
let branch = parent
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v.branch.clone());
|
||||||
|
// Use only the main release component from the dependency's version expression
|
||||||
|
let version_req = f
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.release.clone());
|
||||||
|
// Publisher preference: parent publisher, then image order, then default
|
||||||
|
let preferred_publishers = build_publisher_preference(parent.publisher.as_deref(), image);
|
||||||
|
|
||||||
|
return Some(Constraint { stem, version_req, preferred_publishers, branch });
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// These are light smoke tests using a fabricated Image may be non-trivial.
|
||||||
|
// Leave placeholder tests to ensure API compiles.
|
||||||
|
#[test]
|
||||||
|
fn install_plan_struct_defaults() {
|
||||||
|
let plan = InstallPlan::default();
|
||||||
|
assert!(plan.add.is_empty());
|
||||||
|
assert!(plan.remove.is_empty());
|
||||||
|
assert!(plan.update.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod solver_integration_tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::image::ImageType;
|
||||||
|
use crate::image::catalog::{CATALOG_TABLE, OBSOLETED_TABLE};
|
||||||
|
use redb::Database;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
fn mk_version(release: &str, branch: Option<&str>, timestamp: Option<&str>) -> Version {
|
||||||
|
let mut v = Version::new(release);
|
||||||
|
if let Some(b) = branch { v.branch = Some(b.to_string()); }
|
||||||
|
if let Some(t) = timestamp { v.timestamp = Some(t.to_string()); }
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mk_fmri(publisher: &str, name: &str, v: Version) -> Fmri {
|
||||||
|
Fmri::with_publisher(publisher, name, Some(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mk_manifest(fmri: &Fmri, req_deps: &[Fmri]) -> Manifest {
|
||||||
|
let mut m = Manifest::new();
|
||||||
|
// Add pkg.fmri attribute
|
||||||
|
let mut attr = crate::actions::Attr::default();
|
||||||
|
attr.key = "pkg.fmri".to_string();
|
||||||
|
attr.values = vec![fmri.to_string()];
|
||||||
|
m.attributes.push(attr);
|
||||||
|
// Add require dependencies
|
||||||
|
for df in req_deps {
|
||||||
|
let mut d = Dependency::default();
|
||||||
|
d.fmri = Some(df.clone());
|
||||||
|
d.dependency_type = "require".to_string();
|
||||||
|
m.dependencies.push(d);
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_manifest_to_catalog(image: &Image, fmri: &Fmri, manifest: &Manifest) {
|
||||||
|
let db = Database::open(image.catalog_db_path()).expect("open catalog db");
|
||||||
|
let tx = db.begin_write().expect("begin write");
|
||||||
|
{
|
||||||
|
let mut table = tx.open_table(CATALOG_TABLE).expect("open catalog table");
|
||||||
|
let key = format!("{}@{}", fmri.stem(), fmri.version());
|
||||||
|
let val = serde_json::to_vec(manifest).expect("serialize manifest");
|
||||||
|
table.insert(key.as_str(), val.as_slice()).expect("insert manifest");
|
||||||
|
}
|
||||||
|
tx.commit().expect("commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_obsolete(image: &Image, fmri: &Fmri) {
|
||||||
|
let db = Database::open(image.catalog_db_path()).expect("open catalog db");
|
||||||
|
let tx = db.begin_write().expect("begin write");
|
||||||
|
{
|
||||||
|
let mut table = tx.open_table(OBSOLETED_TABLE).expect("open obsoleted table");
|
||||||
|
let key = fmri.to_string();
|
||||||
|
// store empty value
|
||||||
|
let empty: Vec<u8> = Vec::new();
|
||||||
|
table.insert(key.as_str(), empty.as_slice()).expect("insert obsolete");
|
||||||
|
}
|
||||||
|
tx.commit().expect("commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_image_with_publishers(pubs: &[(&str, bool)]) -> Image {
|
||||||
|
let td = tempdir().expect("tempdir");
|
||||||
|
// Persist the directory for the duration of the test
|
||||||
|
let path = td.keep();
|
||||||
|
let mut img = Image::create_image(&path, ImageType::Partial).expect("create image");
|
||||||
|
for (name, is_default) in pubs.iter().copied() {
|
||||||
|
img.add_publisher(name, &format!("https://example.com/{name}"), vec![], is_default)
|
||||||
|
.expect("add publisher");
|
||||||
|
}
|
||||||
|
img
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_newest_release_then_timestamp() {
|
||||||
|
let img = make_image_with_publishers(&[("pubA", true)]);
|
||||||
|
|
||||||
|
let fmri_100_old = mk_fmri("pubA", "pkg/alpha", mk_version("1.0", None, Some("20200101T000000Z")));
|
||||||
|
let fmri_100_new = mk_fmri("pubA", "pkg/alpha", mk_version("1.0", None, Some("20200201T000000Z")));
|
||||||
|
let fmri_110_any = mk_fmri("pubA", "pkg/alpha", mk_version("1.1", None, Some("20200115T000000Z")));
|
||||||
|
|
||||||
|
write_manifest_to_catalog(&img, &fmri_100_old, &mk_manifest(&fmri_100_old, &[]));
|
||||||
|
write_manifest_to_catalog(&img, &fmri_100_new, &mk_manifest(&fmri_100_new, &[]));
|
||||||
|
write_manifest_to_catalog(&img, &fmri_110_any, &mk_manifest(&fmri_110_any, &[]));
|
||||||
|
|
||||||
|
let c = Constraint { stem: "pkg/alpha".to_string(), version_req: None, preferred_publishers: vec![], branch: None };
|
||||||
|
let plan = resolve_install(&img, &[c]).expect("resolve");
|
||||||
|
assert!(!plan.add.is_empty());
|
||||||
|
let chosen = &plan.add[0].fmri;
|
||||||
|
assert_eq!(chosen.version.as_ref().unwrap().release, "1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignore_obsolete_candidates() {
|
||||||
|
let img = make_image_with_publishers(&[("pubA", true)]);
|
||||||
|
|
||||||
|
let fmri_non_obsolete = mk_fmri("pubA", "pkg/beta", mk_version("0.9", None, Some("20200101T000000Z")));
|
||||||
|
let fmri_obsolete = mk_fmri("pubA", "pkg/beta", mk_version("1.0", None, Some("20200301T000000Z")));
|
||||||
|
|
||||||
|
write_manifest_to_catalog(&img, &fmri_non_obsolete, &mk_manifest(&fmri_non_obsolete, &[]));
|
||||||
|
// mark the 1.0 as obsolete (not adding to catalog table)
|
||||||
|
mark_obsolete(&img, &fmri_obsolete);
|
||||||
|
|
||||||
|
let c = Constraint { stem: "pkg/beta".to_string(), version_req: None, preferred_publishers: vec![], branch: None };
|
||||||
|
let plan = resolve_install(&img, &[c]).expect("resolve");
|
||||||
|
assert!(!plan.add.is_empty());
|
||||||
|
let chosen = &plan.add[0].fmri;
|
||||||
|
assert_eq!(chosen.version.as_ref().unwrap().release, "0.9");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dependency_sticks_to_parent_branch() {
|
||||||
|
let img = make_image_with_publishers(&[("pubA", true)]);
|
||||||
|
// Parent pkg on branch 1 with a require on dep@5.11
|
||||||
|
let parent = mk_fmri("pubA", "pkg/parent", mk_version("5.11", Some("1"), Some("20200102T000000Z")));
|
||||||
|
let dep_req = Fmri::with_version("pkg/dep", Version::new("5.11"));
|
||||||
|
let parent_manifest = mk_manifest(&parent, &[dep_req.clone()]);
|
||||||
|
write_manifest_to_catalog(&img, &parent, &parent_manifest);
|
||||||
|
|
||||||
|
// dep on branch 1 (older) and branch 2 (newer) — branch 1 must be selected
|
||||||
|
let dep_branch1_old = mk_fmri("pubA", "pkg/dep", mk_version("5.11", Some("1"), Some("20200101T000000Z")));
|
||||||
|
let dep_branch1_new = mk_fmri("pubA", "pkg/dep", mk_version("5.11", Some("1"), Some("20200201T000000Z")));
|
||||||
|
let dep_branch2_newer = mk_fmri("pubA", "pkg/dep", mk_version("5.11", Some("2"), Some("20200401T000000Z")));
|
||||||
|
write_manifest_to_catalog(&img, &dep_branch1_old, &mk_manifest(&dep_branch1_old, &[]));
|
||||||
|
write_manifest_to_catalog(&img, &dep_branch1_new, &mk_manifest(&dep_branch1_new, &[]));
|
||||||
|
write_manifest_to_catalog(&img, &dep_branch2_newer, &mk_manifest(&dep_branch2_newer, &[]));
|
||||||
|
|
||||||
|
let c = Constraint { stem: "pkg/parent".to_string(), version_req: None, preferred_publishers: vec![], branch: None };
|
||||||
|
let plan = resolve_install(&img, &[c]).expect("resolve");
|
||||||
|
// find dep in plan
|
||||||
|
let dep_pkg = plan.add.iter().find(|p| p.fmri.stem() == "pkg/dep").expect("dep present");
|
||||||
|
let v = dep_pkg.fmri.version.as_ref().unwrap();
|
||||||
|
assert_eq!(v.release, "5.11");
|
||||||
|
assert_eq!(v.branch.as_deref(), Some("1"));
|
||||||
|
assert_eq!(v.timestamp.as_deref(), Some("20200201T000000Z"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dependency_prefers_parent_publisher_over_newer_other_publisher() {
|
||||||
|
// Parent is from pubA; dep exists on pubA (older) and pubB (newer). Expect pubA.
|
||||||
|
let img = make_image_with_publishers(&[("pubA", true), ("pubB", false)]);
|
||||||
|
// Ensure image publishers order contains both; default already set by first.
|
||||||
|
|
||||||
|
let parent = mk_fmri("pubA", "pkg/root", mk_version("1.0", None, Some("20200101T000000Z")));
|
||||||
|
let dep_req = Fmri::with_version("pkg/child", Version::new("1.0"));
|
||||||
|
let parent_manifest = mk_manifest(&parent, &[dep_req.clone()]);
|
||||||
|
write_manifest_to_catalog(&img, &parent, &parent_manifest);
|
||||||
|
|
||||||
|
let dep_pub_a_old = mk_fmri("pubA", "pkg/child", mk_version("1.0", None, Some("20200101T000000Z")));
|
||||||
|
let dep_pub_b_new = mk_fmri("pubB", "pkg/child", mk_version("1.0", None, Some("20200301T000000Z")));
|
||||||
|
write_manifest_to_catalog(&img, &dep_pub_a_old, &mk_manifest(&dep_pub_a_old, &[]));
|
||||||
|
write_manifest_to_catalog(&img, &dep_pub_b_new, &mk_manifest(&dep_pub_b_new, &[]));
|
||||||
|
|
||||||
|
let c = Constraint { stem: "pkg/root".to_string(), version_req: None, preferred_publishers: vec![], branch: None };
|
||||||
|
let plan = resolve_install(&img, &[c]).expect("resolve");
|
||||||
|
let dep_pkg = plan.add.iter().find(|p| p.fmri.stem() == "pkg/child").expect("child present");
|
||||||
|
assert_eq!(dep_pkg.fmri.publisher.as_deref(), Some("pubA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn top_level_release_only_version_requirement() {
|
||||||
|
let img = make_image_with_publishers(&[("pubA", true)]);
|
||||||
|
let v10_old = mk_fmri("pubA", "pkg/vers", mk_version("1.0", None, Some("20200101T000000Z")));
|
||||||
|
let v10_new = mk_fmri("pubA", "pkg/vers", mk_version("1.0", None, Some("20200201T000000Z")));
|
||||||
|
let v11 = mk_fmri("pubA", "pkg/vers", mk_version("1.1", None, Some("20200301T000000Z")));
|
||||||
|
write_manifest_to_catalog(&img, &v10_old, &mk_manifest(&v10_old, &[]));
|
||||||
|
write_manifest_to_catalog(&img, &v10_new, &mk_manifest(&v10_new, &[]));
|
||||||
|
write_manifest_to_catalog(&img, &v11, &mk_manifest(&v11, &[]));
|
||||||
|
|
||||||
|
let c = Constraint { stem: "pkg/vers".to_string(), version_req: Some("1.0".to_string()), preferred_publishers: vec![], branch: None };
|
||||||
|
let plan = resolve_install(&img, &[c]).expect("resolve");
|
||||||
|
let chosen = &plan.add[0].fmri;
|
||||||
|
let v = chosen.version.as_ref().unwrap();
|
||||||
|
assert_eq!(v.release, "1.0");
|
||||||
|
assert_eq!(v.timestamp.as_deref(), Some("20200201T000000Z"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue