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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bindings/java/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ kpar-ppmd = ["sysand-core/kpar-ppmd"]
[dependencies]
sysand-core = { path = "../../core", features = ["std", "filesystem", "networking"] }
camino.workspace = true
fluent-uri = { version = "0.4.1", features = ["serde", "net"] }
jni = "0.21.1"
reqwest-middleware = { version = "0.5.1" }
indexmap = { version = "2.13.0", default-features = false, features = ["serde"] }
Expand Down
20 changes: 13 additions & 7 deletions bindings/java/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use std::sync::Arc;

use camino::Utf8PathBuf;
use fluent_uri::Iri;
use jni::{
JNIEnv,
errors::Error,
Expand All @@ -14,7 +15,6 @@ use sysand_core::{
build::{KParBuildError, KparCompressionMethod},
commands,
env::{DEFAULT_ENV_NAME, local_directory::LocalWriteError},
info::InfoError,
init::InitError,
project::{
ProjectMut,
Expand Down Expand Up @@ -272,14 +272,20 @@ pub extern "system" fn Java_com_sensmetry_sysand_Sysand_info<'local>(
}
};

let uri = match Iri::parse(uri) {
Ok(u) => u,
Err((error, input)) => {
env.throw_exception(
ExceptionKind::ResolutionError,
format!("Provided IRI `{input}` is invalid: {error}"),
);
return JObject::default();
}
};

let info_meta = match commands::info::do_info(&uri, &combined_resolver) {
Ok(info_meta) => info_meta,
Err(
e @ (InfoError::NoSemanticVersionsFound(_)
| InfoError::NoResolve(..)
| InfoError::UnsupportedIri(..)
| InfoError::Resolution(_)),
) => {
Err(e) => {
env.throw_exception(ExceptionKind::ResolutionError, format_err(e));
return JObject::default();
}
Expand Down
1 change: 1 addition & 0 deletions bindings/py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ kpar-ppmd = ["sysand-core/kpar-ppmd", "sysand/kpar-ppmd"]
sysand-core = { path = "../../core", features = ["python", "filesystem", "networking"] }
sysand = { path = "../../sysand" }
camino.workspace = true
fluent-uri = { version = "0.4.1", features = ["serde", "net"] }
log = { version = "0.4.29", default-features = false }
pyo3 = { version = "0.29.0", default-features = false, features = ["macros"] }
pyo3-log = "0.13.3"
Expand Down
13 changes: 6 additions & 7 deletions bindings/py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use std::{iter, process::ExitCode, sync::Arc};

use camino::{Utf8Path, Utf8PathBuf};
use fluent_uri::Iri;
use pyo3::{
exceptions::{PyFileExistsError, PyFileNotFoundError, PyIOError, PyRuntimeError, PyValueError},
prelude::*,
Expand All @@ -27,7 +28,7 @@ use sysand_core::{
exclude::do_exclude,
include::do_include,
index_location::IndexLocation,
info::{InfoError, InfoProjectError, do_info, do_info_project},
info::{InfoProjectError, do_info, do_info_project},
init::InitError,
model::{InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw, InterchangeProjectUsage},
project::{
Expand Down Expand Up @@ -188,14 +189,12 @@ fn do_info_py(
)
.map_err(|err| PyValueError::new_err(format_err(err)))?;

let uri = Iri::parse(uri)
.map_err(|(e, input)| PyValueError::new_err(format!("invalid IRI `{input}`: {e}")))?;

match do_info(&uri, &combined_resolver) {
Ok(info_meta) => Ok(info_meta),
Err(
e @ (InfoError::NoSemanticVersionsFound(_)
| InfoError::NoResolve(..)
| InfoError::UnsupportedIri(..)
| InfoError::Resolution(_)),
) => Err(PyRuntimeError::new_err(format_err(e))),
Err(e) => Err(PyRuntimeError::new_err(format_err(e))),
}
})
}
Expand Down
52 changes: 35 additions & 17 deletions core/src/commands/info.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
// SPDX-FileCopyrightText: © 2025 Sysand contributors <opensource@sensmetry.com>

use fluent_uri::Iri;
use semver::Version;
use thiserror::Error;

use crate::{
env::utils::ErrorBound,
model::{InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw},
project::ProjectRead,
resolve::{ResolutionOutcome, ResolveRead},
resolve::{ResolutionInfo, ResolutionOutcome, ResolveRead},
utils::format_err,
};

Expand Down Expand Up @@ -42,19 +43,33 @@ pub fn do_info_project<P: ProjectRead>(
pub enum InfoError<Error: ErrorBound> {
#[error("none of the following found versions are valid semantic versions {}", .0.join(", "))]
NoSemanticVersionsFound(Vec<String>),
#[error("failed to resolve IRI `{0}`: {1}")]
NoResolve(Box<str>, String),
#[error("IRI `{0}` is not supported: {1}")]
UnsupportedIri(Box<str>, String),
#[error("failed to resolve {usage}: {reason}")]
NoResolve {
usage: ResolutionInfo,
reason: String,
},
#[error("{usage} was not found: {reason}")]
NotFound {
usage: ResolutionInfo,
reason: String,
},
#[error("{usage} is not supported: {reason}")]
UnsupportedUsage {
usage: ResolutionInfo,
reason: String,
},
#[error("failure during resolution")]
Resolution(#[from] Error),
}

pub fn do_info<S: AsRef<str>, R: ResolveRead>(
uri: S,
#[expect(clippy::result_large_err)]
pub fn do_info<R: ResolveRead>(
uri: &Iri<String>,
resolver: &R,
) -> Result<(InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw), InfoError<R::Error>> {
let outcome = resolver.resolve_read_raw(uri.as_ref())?;
// TODO: support other usage types
let resolve = ResolutionInfo::iri(uri.to_owned());
let outcome = resolver.resolve_read(&resolve)?;

match outcome {
ResolutionOutcome::Resolved(resolved) => {
Expand Down Expand Up @@ -110,14 +125,17 @@ pub fn do_info<S: AsRef<str>, R: ResolveRead>(
None => Err(InfoError::NoSemanticVersionsFound(non_semantic_versions)),
}
}
ResolutionOutcome::UnsupportedUsageType { reason } => {
Err(InfoError::UnsupportedIri(uri.as_ref().into(), reason))
}
ResolutionOutcome::NotFound { reason } => {
Err(InfoError::NoResolve(uri.as_ref().into(), reason))
}
ResolutionOutcome::Unresolvable { reason } => {
Err(InfoError::NoResolve(uri.as_ref().into(), reason))
}
ResolutionOutcome::UnsupportedUsageType { reason } => Err(InfoError::UnsupportedUsage {
usage: resolve,
reason,
}),
ResolutionOutcome::Unresolvable { reason } => Err(InfoError::NoResolve {
usage: resolve,
reason,
}),
ResolutionOutcome::NotFound { reason } => Err(InfoError::NotFound {
usage: resolve,
reason,
}),
}
}
20 changes: 13 additions & 7 deletions core/src/commands/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ use crate::{
model::{
InterchangeProjectUsage, InterchangeProjectUsageRaw, InterchangeProjectValidationError,
},
project::{CanonicalizationError, ProjectRead, memory::InMemoryProject, utils::FsIoError},
project::{CanonicalizationError, ProjectRead, utils::FsIoError},
resolve::ResolveRead,
solve::pubgrub::{SolverError, solve},
utils::ProvidedProjects,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -137,7 +138,7 @@ pub fn do_lock_projects<
>(
projects: I,
resolver: R,
provided_iris: &HashMap<String, Vec<InMemoryProject>>,
provided_usages: &ProvidedProjects,
ctx: &ProjectContext,
) -> Result<LockOutcome<PD>, LockProjectError<PI, PD, R>> {
let mut lock = Lock::default();
Expand Down Expand Up @@ -210,7 +211,7 @@ pub fn do_lock_projects<
all_deps.extend(validated_info.usage);
}

let lock_outcome = do_lock_extend(lock, all_deps, resolver, provided_iris, ctx)?;
let lock_outcome = do_lock_extend(lock, all_deps, resolver, provided_usages, ctx)?;

Ok(lock_outcome)
}
Expand All @@ -233,7 +234,7 @@ pub fn do_lock_extend<
mut lock: Lock,
usages: I,
resolver: R,
provided_iris: &HashMap<String, Vec<InMemoryProject>>,
provided_usages: &ProvidedProjects,
ctx: &ProjectContext,
) -> Result<LockOutcome<PD>, LockError<PD, R>> {
let inputs: Vec<_> = usages.into_iter().collect();
Expand Down Expand Up @@ -274,6 +275,11 @@ pub fn do_lock_extend<
project_label: iri_str.clone(),
field: IncompleteField::Info,
})?;
// Validate dependency projects too, not just the top-level ones.
info.validate().map_err(|e| LockError::InvalidProject {
identifier: iri_str.clone(),
source: e,
})?;
let meta = project
.get_meta()
.map_err(LockError::DependencyProject)?
Expand All @@ -282,7 +288,7 @@ pub fn do_lock_extend<
field: IncompleteField::Meta,
})?;

let sources = if !provided_iris.contains_key(iri.as_str()) {
let sources = if !provided_usages.contains_key(iri.as_str()) {
let sources = project.sources(ctx).map_err(LockError::DependencyProject)?;
debug_assert!(!sources.is_empty());
sources
Expand Down Expand Up @@ -363,7 +369,7 @@ pub fn do_lock_local_editable<
path: P,
project_root: PR,
identifiers: Option<Vec<Iri<String>>>,
provided_iris: &HashMap<String, Vec<InMemoryProject>>,
provided_usages: &ProvidedProjects,
resolver: R,
ctx: &ProjectContext,
) -> Result<LockOutcome<PD>, LockProjectError<EditableLocalSrcProject, PD, R>> {
Expand All @@ -376,7 +382,7 @@ pub fn do_lock_local_editable<
),
);

do_lock_projects([(identifiers, &project)], resolver, provided_iris, ctx)
do_lock_projects([(identifiers, &project)], resolver, provided_usages, ctx)
}

#[cfg(test)]
Expand Down
33 changes: 13 additions & 20 deletions core/src/commands/sources.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
// SPDX-FileCopyrightText: © 2025 Sysand contributors <opensource@sensmetry.com>

use std::{
collections::{HashMap, HashSet},
fmt::Debug,
};
use std::{collections::HashMap, fmt::Debug};

#[cfg(feature = "filesystem")]
use camino::Utf8PathBuf;
Expand All @@ -16,7 +13,7 @@ use crate::project::local_src::{LocalSrcError, LocalSrcProject, PathError};
use crate::{
env::ReadEnvironment,
model::{InterchangeProjectUsage, InterchangeProjectValidationError},
project::{ProjectRead, memory::InMemoryProject},
project::ProjectRead,
resolve::{
ResolveRead,
env::EnvResolver,
Expand All @@ -25,6 +22,7 @@ use crate::{
},
solve::pubgrub::SolverError,
stdlib::known_std_libs,
utils::ProvidedProjects,
};

/// Selects which dependency sources a sources enumeration should yield. Whether
Expand Down Expand Up @@ -145,13 +143,13 @@ pub fn do_sources_local_src_project_no_deps(
/// Transitively resolves a list of usages (typically the usages of some project)
/// in an environment and enumerates the resolved projects together with their IRIs.
///
/// `provided_iris` are assumed to have been satisfied (including their dependencies)
/// `provided_usages` are assumed to have been satisfied (including their dependencies)
/// but have to match.
#[allow(clippy::type_complexity)]
fn solve_dependencies<Env: ReadEnvironment + Debug + 'static>(
requested: Vec<InterchangeProjectUsage>,
env: Env,
provided_iris: &HashMap<String, Vec<InMemoryProject>>,
provided_usages: &ProvidedProjects,
) -> Result<
Vec<(
fluent_uri::Iri<String>,
Expand All @@ -161,8 +159,8 @@ fn solve_dependencies<Env: ReadEnvironment + Debug + 'static>(
> {
let mut memory_projects = HashMap::default();

for (k, v) in provided_iris {
memory_projects.insert(fluent_uri::Iri::parse(k.clone()).unwrap(), v.to_vec());
for (k, v) in provided_usages {
memory_projects.insert(k.clone(), v.to_vec());
}

let wrapped_resolver = PriorityResolver::new(
Expand All @@ -187,17 +185,17 @@ fn solve_dependencies<Env: ReadEnvironment + Debug + 'static>(
/// Transitively resolve a list of usages (typically the usages of some project)
/// in an environment and enumerate the resolved projects.
///
/// `provided_iris` are assumed to have been satisfied (including their dependencies)
/// `provided_usages` are assumed to have been satisfied (including their dependencies)
/// but have to match.
pub fn find_project_dependencies<Env: ReadEnvironment + Debug + 'static>(
requested: Vec<InterchangeProjectUsage>,
env: Env,
provided_iris: &HashMap<String, Vec<InMemoryProject>>,
provided_usages: &ProvidedProjects,
) -> Result<
Vec<<Env as ReadEnvironment>::InterchangeProjectRead>,
SolverError<impl ResolveRead + Debug + use<Env>>,
> {
Ok(solve_dependencies(requested, env, provided_iris)?
Ok(solve_dependencies(requested, env, provided_usages)?
.into_iter()
.map(|(_, project)| project)
.collect())
Expand Down Expand Up @@ -227,22 +225,17 @@ pub fn resolve_dependencies<Env: ReadEnvironment + Debug + 'static>(
// For `Deps` the standard libraries are treated as already provided so the
// solver omits them; otherwise everything is resolved and filtered below.
let empty = HashMap::default();
let provided_iris = match dependencies {
let provided_usages = match dependencies {
Dependencies::Deps => &std_libs,
_ => &empty,
};

let resolved = solve_dependencies(requested, env, provided_iris)?;

let std_iris: HashSet<fluent_uri::Iri<String>> = std_libs
.keys()
.map(|iri| fluent_uri::Iri::parse(iri.clone()).expect("BUG: invalid std lib IRI"))
.collect();
let resolved = solve_dependencies(requested, env, provided_usages)?;

Ok(resolved
.into_iter()
.filter(|(iri, _)| match dependencies {
Dependencies::Std => std_iris.contains(iri),
Dependencies::Std => std_libs.contains_key(iri.as_str()),
// std_libs are already filtered out by `solve_dependencies`
Dependencies::Deps | Dependencies::DepsStd => true,
Dependencies::None => false,
Expand Down
Loading
Loading