Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ jobs:
with:
tool: cargo-tarpaulin
- run: cargo tarpaulin -o lcov --output-dir coverage
- run: brew trust coverallsapp/coveralls
if: matrix.os == 'macos-latest'
Comment thread
jhheider marked this conversation as resolved.
- uses: coverallsapp/github-action@v2
with:
path-to-lcov: coverage/lcov.info
Expand Down Expand Up @@ -234,6 +236,8 @@ jobs:
-Xdemangler=rustfilt \
> lcov.info

- run: brew trust coverallsapp/coveralls
if: matrix.os == 'macos-latest'
Comment thread
jhheider marked this conversation as resolved.
- uses: coverallsapp/github-action@v2
with:
path-to-lcov: lcov.info
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub async fn resolve(
let mut installations = resolution.installed;
if !resolution.pending.is_empty() {
if env::var("PKGX_NO_INSTALL").is_ok() {
return Err("PKGX_NO_INSTALL is set, refusing to install pending packages")?;
Err("PKGX_NO_INSTALL is set, refusing to install pending packages")?;
}
Comment thread
jhheider marked this conversation as resolved.
let installed = install_multi(&resolution.pending, config, spinner.arc()).await?;
installations.extend(installed);
Expand Down
4 changes: 2 additions & 2 deletions crates/lib/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ pub fn expand_moustaches(input: &str, pkg: &Installation, deps: &Vec<Installatio
}

output = output.replace("{{prefix}}", &pkg.path.to_string_lossy());
output = output.replace("{{version}}", &format!("{}", &pkg.pkg.version));
output = output.replace("{{version}}", &format!("{}", pkg.pkg.version));
output = output.replace("{{version.major}}", &format!("{}", pkg.pkg.version.major));
output = output.replace("{{version.minor}}", &format!("{}", pkg.pkg.version.minor));
output = output.replace("{{version.patch}}", &format!("{}", pkg.pkg.version.patch));
Expand All @@ -228,7 +228,7 @@ pub fn expand_moustaches(input: &str, pkg: &Installation, deps: &Vec<Installatio
);
output = output.replace(
&format!("{{{{{}.version}}}}", prefix),
&format!("{}", &dep.pkg.version),
&format!("{}", dep.pkg.version),
);
output = output.replace(
&format!("{{{{{}.version.major}}}}", prefix),
Expand Down
127 changes: 92 additions & 35 deletions crates/lib/src/hydrate.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
use crate::types::PackageReq;
use libsemverator::range::Range as VersionReq;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::error::Error;

/// Projects whose distinct version lines are parallel-installable (different
/// sonames / ICU majors / abseil LTS namespaces). When constraints cannot
/// intersect we keep the extra lines as separate `PackageReq` entries instead
/// of failing the graph. Note: these extra lines are surfaced in the resolved
/// set but their own deps are not re-hydrated — fine while alt-line deps match
/// the primary line (openssl/abseil/unicode), revisit if that stops holding.
///
/// - unicode.org: ICU major ABI (see pantry#4104, pkgx#899)
/// - openssl.org: libssl.so.1.1 vs libssl.so.3
/// - abseil.io: LTS inline-namespace + soversion (20250127 vs 20250512, …)
const MULTI_VERSION_PROJECTS: &[&str] = &["unicode.org", "openssl.org", "abseil.io"];

fn is_multi_version(project: &str) -> bool {
MULTI_VERSION_PROJECTS.contains(&project)
}

/// Record an extra constraint for a multi-version project, merging into an
/// existing additional entry when the ranges intersect.
fn push_additional(additional: &mut Vec<PackageReq>, pkg: PackageReq) {
for existing in additional.iter_mut().filter(|p| p.project == pkg.project) {
if let Ok(constraint) = intersect_constraints(&existing.constraint, &pkg.constraint) {
existing.constraint = constraint;
return;
}
}
additional.push(pkg);
}

#[derive(Clone)]
struct Node {
parent: Option<Box<Node>>,
pkg: PackageReq,
children: HashSet<String>,
}

impl Node {
fn new(pkg: PackageReq, parent: Option<Box<Node>>) -> Self {
Self {
parent,
pkg,
children: HashSet::new(),
}
Self { parent, pkg }
}

fn count(&self) -> usize {
Expand All @@ -38,36 +61,55 @@ pub async fn hydrate<F>(
where
F: Fn(String) -> Result<Vec<PackageReq>, Box<dyn Error>>,
{
let dry = condense(input);
let dry = condense(input)?;
let mut graph: HashMap<String, Box<Node>> = HashMap::new();
let mut stack: Vec<Box<Node>> = vec![];
let mut additional_unicodes: Vec<VersionReq> = vec![];
let mut additional: Vec<PackageReq> = vec![];

for pkg in dry.iter() {
let node = graph
.entry(pkg.project.clone())
.or_insert_with(|| Box::new(Node::new(pkg.clone(), None)));
node.pkg.constraint = intersect_constraints(&node.pkg.constraint, &pkg.constraint)
.map_err(|e| format!("{} for {}", e, pkg.project))?;
stack.push(node.clone());
if let Some(node) = graph.get_mut(&pkg.project) {
match intersect_constraints(&node.pkg.constraint, &pkg.constraint) {
Ok(constraint) => {
node.pkg.constraint = constraint;
stack.push(node.clone());
}
Err(e) => {
if is_multi_version(&pkg.project) {
push_additional(&mut additional, pkg.clone());
} else {
return Err(format!("{} for {}", e, pkg.project).into());
}
}
}
} else {
let node = Box::new(Node::new(pkg.clone(), None));
graph.insert(pkg.project.clone(), node.clone());
stack.push(node);
}
}

while let Some(mut current) = stack.pop() {
while let Some(current) = stack.pop() {
for child_pkg in get_deps(current.pkg.project.clone())? {
let was_new = !graph.contains_key(&child_pkg.project);
let child_node = graph
.entry(child_pkg.project.clone())
.or_insert_with(|| Box::new(Node::new(child_pkg.clone(), Some(current.clone()))));

if was_new {
// Fresh node already carries child_pkg.constraint.
stack.push(child_node.clone());
continue;
}

// Already have a graph node: try the primary constraint, then any
// additional lines for this multi-version project.
let intersection =
intersect_constraints(&child_node.pkg.constraint, &child_pkg.constraint);
if let Ok(constraint) = intersection {
child_node.pkg.constraint = constraint;
current.children.insert(child_node.pkg.project.clone());
stack.push(child_node.clone());
} else if child_pkg.project == "unicode.org" {
// we handle unicode.org for now to allow situations like:
// https://github.com/pkgxdev/pantry/issues/4104
// https://github.com/pkgxdev/pkgx/issues/899
additional_unicodes.push(child_pkg.constraint);
} else if is_multi_version(&child_pkg.project) {
push_additional(&mut additional, child_pkg);
} else {
return Err(
format!("{} for {}", intersection.unwrap_err(), child_pkg.project).into(),
Expand All @@ -80,30 +122,45 @@ where
pkgs.sort_by_key(|node| node.count());
let mut pkgs: Vec<PackageReq> = pkgs.into_iter().map(|node| node.pkg.clone()).collect();

// see above explanation
for constraint in additional_unicodes {
let pkg = PackageReq {
project: "unicode.org".to_string(),
constraint,
};
pkgs.push(pkg);
}
pkgs.extend(additional);
Comment thread
jhheider marked this conversation as resolved.

Ok(pkgs)
}

/// Condenses a list of `PackageRequirement` by intersecting constraints for duplicates.
fn condense(pkgs: &Vec<PackageReq>) -> Vec<PackageReq> {
/// Condenses a list of `PackageReq` by intersecting constraints for duplicates.
/// Multi-version projects keep non-intersecting constraints as separate entries.
fn condense(pkgs: &Vec<PackageReq>) -> Result<Vec<PackageReq>, Box<dyn Error>> {
let mut out: Vec<PackageReq> = vec![];
for pkg in pkgs {
if let Some(existing) = out.iter_mut().find(|p| p.project == pkg.project) {
existing.constraint = intersect_constraints(&existing.constraint, &pkg.constraint)
.expect("Failed to intersect constraints");
match intersect_constraints(&existing.constraint, &pkg.constraint) {
Ok(constraint) => existing.constraint = constraint,
Err(e) => {
if is_multi_version(&pkg.project) {
// merge into a later non-intersecting sibling if possible
let mut merged = false;
for sibling in out.iter_mut().filter(|p| p.project == pkg.project).skip(1) {
if let Ok(constraint) =
intersect_constraints(&sibling.constraint, &pkg.constraint)
Comment thread
jhheider marked this conversation as resolved.
{
sibling.constraint = constraint;
merged = true;
break;
}
}
if !merged {
out.push(pkg.clone());
}
} else {
return Err(format!("{} for {}", e, pkg.project).into());
}
}
}
} else {
out.push(pkg.clone());
}
}
out
Ok(out)
}

/// Intersects two version constraints.
Expand Down
2 changes: 2 additions & 0 deletions crates/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub mod pantry_db;
pub mod platform_case_aware_env_key;
pub mod resolve;
pub mod sync;
#[cfg(test)]
mod tests;
pub mod types;
pub mod utils;

Expand Down
4 changes: 1 addition & 3 deletions crates/lib/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ pub async fn update(config: &Config, conn: &mut Connection) -> Result<(), Box<dy
FileExt::unlock(&lockfile)?;
return Ok(());
} else {
return Err(
"PKGX_PANTRY_DIR is set but does not contain a pantry (missing projects/)",
)?;
Err("PKGX_PANTRY_DIR is set but does not contain a pantry (missing projects/)")?;
Comment thread
jhheider marked this conversation as resolved.
}
}
replace(config, conn).await
Expand Down
Loading
Loading