diff --git a/Cargo.lock b/Cargo.lock index 50b89da28..b0f7ce747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3655,6 +3655,7 @@ name = "sysand-java" version = "0.1.7" dependencies = [ "camino", + "fluent-uri", "indexmap", "jni 0.21.1", "reqwest", @@ -3701,6 +3702,7 @@ version = "0.1.7" dependencies = [ "camino", "camino-tempfile", + "fluent-uri", "log", "predicates", "pyo3", diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml index 64c355af4..09e7ae0ad 100644 --- a/bindings/java/Cargo.toml +++ b/bindings/java/Cargo.toml @@ -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"] } diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index a589ade02..1b4417d36 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use camino::Utf8PathBuf; +use fluent_uri::Iri; use jni::{ JNIEnv, errors::Error, @@ -14,7 +15,6 @@ use sysand_core::{ build::{KParBuildError, KparCompressionMethod}, commands, env::{DEFAULT_ENV_NAME, local_directory::LocalWriteError}, - info::InfoError, init::InitError, project::{ ProjectMut, @@ -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(); } diff --git a/bindings/py/Cargo.toml b/bindings/py/Cargo.toml index 0a4baa7ff..23707f5be 100644 --- a/bindings/py/Cargo.toml +++ b/bindings/py/Cargo.toml @@ -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" diff --git a/bindings/py/src/lib.rs b/bindings/py/src/lib.rs index 5d1cec484..4eaf79d4c 100644 --- a/bindings/py/src/lib.rs +++ b/bindings/py/src/lib.rs @@ -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::*, @@ -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::{ @@ -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))), } }) } diff --git a/core/src/commands/info.rs b/core/src/commands/info.rs index eda1971cd..c7b7f0d7d 100644 --- a/core/src/commands/info.rs +++ b/core/src/commands/info.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: © 2025 Sysand contributors +use fluent_uri::Iri; use semver::Version; use thiserror::Error; @@ -8,7 +9,7 @@ use crate::{ env::utils::ErrorBound, model::{InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw}, project::ProjectRead, - resolve::{ResolutionOutcome, ResolveRead}, + resolve::{ResolutionInfo, ResolutionOutcome, ResolveRead}, utils::format_err, }; @@ -42,19 +43,33 @@ pub fn do_info_project( pub enum InfoError { #[error("none of the following found versions are valid semantic versions {}", .0.join(", "))] NoSemanticVersionsFound(Vec), - #[error("failed to resolve IRI `{0}`: {1}")] - NoResolve(Box, String), - #[error("IRI `{0}` is not supported: {1}")] - UnsupportedIri(Box, 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, R: ResolveRead>( - uri: S, +#[expect(clippy::result_large_err)] +pub fn do_info( + uri: &Iri, resolver: &R, ) -> Result<(InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw), InfoError> { - 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) => { @@ -110,14 +125,17 @@ pub fn do_info, 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, + }), } } diff --git a/core/src/commands/lock.rs b/core/src/commands/lock.rs index d6c71ddad..aa3a986ed 100644 --- a/core/src/commands/lock.rs +++ b/core/src/commands/lock.rs @@ -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)] @@ -137,7 +138,7 @@ pub fn do_lock_projects< >( projects: I, resolver: R, - provided_iris: &HashMap>, + provided_usages: &ProvidedProjects, ctx: &ProjectContext, ) -> Result, LockProjectError> { let mut lock = Lock::default(); @@ -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) } @@ -233,7 +234,7 @@ pub fn do_lock_extend< mut lock: Lock, usages: I, resolver: R, - provided_iris: &HashMap>, + provided_usages: &ProvidedProjects, ctx: &ProjectContext, ) -> Result, LockError> { let inputs: Vec<_> = usages.into_iter().collect(); @@ -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)? @@ -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 @@ -363,7 +369,7 @@ pub fn do_lock_local_editable< path: P, project_root: PR, identifiers: Option>>, - provided_iris: &HashMap>, + provided_usages: &ProvidedProjects, resolver: R, ctx: &ProjectContext, ) -> Result, LockProjectError> { @@ -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)] diff --git a/core/src/commands/sources.rs b/core/src/commands/sources.rs index ec4ec06bb..5d7971181 100644 --- a/core/src/commands/sources.rs +++ b/core/src/commands/sources.rs @@ -1,10 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: © 2025 Sysand contributors -use std::{ - collections::{HashMap, HashSet}, - fmt::Debug, -}; +use std::{collections::HashMap, fmt::Debug}; #[cfg(feature = "filesystem")] use camino::Utf8PathBuf; @@ -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, @@ -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 @@ -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( requested: Vec, env: Env, - provided_iris: &HashMap>, + provided_usages: &ProvidedProjects, ) -> Result< Vec<( fluent_uri::Iri, @@ -161,8 +159,8 @@ fn solve_dependencies( > { 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( @@ -187,17 +185,17 @@ fn solve_dependencies( /// 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( requested: Vec, env: Env, - provided_iris: &HashMap>, + provided_usages: &ProvidedProjects, ) -> Result< Vec<::InterchangeProjectRead>, SolverError>, > { - Ok(solve_dependencies(requested, env, provided_iris)? + Ok(solve_dependencies(requested, env, provided_usages)? .into_iter() .map(|(_, project)| project) .collect()) @@ -227,22 +225,17 @@ pub fn resolve_dependencies( // 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> = 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, diff --git a/core/src/commands/sync.rs b/core/src/commands/sync.rs index 00fa4c0db..55dfc2375 100644 --- a/core/src/commands/sync.rs +++ b/core/src/commands/sync.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: © 2025 Sysand contributors -use std::{collections::HashMap, num::NonZeroU64}; +use std::num::NonZeroU64; use thiserror::Error; use typed_path::Utf8UnixPathBuf; @@ -9,10 +9,9 @@ use typed_path::Utf8UnixPathBuf; use crate::{ commands::env::do_env_install_project, env::{ProjectChecksumResult, ReadEnvironment, WriteEnvironment, utils::ErrorBound}, - iri_normalize::canonicalize_iri_tolerant, lock::{Lock, Source}, - project::{ProjectChecksum, ProjectRead, memory::InMemoryProject}, - utils::format_err, + project::{ProjectChecksum, ProjectRead}, + utils::{ProvidedProjects, format_err}, }; #[derive(Error, Debug)] @@ -116,7 +115,7 @@ pub fn do_sync< remote_kpar_storage: Option, index_kpar_storage: Option, remote_git_storage: Option, - provided_iris: &HashMap>, + provided_usages: &ProvidedProjects, ) -> Result<(), SyncError> where Environment: ReadEnvironment + WriteEnvironment, @@ -147,12 +146,8 @@ where let main_uri = project.identifiers.first(); for iri in &project.identifiers { - let excluded_versions = if let Ok(parsed_iri) = fluent_uri::Iri::parse(iri.clone()) { - // TODO: maybe canonicalize on lock read, or don't canonicalize at all? - provided_iris.get(canonicalize_iri_tolerant(parsed_iri.borrow()).as_str()) - } else { - provided_iris.get(iri.as_str()) - }; + // TODO: maybe canonicalize on lock read, or don't canonicalize at all? + let excluded_versions = provided_usages.get(iri.as_str()); if let Some(versions) = excluded_versions { let mut provided_versions = vec![]; diff --git a/core/src/iri_normalize.rs b/core/src/iri_normalize.rs index 4dcfbf600..eeb6c4591 100644 --- a/core/src/iri_normalize.rs +++ b/core/src/iri_normalize.rs @@ -4,10 +4,12 @@ use std::{char::REPLACEMENT_CHARACTER, fmt::Write as _}; use crate::purl::parse_sysand_purl; +#[cfg(feature = "filesystem")] use crate::utils::scheme::{SCHEME_HTTP, SCHEME_HTTPS}; +#[cfg(feature = "filesystem")] +use fluent_uri::component::Host; use fluent_uri::{ Iri, - component::Host, pct_enc::{self, DecodedChunk, EStr}, }; use icu_casemap::CaseMapperBorrowed; @@ -73,48 +75,12 @@ pub(crate) fn canonicalize_iri(iri: Iri<&str>) -> Result) -> String { - let normalized = iri.normalize(); - let with_idn = match punycode_host(&normalized) { - Ok(iri) => iri, - Err(e) => { - log::debug!("IRI `{iri}` failed punycode host conversion: {e}"); - normalized.to_string() - } - }; - - // For `http`/`https` with an empty path, WHATWG URL serialization - // produces a `/` before any query/fragment; `fluent_uri::normalize` - // deliberately leaves the path untouched, so apply the fixup here. - // Scheme and path are read from `normalized` because `punycode_host` - // only edits the host. - let scheme = normalized.scheme(); - let needs_root_slash = - (scheme == SCHEME_HTTP || scheme == SCHEME_HTTPS) && normalized.path().as_str().is_empty(); - let final_string = if needs_root_slash { - match with_idn.find(['?', '#']) { - Some(i) => format!("{}/{}", &with_idn[..i], &with_idn[i..]), - None => format!("{with_idn}/"), - } - } else { - with_idn - }; - - debug_assert!( - Iri::parse(final_string.as_str()).is_ok(), - "canonical IRI must remain valid" - ); - final_string -} - /// Replace a non-ASCII RegName host with its `domainToASCII` (Punycode) form. /// IPv4, IPv6 literals, and already-ASCII RegNames pass through untouched. /// Returns the resulting serialization as an owned `String`; the rewrite is a /// localized splice on a known-valid IRI and does not rebuild via the IRI /// builder (whose strict typestate is awkward for "change only the host"). +#[cfg(feature = "filesystem")] fn punycode_host(iri: &Iri) -> Result { let s = iri.as_str(); let Some(authority) = iri.authority() else { @@ -139,6 +105,7 @@ fn punycode_host(iri: &Iri) -> Result { )) } +#[cfg(feature = "filesystem")] #[derive(Debug, thiserror::Error)] pub enum IriNormalizeError { #[cfg(feature = "filesystem")] diff --git a/core/src/project/utils.rs b/core/src/project/utils.rs index 920437d5d..8bd9a60df 100644 --- a/core/src/project/utils.rs +++ b/core/src/project/utils.rs @@ -9,14 +9,20 @@ use std::{ }; use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; -use fluent_uri::Iri; +use fluent_uri::{ + Iri, + pct_enc::{EString, encoder::IData}, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; use typed_path::Utf8UnixPathBuf; #[cfg(feature = "filesystem")] use zip::{self, result::ZipError}; -use crate::model::InterchangeProjectUsage; +use crate::{ + model::{InterchangeProjectUsage, InterchangeProjectUsageRaw}, + purl::{is_valid_purl_name, is_valid_purl_publisher, normalize_field}, +}; /// A file that is guaranteed to exist as long as the lifetime. /// Intended to be used with temporary files that are automatically @@ -575,6 +581,37 @@ impl Identifier { pub fn from_iri_unchecked_str(iri: &str) -> Identifier { Self(iri.to_owned()) } + + pub fn from_pub_name(publisher: &str, name: &str) -> Identifier { + Self::make_identifier_iri(publisher, name) + } + + pub fn from_interchange_usage_unchecked(usage: &InterchangeProjectUsageRaw) -> Identifier { + let InterchangeProjectUsageRaw::Resource { resource, .. } = usage; + Self(resource.to_string()) + } + + fn make_identifier_iri(publisher: impl AsRef, name: impl AsRef) -> Identifier { + let publisher = publisher.as_ref(); + let name = name.as_ref(); + debug_assert!(!publisher.is_empty()); + debug_assert!(!name.is_empty()); + + let normalized_pub = normalize_field(publisher); + let normalized_name = normalize_field(name); + + let iri = + if is_valid_purl_publisher(&normalized_pub) && is_valid_purl_name(&normalized_name) { + format!("pkg:sysand/{normalized_pub}/{normalized_name}") + } else { + let mut enc_pub = EString::::new(); + enc_pub.encode_str::(publisher); + let mut enc_name = EString::::new(); + enc_name.encode_str::(name); + format!("urn:sysand:{enc_pub}/{enc_name}") + }; + Self(iri) + } } impl AsRef for Identifier { diff --git a/core/src/resolve/combined_tests.rs b/core/src/resolve/combined_tests.rs index 2c29c3dcd..084e0424d 100644 --- a/core/src/resolve/combined_tests.rs +++ b/core/src/resolve/combined_tests.rs @@ -10,7 +10,7 @@ use indexmap::IndexMap; use crate::{ info::{InfoError, do_info}, model::{InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw}, - project::memory::InMemoryProject, + project::{memory::InMemoryProject, utils::Identifier}, resolve::{ ResolutionInfo, ResolutionOutcome, ResolveRead, combined::{CombinedResolver, NO_RESOLVER}, @@ -55,7 +55,7 @@ fn single_project_any_resolver>( uri: S, project: InMemoryProject, ) -> Option> { - let uri = Iri::parse(uri.as_ref().to_string()).unwrap(); + let uri = Identifier::from_iri_unchecked_str(uri.as_ref()); let mut projects = HashMap::new(); @@ -71,7 +71,7 @@ fn multiple_projects_any_resolver>( uri: S, projects: Vec, ) -> Option> { - let uri = Iri::parse(uri.as_ref().to_string()).unwrap(); + let uri = Identifier::from_iri_unchecked_str(uri.as_ref()); let mut projects_map = HashMap::new(); projects_map.insert(uri, projects); Some(MemoryResolver { diff --git a/core/src/resolve/env.rs b/core/src/resolve/env.rs index 8d17a09f9..55009e6f0 100644 --- a/core/src/resolve/env.rs +++ b/core/src/resolve/env.rs @@ -4,7 +4,6 @@ // Resolve IRIs in an environment use crate::{ env::{ReadEnvironment, ReadEnvironmentAsync}, - model::InterchangeProjectUsage, resolve::{ResolutionInfo, ResolutionOutcome, ResolveRead, ResolveReadAsync}, }; @@ -24,21 +23,20 @@ impl ResolveRead for EnvResolver { &self, resolve: &ResolutionInfo, ) -> Result, Self::Error> { - let InterchangeProjectUsage::Resource { resource: uri, .. } = resolve.usage(); - - let versions = self.env.versions(uri)?; + let id = resolve.id().into_string(); + let versions = self.env.versions(&id)?; let projects: Self::ResolvedStorages = versions .into_iter() .map( |version| -> Result { - self.env.get_project(uri.clone(), version?) + self.env.get_project(&id, version?) }, ) .collect(); if projects.is_empty() { Ok(ResolutionOutcome::NotFound { - reason: format!("no versions of `{uri}` found in environment"), + reason: String::from("environment does not contain this project"), }) } else { Ok(ResolutionOutcome::Resolved(projects)) @@ -66,19 +64,18 @@ impl ResolveReadAsync for EnvResolver { ) -> Result, Self::Error> { use futures::StreamExt as _; - let InterchangeProjectUsage::Resource { resource: uri, .. } = resolve.usage(); - - let versions: Vec> = self.env.versions_async(uri).await?.collect().await; + let id = resolve.id().into_string(); + let versions: Vec> = self.env.versions_async(&id).await?.collect().await; if versions.is_empty() { return Ok(ResolutionOutcome::NotFound { - reason: format!("no versions of `{uri}` found in environment"), + reason: String::from("environment does not contain this project"), }); } let projects = futures::future::join_all( versions .into_iter() - .map(|version| async { self.env.get_project_async(uri.clone(), version?).await }), + .map(|version| async { self.env.get_project_async(&id, version?).await }), ) .await; diff --git a/core/src/resolve/file.rs b/core/src/resolve/file.rs index acca0e9f7..220dcaa5f 100644 --- a/core/src/resolve/file.rs +++ b/core/src/resolve/file.rs @@ -75,7 +75,7 @@ fn try_file_uri_to_path( } impl FileResolver { - fn resolve_platform_path( + fn check_sandbox( &self, path: Utf8PathBuf, ) -> Result, FileResolverError> { @@ -96,28 +96,14 @@ impl FileResolver { if !found { return Ok(ResolutionOutcome::Unresolvable { reason: format!( - "refusing to resolve path `{}`, is not inside in any of the allowed directories\n{}", - path, + "refusing to resolve path `{path}`, is not inside in any of the allowed directories\n{}", sandbox_roots_canonical.join("; "), ), }); } } - Ok(ResolutionOutcome::Resolved(path)) } - - fn resolve_general( - &self, - uri: &fluent_uri::Iri, - ) -> Result, FileResolverError> { - match try_file_uri_to_path(uri)? { - Some(path) => self.resolve_platform_path(path), - None => Ok(ResolutionOutcome::UnsupportedUsageType { - reason: format!("`{uri}` is not a file URL"), - }), - } - } } #[derive(Debug)] @@ -313,24 +299,25 @@ impl ResolveRead for FileResolver { &self, resolve: &ResolutionInfo, ) -> Result, Self::Error> { - let InterchangeProjectUsage::Resource { resource: uri, .. } = resolve.usage(); - - Ok(match self.resolve_general(uri)? { - ResolutionOutcome::Resolved(path) => ResolutionOutcome::Resolved(vec![ - Ok(FileResolverProject::LocalSrcProject( - LocalSrcProject::new_access(path.clone(), None), - )), - Ok(FileResolverProject::LocalKParProject( - LocalKParProject::new(path, KparInnerPath::Guess, None, None), - )), - ]), - ResolutionOutcome::UnsupportedUsageType { reason } => { - ResolutionOutcome::UnsupportedUsageType { reason } - } - ResolutionOutcome::Unresolvable { reason } => { - ResolutionOutcome::Unresolvable { reason } + let InterchangeProjectUsage::Resource { resource: url, .. } = resolve.usage(); + + match try_file_uri_to_path(url)? { + Some(path) => { + let res = self.check_sandbox(path)?; + Ok(res.map(|path| { + vec![ + Ok(FileResolverProject::LocalSrcProject( + LocalSrcProject::new_access(path.clone(), None), + )), + Ok(FileResolverProject::LocalKParProject( + LocalKParProject::new(path, KparInnerPath::Guess, None, None), + )), + ] + })) } - ResolutionOutcome::NotFound { reason } => ResolutionOutcome::NotFound { reason }, - }) + None => Ok(ResolutionOutcome::UnsupportedUsageType { + reason: String::from("resource is not a file URL"), + }), + } } } diff --git a/core/src/resolve/gix_git.rs b/core/src/resolve/gix_git.rs index 0923c7bdb..8d86b2aaa 100644 --- a/core/src/resolve/gix_git.rs +++ b/core/src/resolve/gix_git.rs @@ -33,46 +33,43 @@ impl ResolveRead for GitResolver { &self, resolve: &ResolutionInfo, ) -> Result, Self::Error> { - let InterchangeProjectUsage::Resource { resource: uri, .. } = resolve.usage(); + match resolve.usage() { + InterchangeProjectUsage::Resource { + resource, + version_constraint: _, + } => { + let scheme = resource.scheme(); - let scheme = uri.scheme(); + if ![ + SCHEME_HTTP, + SCHEME_HTTPS, + SCHEME_FILE, + SCHEME_SSH, + SCHEME_GIT_HTTP, + SCHEME_GIT_HTTPS, + SCHEME_GIT_FILE, + SCHEME_GIT_SSH, + ] + .contains(&scheme) + { + return Ok(ResolutionOutcome::UnsupportedUsageType { + reason: format!( + "url scheme `{scheme}` of IRI `{resource}` is not known to be git-compatible" + ), + }); + } - if ![ - SCHEME_HTTP, - SCHEME_HTTPS, - SCHEME_FILE, - SCHEME_SSH, - SCHEME_GIT_HTTP, - SCHEME_GIT_HTTPS, - SCHEME_GIT_FILE, - SCHEME_GIT_SSH, - ] - .contains(&scheme) - { - return Ok(ResolutionOutcome::UnsupportedUsageType { - reason: format!( - "url scheme `{}` of IRI `{}` is not known to be git-compatible", - scheme, - uri.as_str() - ), - }); - } - - Ok(ResolutionOutcome::Resolved(std::iter::once( - // TODO: use trim_prefix() once it's stable - GixDownloadedProject::new(uri.as_str().strip_prefix("git+").unwrap_or(uri.as_str())) - .map_err(|e| e.into()), - ))) - } - - fn resolve_read_raw>( - &self, - uri: S, - ) -> Result, Self::Error> { - if let Some(stripped_uri) = uri.as_ref().strip_prefix("git+") { - self.default_resolve_read_raw(stripped_uri) - } else { - self.default_resolve_read_raw(uri) + Ok(ResolutionOutcome::Resolved(std::iter::once( + // TODO: use trim_prefix() once it's stable + GixDownloadedProject::new( + resource + .as_str() + .strip_prefix("git+") + .unwrap_or(resource.as_str()), + ) + .map_err(|e| e.into()), + ))) + } } } } diff --git a/core/src/resolve/gix_git_tests.rs b/core/src/resolve/gix_git_tests.rs index ba2e637e7..77fa4acb0 100644 --- a/core/src/resolve/gix_git_tests.rs +++ b/core/src/resolve/gix_git_tests.rs @@ -1,18 +1,28 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: © 2026 Sysand contributors -use crate::resolve::{ResolutionOutcome, ResolveRead, gix_git::GitResolver}; +use fluent_uri::Iri; + +use crate::resolve::{ResolutionInfo, ResolutionOutcome, ResolveRead, gix_git::GitResolver}; fn un_once(x: &mut std::iter::Once) -> T { x.next().unwrap() } +fn resolve( + resolver: &R, + iri: &str, +) -> Result, R::Error> { + let resolve = ResolutionInfo::iri(Iri::parse(iri).unwrap().into()); + resolver.resolve_read(&resolve) +} + #[test] fn basic_url_examples() -> Result<(), Box> { - let resolver = GitResolver {}; + let res = GitResolver {}; let ResolutionOutcome::Resolved(mut one_http_proj) = - resolver.resolve_read_raw("http://www.example.com/proj")? + resolve(&res, "http://www.example.com/proj")? else { panic!("expected http url to resolve"); }; @@ -22,7 +32,7 @@ fn basic_url_examples() -> Result<(), Box> { ); let ResolutionOutcome::Resolved(mut one_https_proj) = - resolver.resolve_read_raw("https://www.example.com/proj")? + resolve(&res, "https://www.example.com/proj")? else { panic!("expected https url to resolve"); }; @@ -32,7 +42,7 @@ fn basic_url_examples() -> Result<(), Box> { ); let ResolutionOutcome::Resolved(mut one_ssh_proj) = - resolver.resolve_read_raw("ssh://www.example.com/proj")? + resolve(&res, "ssh://www.example.com/proj")? else { panic!("expected ssh url to resolve"); }; @@ -42,7 +52,7 @@ fn basic_url_examples() -> Result<(), Box> { ); let ResolutionOutcome::Resolved(mut one_file_proj) = - resolver.resolve_read_raw("file://www.example.com/proj")? + resolve(&res, "file://www.example.com/proj")? else { panic!("expected file url to resolve"); }; @@ -52,7 +62,7 @@ fn basic_url_examples() -> Result<(), Box> { ); let ResolutionOutcome::Resolved(mut one_git_http_proj) = - resolver.resolve_read_raw("git+http://www.example.com/proj")? + resolve(&res, "git+http://www.example.com/proj")? else { panic!("expected git+http url to resolve"); }; @@ -62,7 +72,7 @@ fn basic_url_examples() -> Result<(), Box> { ); let ResolutionOutcome::Resolved(mut one_git_https_proj) = - resolver.resolve_read_raw("git+https://www.example.com/proj")? + resolve(&res, "git+https://www.example.com/proj")? else { panic!("expected git+https url to resolve"); }; @@ -72,7 +82,7 @@ fn basic_url_examples() -> Result<(), Box> { ); let ResolutionOutcome::Resolved(mut one_git_ssh_proj) = - resolver.resolve_read_raw("git+ssh://www.example.com/proj")? + resolve(&res, "git+ssh://www.example.com/proj")? else { panic!("expected git+ssh url to resolve"); }; @@ -82,7 +92,7 @@ fn basic_url_examples() -> Result<(), Box> { ); let ResolutionOutcome::Resolved(mut one_git_file_proj) = - resolver.resolve_read_raw("git+file://www.example.com/proj")? + resolve(&res, "git+file://www.example.com/proj")? else { panic!("expected git+file url to resolve"); }; diff --git a/core/src/resolve/memory.rs b/core/src/resolve/memory.rs index 023c5a5d9..c81eb0c36 100644 --- a/core/src/resolve/memory.rs +++ b/core/src/resolve/memory.rs @@ -3,24 +3,24 @@ use std::{collections::HashMap, convert::Infallible}; -use fluent_uri::{Iri, component::Scheme}; +use fluent_uri::component::Scheme; use crate::{ model::InterchangeProjectUsage, - project::ProjectRead, + project::{ProjectRead, utils::Identifier}, resolve::{ResolutionInfo, ResolutionOutcome, ResolveRead}, }; #[derive(Debug)] pub struct MemoryResolver { pub iri_predicate: Predicate, - pub projects: HashMap, Vec>, + pub projects: HashMap>, } -impl FromIterator<(Iri, Vec)> +impl FromIterator<(Identifier, Vec)> for MemoryResolver { - fn from_iter, Vec)>>(iter: T) -> Self { + fn from_iter)>>(iter: T) -> Self { Self { iri_predicate: AcceptAll {}, projects: HashMap::from_iter(iter), @@ -28,38 +28,31 @@ impl FromIterator<(Iri, Vec)> } } -impl From<[(Iri, Vec); N]> +impl From<[(Identifier, Vec); N]> for MemoryResolver { - fn from(value: [(Iri, Vec); N]) -> Self { + fn from(value: [(Identifier, Vec); N]) -> Self { Self::from_iter(value) } } -impl From, Vec)>> +impl From)>> for MemoryResolver { - fn from(value: Vec<(Iri, Vec)>) -> Self { + fn from(value: Vec<(Identifier, Vec)>) -> Self { Self::from_iter(value) } } pub trait IRIPredicate { - fn accept_iri(&self, iri: &Iri) -> bool; - - fn accept_iri_raw(&self, iri: &str) -> bool { - match Iri::parse(iri.to_string()) { - Ok(iri) => self.accept_iri(&iri), - Err(_) => false, - } - } + fn accept(&self, usage: &ResolutionInfo) -> bool; } #[derive(Debug)] pub struct AcceptAll {} impl IRIPredicate for AcceptAll { - fn accept_iri(&self, _iri: &Iri) -> bool { + fn accept(&self, _: &ResolutionInfo) -> bool { true } } @@ -70,8 +63,12 @@ pub struct AcceptScheme<'a> { } impl IRIPredicate for AcceptScheme<'_> { - fn accept_iri(&self, iri: &Iri) -> bool { - iri.scheme() == self.scheme + fn accept(&self, usage: &ResolutionInfo) -> bool { + let InterchangeProjectUsage::Resource { + resource, + version_constraint: _, + } = usage.usage(); + resource.scheme() == self.scheme } } @@ -88,18 +85,19 @@ impl ResolveRead &self, resolve: &ResolutionInfo, ) -> Result, Self::Error> { - let InterchangeProjectUsage::Resource { resource: uri, .. } = resolve.usage(); - - if !self.iri_predicate.accept_iri(uri) { + if !self.iri_predicate.accept(resolve) { return Ok(ResolutionOutcome::UnsupportedUsageType { - reason: format!("invalid IRI `{uri}` for this memory resolver"), + reason: String::from( + "this memory resolver is configured to not accept such a usage", + ), }); } - Ok(match self.projects.get(uri) { + let identifier = resolve.id(); + Ok(match self.projects.get(&identifier) { Some(xs) => ResolutionOutcome::Resolved(xs.iter().map(|x| Ok(x.clone())).collect()), None => ResolutionOutcome::NotFound { - reason: format!("no project found for IRI `{uri}` in this memory resolver"), + reason: String::from("project is not present in this memory resolver"), }, }) } diff --git a/core/src/resolve/mod.rs b/core/src/resolve/mod.rs index 420dfd44f..a21beca57 100644 --- a/core/src/resolve/mod.rs +++ b/core/src/resolve/mod.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: © 2025 Sysand contributors -use std::{fmt::Debug, sync::Arc}; +use std::{ + fmt::{Debug, Display}, + sync::Arc, +}; use crate::{ env::{SyncStreamIter, utils::ErrorBound}, @@ -11,6 +14,7 @@ use crate::{ }, }; +use camino::{Utf8Path, Utf8PathBuf}; use fluent_uri::Iri; use futures::stream::StreamExt as _; @@ -62,11 +66,87 @@ impl ResolutionOutcome { #[derive(Debug, Clone)] pub struct ResolutionInfo { usage: InterchangeProjectUsage, + /// Base path to resolve this usage against. Not relevant for + /// usages that do not involve filesystem paths + base_path: Option, +} + +/// Information needed to resolve a usage, which determines equivalency +/// by `Identifier`. +#[derive(Debug, Clone)] +pub struct CoalescingUsage { + usage: ResolutionInfo, + id: Identifier, } +impl CoalescingUsage { + pub fn new_usage(usage: InterchangeProjectUsage, base_path: Option) -> Self { + let usage = ResolutionInfo::new(usage, base_path); + Self { + id: usage.id(), + usage, + } + } + + pub fn to_usage(&self) -> ResolutionInfo { + self.usage.clone() + } + + pub fn to_id(&self) -> Identifier { + self.id.clone() + } + + pub fn usage(&self) -> &ResolutionInfo { + &self.usage + } + + pub fn id(&self) -> &Identifier { + &self.id + } + + pub fn into_parts(self) -> (ResolutionInfo, Identifier) { + (self.usage, self.id) + } +} + +// It is incorrect to use the derived `Hash` impl of `ResolutionInfo` +// for resolution, since pubgrub +// seemingly identifies packages by their hash, so e.g. usages of the same +// package that have different version requirements will be treated as +// referring to two distinct packages, and they will all be included in the +// solution +// Note that `PartialEq` effectively must match the behaviour of this due +// to the way we implement dependency solving +impl std::hash::Hash for CoalescingUsage { + fn hash(&self, state: &mut H) { + // Mention all fields here to remember to update whenever + // the struct changes + let Self { usage: _, id } = self; + id.hash(state); + } +} + +impl PartialEq for CoalescingUsage { + fn eq(&self, other: &Self) -> bool { + // Mention all fields here to remember to update whenever + // the struct changes + let Self { + usage: _, + id: id_self, + } = self; + let Self { + usage: _, + id: id_other, + } = other; + id_self == id_other + } +} + +impl Eq for CoalescingUsage {} + impl ResolutionInfo { - pub fn new(usage: InterchangeProjectUsage) -> Self { - Self { usage } + pub fn new(usage: InterchangeProjectUsage, base_path: Option) -> Self { + Self { usage, base_path } } pub fn iri(iri: Iri) -> Self { @@ -75,6 +155,7 @@ impl ResolutionInfo { resource: iri, version_constraint: None, }, + base_path: None, } } @@ -82,6 +163,10 @@ impl ResolutionInfo { &self.usage } + pub fn base_path(&self) -> Option<&Utf8Path> { + self.base_path.as_deref() + } + /// Identifier of this usage, to be used in lock/env. // TODO: how to take versions/requirements into account here? pub fn id(&self) -> Identifier { @@ -89,45 +174,35 @@ impl ResolutionInfo { } } -impl std::fmt::Display for ResolutionInfo { +impl Display for ResolutionInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let InterchangeProjectUsage::Resource { - resource, - version_constraint, - } = &self.usage; - write!(f, "IRI `{resource}`")?; - if let Some(vc) = version_constraint { - write!(f, " ({vc})")?; + match &self.usage { + InterchangeProjectUsage::Resource { + resource, + version_constraint, + } => { + write!(f, "IRI `{resource}`")?; + if let Some(vc) = version_constraint { + write!(f, " ({vc})")?; + } + } } Ok(()) } } +impl Display for CoalescingUsage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.usage, f) + } +} + pub trait ResolveRead { type Error: ErrorBound; type ProjectStorage: ProjectRead; type ResolvedStorages: IntoIterator>; - fn default_resolve_read_raw>( - &self, - uri: S, - ) -> Result, Self::Error> { - match fluent_uri::Iri::parse(uri.as_ref().to_string()) { - Ok(uri) => self.resolve_read(&ResolutionInfo::iri(uri)), - Err((err, val)) => Ok(ResolutionOutcome::UnsupportedUsageType { - reason: format!("unable to parse IRI `{}`: {}", val, err), - }), - } - } - - fn resolve_read_raw>( - &self, - uri: S, - ) -> Result, Self::Error> { - self.default_resolve_read_raw(uri) - } - fn resolve_read( &self, resolve: &ResolutionInfo, @@ -148,27 +223,6 @@ pub trait ResolveReadAsync { type ProjectStorage: ProjectReadAsync; type ResolvedStorages: futures::Stream>; - fn default_resolve_read_raw_async>( - &self, - uri: S, - ) -> impl Future, Self::Error>> { - async move { - match fluent_uri::Iri::parse(uri.as_ref().to_string()) { - Ok(uri) => self.resolve_read_async(&ResolutionInfo::iri(uri)).await, - Err((err, val)) => Ok(ResolutionOutcome::UnsupportedUsageType { - reason: format!("unable to parse IRI `{}`: {}", val, err), - }), - } - } - } - - fn resolve_read_raw_async>( - &self, - uri: S, - ) -> impl Future, Self::Error>> { - async move { self.default_resolve_read_raw_async(uri).await } - } - fn resolve_read_async( &self, resolve: &ResolutionInfo, diff --git a/core/src/resolve/priority_tests.rs b/core/src/resolve/priority_tests.rs index f02a860a5..ca7b5c8a9 100644 --- a/core/src/resolve/priority_tests.rs +++ b/core/src/resolve/priority_tests.rs @@ -8,9 +8,9 @@ use indexmap::IndexMap; use crate::{ model::{InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw}, - project::{ProjectRead as _, memory::InMemoryProject}, + project::{ProjectRead as _, memory::InMemoryProject, utils::Identifier}, resolve::{ - ResolutionOutcome, ResolveRead, + ResolutionInfo, ResolutionOutcome, ResolveRead, memory::{AcceptAll, MemoryResolver}, }, }; @@ -48,20 +48,22 @@ fn mock_project, T: AsRef, V: AsRef>( ) } -fn mock_resolver, InMemoryProject)>>( +fn mock_iri_resolver, InMemoryProject)>>( projects: I, ) -> MemoryResolver { MemoryResolver { iri_predicate: AcceptAll {}, - projects: HashMap::from_iter(projects.into_iter().map(|(k, v)| (k, vec![v]))), + projects: HashMap::from_iter( + projects + .into_iter() + .map(|(k, v)| (Identifier::from_iri_owned(k), vec![v])), + ), } } -fn expect_to_resolve>( - resolver: &R, - uri: S, -) -> Vec { - let resolved = resolver.resolve_read_raw(uri).unwrap(); +fn expect_to_resolve_iri(resolver: &R, uri: &str) -> Vec { + let uri = ResolutionInfo::iri(Iri::parse(uri).unwrap().into()); + let resolved = resolver.resolve_read(&uri).unwrap(); let foo_projects: Result, _> = if let ResolutionOutcome::Resolved(foo_projects) = resolved { @@ -75,29 +77,29 @@ fn expect_to_resolve>( #[test] fn resolution_priority() -> Result<(), Box> { - let higher = mock_resolver([ + let higher = mock_iri_resolver([ mock_project("urn:kpar:foo", "foo", "1.2.3"), mock_project("urn:kpar:bar", "bar", "1.2.3"), ]); - let lower = mock_resolver([ + let lower = mock_iri_resolver([ mock_project("urn:kpar:bar", "bar", "3.2.1"), mock_project("urn:kpar:baz", "baz", "3.2.1"), ]); let resolver = super::PriorityResolver::new(higher, lower); - let foos = expect_to_resolve(&resolver, "urn:kpar:foo"); + let foos = expect_to_resolve_iri(&resolver, "urn:kpar:foo"); assert_eq!(foos.len(), 1); assert_eq!(foos[0].version().unwrap(), Some("1.2.3".to_string())); - let bars = expect_to_resolve(&resolver, "urn:kpar:bar"); + let bars = expect_to_resolve_iri(&resolver, "urn:kpar:bar"); assert_eq!(bars.len(), 1); assert_eq!(bars[0].version().unwrap(), Some("1.2.3".to_string())); - let bazs = expect_to_resolve(&resolver, "urn:kpar:baz"); + let bazs = expect_to_resolve_iri(&resolver, "urn:kpar:baz"); assert_eq!(bazs.len(), 1); assert_eq!(bazs[0].version().unwrap(), Some("3.2.1".to_string())); diff --git a/core/src/resolve/reqwest_http_tests.rs b/core/src/resolve/reqwest_http_tests.rs index 1e47cbee2..bc8bcf665 100644 --- a/core/src/resolve/reqwest_http_tests.rs +++ b/core/src/resolve/reqwest_http_tests.rs @@ -5,12 +5,25 @@ use std::sync::Arc; +use fluent_uri::Iri; + use crate::{ auth::Unauthenticated, project::ProjectRead, - resolve::{ResolutionOutcome, ResolveRead, ResolveReadAsync, net_utils::create_reqwest_client}, + resolve::{ + ResolutionInfo, ResolutionOutcome, ResolveRead, ResolveReadAsync, + net_utils::create_reqwest_client, + }, }; +fn resolve( + resolver: &R, + iri: &str, +) -> Result, R::Error> { + let resolve = ResolutionInfo::iri(Iri::parse(iri).unwrap().into()); + resolver.resolve_read(&resolve) +} + #[test] fn basic_http_src_url_non_lax() -> Result<(), Box> { let mut server = mockito::Server::new(); @@ -47,8 +60,7 @@ fn basic_http_src_url_non_lax() -> Result<(), Box> { .unwrap(), )); - let ResolutionOutcome::Resolved(projects) = - resolver.resolve_read_raw(format!("http://{}/foo/", host))? + let ResolutionOutcome::Resolved(projects) = resolve(&resolver, &format!("http://{host}/foo/"))? else { panic!() }; @@ -94,7 +106,7 @@ fn template_basic_http_url_lax( "http://www.example.invalid/foo" }; - let ResolutionOutcome::Resolved(projects) = resolver.resolve_read_raw(url)? else { + let ResolutionOutcome::Resolved(projects) = resolve(&resolver, url)? else { panic!() }; let projects: Vec> = diff --git a/core/src/resolve/sequential_tests.rs b/core/src/resolve/sequential_tests.rs index fdd4e77a9..8179d9d3e 100644 --- a/core/src/resolve/sequential_tests.rs +++ b/core/src/resolve/sequential_tests.rs @@ -8,9 +8,9 @@ use indexmap::IndexMap; use crate::{ model::{InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw}, - project::{ProjectRead, memory::InMemoryProject}, + project::{ProjectRead, memory::InMemoryProject, utils::Identifier}, resolve::{ - ResolutionOutcome, ResolveRead, + ResolutionInfo, ResolutionOutcome, ResolveRead, memory::{AcceptAll, MemoryResolver}, sequential::SequentialResolver, }, @@ -49,20 +49,22 @@ fn mock_project, T: AsRef, V: AsRef>( ) } -fn mock_resolver, InMemoryProject)>>( +fn mock_iri_resolver, InMemoryProject)>>( projects: I, ) -> MemoryResolver { MemoryResolver { iri_predicate: AcceptAll {}, - projects: HashMap::from_iter(projects.into_iter().map(|(k, v)| (k, vec![v]))), + projects: HashMap::from_iter( + projects + .into_iter() + .map(|(k, v)| (Identifier::from_iri_owned(k), vec![v])), + ), } } -fn expect_to_resolve>( - resolver: &R, - uri: S, -) -> Vec { - let resolved = resolver.resolve_read_raw(uri).unwrap(); +fn expect_to_resolve_iri(resolver: &R, uri: &str) -> Vec { + let uri = ResolutionInfo::iri(Iri::parse(uri).unwrap().into()); + let resolved = resolver.resolve_read(&uri).unwrap(); let foo_projects: Result, _> = if let ResolutionOutcome::Resolved(foo_projects) = resolved { @@ -76,30 +78,30 @@ fn expect_to_resolve>( #[test] fn resolution_preference() -> Result<(), Box> { - let resolver_1 = mock_resolver([ + let resolver_1 = mock_iri_resolver([ mock_project("urn:kpar:foo", "foo", "1.2.3"), mock_project("urn:kpar:bar", "bar", "1.2.3"), ]); - let resolver_2 = mock_resolver([ + let resolver_2 = mock_iri_resolver([ mock_project("urn:kpar:bar", "bar", "3.2.1"), mock_project("urn:kpar:baz", "baz", "3.2.1"), ]); let resolver = SequentialResolver::new([resolver_1, resolver_2]); - let foos = expect_to_resolve(&resolver, "urn:kpar:foo"); + let foos = expect_to_resolve_iri(&resolver, "urn:kpar:foo"); assert_eq!(foos.len(), 1); assert_eq!(foos[0].version().unwrap(), Some("1.2.3".to_string())); - let bars = expect_to_resolve(&resolver, "urn:kpar:bar"); + let bars = expect_to_resolve_iri(&resolver, "urn:kpar:bar"); assert_eq!(bars.len(), 2); assert_eq!(bars[0].version().unwrap(), Some("1.2.3".to_string())); assert_eq!(bars[1].version().unwrap(), Some("3.2.1".to_string())); - let bazs = expect_to_resolve(&resolver, "urn:kpar:baz"); + let bazs = expect_to_resolve_iri(&resolver, "urn:kpar:baz"); assert_eq!(bazs.len(), 1); assert_eq!(bazs[0].version().unwrap(), Some("3.2.1".to_string())); diff --git a/core/src/solve/pubgrub_tests.rs b/core/src/solve/pubgrub_tests.rs index f5d6f812d..94a3d8916 100644 --- a/core/src/solve/pubgrub_tests.rs +++ b/core/src/solve/pubgrub_tests.rs @@ -12,7 +12,7 @@ use crate::{ InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw, InterchangeProjectUsage, InterchangeProjectUsageRaw, }, - project::{ProjectRead, memory::InMemoryProject}, + project::{ProjectRead, memory::InMemoryProject, utils::Identifier}, resolve::{ env::EnvResolver, memory::{AcceptAll, MemoryResolver}, @@ -63,7 +63,12 @@ fn memory_resolver( iri_predicate: AcceptAll {}, projects: structure .iter() - .map(|(id, projs)| (Iri::parse(id.to_string()).unwrap(), projs.to_vec())) + .map(|(id, projs)| { + ( + Identifier::from(Iri::parse(id.to_string()).unwrap()), + projs.to_vec(), + ) + }) .collect(), } } diff --git a/core/src/stdlib.rs b/core/src/stdlib.rs index 16a262a40..c2647f445 100644 --- a/core/src/stdlib.rs +++ b/core/src/stdlib.rs @@ -3,7 +3,10 @@ use std::collections::HashMap; -use crate::project::memory::InMemoryProject; +use crate::{ + project::{memory::InMemoryProject, utils::Identifier}, + utils::ProvidedProjects, +}; const QUANTITIES_AND_UNITS_LIBRARY_INFO_20250201: &str = include_str!("stdlib_assets/20250201/quantities-and-units-library.project.json"); @@ -51,14 +54,16 @@ const SEMANTIC_LIBRARY_META_20250201: &str = // embed the .project.json and .meta.json files separately // TODO: use std::cell::Lazy (or similar), since this does not need // to be recreated on each call -pub fn known_std_libs() -> HashMap> { +pub fn known_std_libs() -> ProvidedProjects { fn entries( xs: impl IntoIterator, - ) -> HashMap> { + ) -> ProvidedProjects { let mut result = HashMap::default(); for (iri, info, meta) in xs { - let projects = result.entry(iri.to_string()).or_insert_with(Vec::new); + let projects = result + .entry(Identifier::from_iri_unchecked_str(iri)) + .or_insert_with(Vec::new); projects.push(InMemoryProject::from_info_meta( serde_json::from_str(info).unwrap(), serde_json::from_str(meta).unwrap(), diff --git a/core/src/utils.rs b/core/src/utils.rs index b93cb1336..71f845589 100644 --- a/core/src/utils.rs +++ b/core/src/utils.rs @@ -1,7 +1,11 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: © 2026 Sysand contributors -use std::{error::Error, fmt::Write as _}; +use std::{ + collections::{HashMap, HashSet}, + error::Error, + fmt::Write as _, +}; use digest::{array::Array, typenum}; use indexmap::IndexSet; @@ -10,7 +14,13 @@ use sha2::{Digest, Sha256}; use thiserror::Error; use typed_path::{Utf8UnixPath, Utf8WindowsPath}; +use crate::project::{memory::InMemoryProject, utils::Identifier}; + +pub type ProvidedProjects = HashMap>; +pub type ProvidedIdentifiers = HashSet; + pub(crate) mod scheme { + #[cfg(feature = "filesystem")] use fluent_uri::component::Scheme; #[cfg(feature = "filesystem")] pub const SCHEME_FILE: &Scheme = Scheme::new_or_panic("file"); @@ -24,7 +34,9 @@ pub(crate) mod scheme { pub const SCHEME_GIT_HTTP: &Scheme = Scheme::new_or_panic("git+http"); #[cfg(all(feature = "filesystem", feature = "networking"))] pub const SCHEME_GIT_HTTPS: &Scheme = Scheme::new_or_panic("git+https"); + #[cfg(feature = "filesystem")] pub const SCHEME_HTTP: &Scheme = Scheme::new_or_panic("http"); + #[cfg(feature = "filesystem")] pub const SCHEME_HTTPS: &Scheme = Scheme::new_or_panic("https"); } diff --git a/core/tests/filesystem_env.rs b/core/tests/filesystem_env.rs index 44b59a2f6..a87e0aaf4 100644 --- a/core/tests/filesystem_env.rs +++ b/core/tests/filesystem_env.rs @@ -11,6 +11,7 @@ mod filesystem_tests { use camino::Utf8Path; use camino_tempfile::tempdir; + use fluent_uri::Iri; use indexmap::IndexMap; use sysand_core::{ commands::env::do_env_local_dir, @@ -252,6 +253,7 @@ version = \"0.1\" fn env_manual_install() -> Result<(), Box> { let cwd = tempdir()?; let mut directory_environment = do_env_local_dir(cwd.path().join(DEFAULT_ENV_NAME))?; + let iri = Iri::parse("urn:sysand_test:1").unwrap().to_owned(); let info = InterchangeProjectInfoRaw { name: "env_manual_install".to_string(), @@ -331,7 +333,7 @@ version = \"0.1\" env: directory_environment, }; - let resolved_project = do_info("urn:sysand_test:1", &resolver)?; + let resolved_project = do_info(&iri, &resolver)?; assert_eq!(resolved_project, (info, meta)); diff --git a/core/tests/memory_env.rs b/core/tests/memory_env.rs index 1113815d1..02b374fc0 100644 --- a/core/tests/memory_env.rs +++ b/core/tests/memory_env.rs @@ -7,6 +7,7 @@ use std::{ }; use chrono::DateTime; +use fluent_uri::Iri; use indexmap::IndexMap; use semver::Version; use sysand_core::{ @@ -20,6 +21,7 @@ use sysand_core::{ project::{ ProjectMut, ProjectRead, memory::{InMemoryError, InMemoryProject}, + utils::Identifier, }, resolve::memory::{AcceptAll, MemoryResolver}, }; @@ -37,6 +39,7 @@ fn env_basic() -> Result<(), Box> { #[test] fn env_manual_install() -> Result<(), Box> { let mut memory_environment = do_env_memory()?; + let iri = Iri::parse("urn:sysand_test:1").unwrap().to_owned(); let info = InterchangeProjectInfo { name: "env_manual_install".to_string(), @@ -98,12 +101,12 @@ fn env_manual_install() -> Result<(), Box> { let resolver = MemoryResolver { iri_predicate: AcceptAll {}, projects: HashMap::from([( - fluent_uri::Iri::parse("urn:sysand_test:1")?.into(), + Identifier::from_iri_owned(iri.clone()), vec![source_project.clone()], )]), }; - let resolved_project = do_info("urn:sysand_test:1", &resolver)?; + let resolved_project = do_info(&iri, &resolver)?; assert_eq!(resolved_project, (info, meta)); diff --git a/sysand/src/commands/add.rs b/sysand/src/commands/add.rs index cc156d103..fd77d48f6 100644 --- a/sysand/src/commands/add.rs +++ b/sysand/src/commands/add.rs @@ -22,7 +22,7 @@ use sysand_core::{ utils::{relativize_path, wrapfs}, }, resolve::{ResolutionInfo, ResolutionOutcome, ResolveRead, standard::standard_resolver}, - utils::format_err, + utils::{ProvidedProjects, format_err}, }; use crate::{ @@ -262,7 +262,7 @@ fn resolve_deps, Policy: HTTPAuthentication>( auth_policy: Arc, project_root: P, project_identifiers: Option>>, - provided_iris: HashMap>, + provided_iris: ProvidedProjects, ctx: ProjectContext, ) -> Result<(), anyhow::Error> { let resolver = create_resolver( diff --git a/sysand/src/commands/clone.rs b/sysand/src/commands/clone.rs index d97131e5f..231514601 100644 --- a/sysand/src/commands/clone.rs +++ b/sysand/src/commands/clone.rs @@ -112,20 +112,15 @@ pub fn command_clone( }; if !no_deps { - let provided_iris = if !include_std { + let provided_usages = if !include_std { crate::known_std_libs() } else { HashMap::default() }; - let mut memory_projects = HashMap::default(); - for (k, v) in provided_iris.iter() { - memory_projects.insert(fluent_uri::Iri::parse(k.clone()).unwrap(), v.to_vec()); - } - let resolver = PriorityResolver::new( MemoryResolver { iri_predicate: AcceptAll {}, - projects: memory_projects, + projects: provided_usages.clone(), }, std_resolver, ); @@ -140,15 +135,16 @@ pub fn command_clone( } = sysand_core::commands::lock::do_lock_projects( [(identifiers, &project)], resolver, - &provided_iris, + &provided_usages, &ctx, )?; // If we have any std lib dependencies, they will not be installed - if !provided_iris.is_empty() - && lock - .projects - .iter() - .any(|x| x.identifiers.iter().any(|y| provided_iris.contains_key(y))) + if !provided_usages.is_empty() + && lock.projects.iter().any(|x| { + x.identifiers + .iter() + .any(|y| provided_usages.contains_key(y.as_str())) + }) { log::info!( "{GOOD}note{GOOD:#}: SysMLv2/KerML standard library packages will not be installed during sync,\n\ @@ -173,7 +169,7 @@ pub fn command_clone( project.inner().root_path(), &mut env, client, - &provided_iris, + &provided_usages, runtime, auth_policy, ctx.current_workspace.as_ref(), @@ -269,7 +265,8 @@ fn obtain_project( ' ', local_project.root_path(), ); - let (_version, storage) = get_project_version(iri, version, &std_resolver)?; + let resolve = ResolutionInfo::iri(iri.to_owned()); + let (_version, storage) = get_project_version(&resolve, version, &std_resolver)?; let (info, _meta) = clone_project(&storage, &mut local_project, true)?; log::info!( "{header}{cloned:>12}{header:#} `{}` {}", @@ -341,16 +338,15 @@ fn clone_local( Ok(()) } -/// Obtains a project identified by `iri` via `resolver`. If +/// Obtains a project identified by `resolve` via `resolver`. If /// version is given, obtains exactly that version. If not, /// obtains the latest version (including prerelease versions) pub fn get_project_version( - iri: &Iri, + resolve: &ResolutionInfo, version: Option, resolver: &R, ) -> Result<(semver::Version, R::ProjectStorage), anyhow::Error> { - let resolve_info = ResolutionInfo::iri(iri.clone()); - match resolver.resolve_read(&resolve_info)? { + match resolver.resolve_read(resolve)? { ResolutionOutcome::Resolved(alternatives) => { // If no version is supplied, choose the highest // Else, choose version that is supplied @@ -413,8 +409,8 @@ pub fn get_project_version( match candidates.len() { 0 => match version { - Some(v) => bail!(CliError::MissingProjectVersion(resolve_info.to_string(), v)), - None => bail!(CliError::MissingProject(resolve_info.to_string())), + Some(v) => bail!(CliError::MissingProjectVersion(resolve.to_string(), v)), + None => bail!(CliError::MissingProject(resolve.to_string())), }, 1 => { // Can't move out values with match @@ -430,13 +426,13 @@ pub fn get_project_version( } } ResolutionOutcome::UnsupportedUsageType { reason } => { - bail!("locator type of {resolve_info} is not supported: {reason}") + bail!("locator type of {resolve} is not supported: {reason}") } ResolutionOutcome::NotFound { reason } => { - bail!("usage {resolve_info} was not found: {reason}") + bail!("usage {resolve} was not found: {reason}") } ResolutionOutcome::Unresolvable { reason } => { - bail!("usage {resolve_info} is not resolvable: {reason}") + bail!("usage {resolve} is not resolvable: {reason}") } } } diff --git a/sysand/src/commands/env.rs b/sysand/src/commands/env.rs index 1bbe6fb3b..0572b4022 100644 --- a/sysand/src/commands/env.rs +++ b/sysand/src/commands/env.rs @@ -23,6 +23,7 @@ use sysand_core::{ utils::wrapfs, }, resolve::{ + ResolutionInfo, file::FileResolverProject, memory::{AcceptAll, MemoryResolver}, priority::PriorityResolver, @@ -80,7 +81,7 @@ pub fn command_env_install( } = resolution_opts; // TODO: should probably first check that current project exists - let provided_iris = if !include_std { + let provided_usages = if !include_std { let sysml_std = crate::known_std_libs(); if sysml_std.contains_key(iri.as_ref()) { warn_std_install(iri.as_ref()); @@ -105,15 +106,11 @@ pub fn command_env_install( auth_policy.clone(), )?; - 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()); - } let override_resolver = PriorityResolver::new( MemoryResolver::from(overrides), MemoryResolver { iri_predicate: AcceptAll {}, - projects: memory_projects, + projects: provided_usages.clone(), }, ); // TODO: Move out the runtime @@ -131,10 +128,12 @@ pub fn command_env_install( // TODO: don't use different root project resolution // mechanisms depending on no_deps if no_deps { + let id = iri.to_string(); + let resolve = ResolutionInfo::iri(iri); let (version, storage) = - crate::commands::clone::get_project_version(&iri, version, &resolver)?; + crate::commands::clone::get_project_version(&resolve, version, &resolver)?; sysand_core::commands::env::do_env_install_project( - &iri, + id, &version.to_string(), &storage, Some(storage.checksum_canonical_variant()?), @@ -156,7 +155,7 @@ pub fn command_env_install( Lock::default(), usages, resolver, - &provided_iris, + &provided_usages, &ctx, )?; // Find if we added any std lib dependencies. This relies on `Lock::default()` @@ -164,11 +163,12 @@ pub fn command_env_install( // only `iri` and `iri`'s dependencies. // This is unreachable if `iri` is an std lib, so this warning will not duplicate // the above one - if !provided_iris.is_empty() - && lock - .projects - .iter() - .any(|x| x.identifiers.iter().any(|y| provided_iris.contains_key(y))) + if !provided_usages.is_empty() + && lock.projects.iter().any(|x| { + x.identifiers + .iter() + .any(|y| provided_usages.contains_key(y.as_str())) + }) { // TODO: this could be more helpful, currently it suggests `--include-std`, // but that by itself will not work, since the project will be already installed, @@ -180,7 +180,7 @@ pub fn command_env_install( project_root, &mut ctx.env.unwrap(), client, - &provided_iris, + &provided_usages, runtime, auth_policy, ctx.current_workspace.as_ref(), @@ -245,7 +245,7 @@ pub fn command_env_install_path( bail!("path `{path}` is neither a directory nor a file"); }; - let provided_iris = if !include_std { + let provided_usages = if !include_std { let sysml_std = crate::known_std_libs(); if sysml_std.contains_key(iri.as_ref()) { warn_std_install(&iri); @@ -293,15 +293,11 @@ pub fn command_env_install_path( auth_policy.clone(), )?; - let mut memory_projects = HashMap::default(); - for (k, v) in provided_iris.iter() { - memory_projects.insert(fluent_uri::Iri::parse(k.clone()).unwrap(), v.to_vec()); - } let override_resolver = PriorityResolver::new( MemoryResolver::from(overrides), MemoryResolver { iri_predicate: AcceptAll {}, - projects: memory_projects, + projects: provided_usages.clone(), }, ); // TODO: Move out the runtime @@ -321,7 +317,7 @@ pub fn command_env_install_path( } = sysand_core::commands::lock::do_lock_projects( [(Some(vec![iri]), &project)], resolver, - &provided_iris, + &provided_usages, &ctx, )?; // FIXME: part of hack above, the project is already installed @@ -333,7 +329,7 @@ pub fn command_env_install_path( project_root, &mut ctx.env.unwrap(), client, - &provided_iris, + &provided_usages, runtime, auth_policy, ctx.current_workspace.as_ref(), diff --git a/sysand/src/commands/info.rs b/sysand/src/commands/info.rs index 27ea0c599..7fa95224a 100644 --- a/sysand/src/commands/info.rs +++ b/sysand/src/commands/info.rs @@ -17,19 +17,21 @@ use sysand_core::{ InterchangeProjectChecksumRaw, InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw, InterchangeProjectUsageRaw, }, - project::{ProjectMut, ProjectRead, any::OverrideProject, local_kpar::KparInnerPath}, + project::{ + ProjectMut, ProjectRead, any::OverrideProject, local_kpar::KparInnerPath, utils::Identifier, + }, resolve::{ file::FileResolverProject, memory::MemoryResolver, priority::PriorityResolver, standard::standard_resolver, }, style, - utils::format_err, + utils::{ProvidedIdentifiers, format_err}, }; use anstream::{print, println}; use anyhow::{Result, bail}; use fluent_uri::Iri; -use std::{collections::HashSet, sync::Arc}; +use std::sync::Arc; use sysand_core::{ info::{do_info, do_info_project}, project::utils::wrapfs, @@ -38,7 +40,7 @@ use sysand_core::{ pub fn pprint_interchange_project( info: &InterchangeProjectInfoRaw, - excluded_iris: &HashSet, + excluded_iris: &ProvidedIdentifiers, ) { let header = style::get_style_config().header; println!("{header}Name:{header:#} {}", info.name); @@ -137,7 +139,7 @@ fn interpret_project_path>(path: P) -> Result>( path: P, - excluded_iris: &HashSet, + excluded_iris: &ProvidedIdentifiers, ) -> Result<()> { let project = interpret_project_path(&path)?; match do_info_project(&project) { @@ -159,8 +161,8 @@ pub fn command_info_uri( _normalise: bool, client: reqwest_middleware::ClientWithMiddleware, index_urls: Option>, - excluded_iris: &HashSet, - overrides: Vec<(Iri, Vec>)>, + excluded_iris: &ProvidedIdentifiers, + overrides: Vec<(Identifier, Vec>)>, runtime: Arc, auth_policy: Arc, ctx: ProjectContext, @@ -229,7 +231,7 @@ pub fn command_info_verb_uri( numbered: bool, client: reqwest_middleware::ClientWithMiddleware, index_urls: Option>, - overrides: Vec<(Iri, Vec>)>, + overrides: Vec<(Identifier, Vec>)>, runtime: Arc, auth_policy: Arc, ctx: ProjectContext, diff --git a/sysand/src/commands/lock.rs b/sysand/src/commands/lock.rs index 8771fd627..3ba325e68 100644 --- a/sysand/src/commands/lock.rs +++ b/sysand/src/commands/lock.rs @@ -19,6 +19,7 @@ use sysand_core::{ standard::{StandardResolver, standard_resolver}, }, stdlib::known_std_libs, + utils::ProvidedProjects, }; use typed_path::Utf8UnixPath; @@ -36,7 +37,7 @@ pub fn command_lock, Policy: HTTPAuthentication, R: AsRef auth_policy: Arc, ctx: &ProjectContext, ) -> Result { - let provided_iris = if !resolution_opts.include_std { + let provided_usages = if !resolution_opts.include_std { known_std_libs() } else { HashMap::default() @@ -47,7 +48,7 @@ pub fn command_lock, Policy: HTTPAuthentication, R: AsRef &project_root, ctx, // TODO: avoid expensive clone here - provided_iris.clone(), + provided_usages.clone(), client, runtime, auth_policy, @@ -70,7 +71,7 @@ pub fn command_lock, Policy: HTTPAuthentication, R: AsRef &path, &project_root, alias_iris, - &provided_iris, + &provided_usages, wrapped_resolver, ctx, )?; @@ -90,7 +91,7 @@ pub fn create_resolver, Policy: HTTPAuthentication>( config: &Config, project_root: R, ctx: &ProjectContext, - provided_iris: HashMap>, + provided_usages: ProvidedProjects, client: reqwest_middleware::ClientWithMiddleware, runtime: Arc, auth_policy: Arc, @@ -130,18 +131,11 @@ pub fn create_resolver, Policy: HTTPAuthentication>( auth_policy.clone(), )?; - // TODO: add fn next to known_std_libs() to get this structure directly - // it is created in most? all? places where `known_std_libs()` is used - let mut memory_projects = HashMap::default(); - for (k, v) in provided_iris { - memory_projects.insert(fluent_uri::Iri::parse(k).unwrap(), v); - } - let override_resolver = PriorityResolver::new( MemoryResolver::from(overrides), MemoryResolver { iri_predicate: AcceptAll {}, - projects: memory_projects, + projects: provided_usages, }, ); let wrapped_resolver = PriorityResolver::new( diff --git a/sysand/src/commands/sync.rs b/sysand/src/commands/sync.rs index 142fa7b53..be0c656ef 100644 --- a/sysand/src/commands/sync.rs +++ b/sysand/src/commands/sync.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: © 2025 Sysand contributors -use std::{collections::HashMap, num::NonZeroU64, sync::Arc}; +use std::{num::NonZeroU64, sync::Arc}; use anyhow::Result; use camino::Utf8Path; @@ -17,12 +17,12 @@ use sysand_core::{ gix_git_download::{GixDownloadedError, GixDownloadedProject}, local_kpar::{KparInnerPath, LocalKParProject}, local_src::LocalSrcProject, - memory::InMemoryProject, reqwest_kpar_download::{ ReqwestIndexKparDownloadedProject, ReqwestRemoteKparDownloadedProject, }, reqwest_src::ReqwestSrcProjectAsync, }, + utils::ProvidedProjects, workspace::Workspace, }; @@ -32,7 +32,7 @@ pub fn command_sync, Policy: HTTPAuthentication>( project_root: P, env: &mut LocalDirectoryEnvironment, client: reqwest_middleware::ClientWithMiddleware, - provided_iris: &HashMap>, + provided_usages: &ProvidedProjects, runtime: Arc, auth_policy: Arc, ws: Option<&Workspace>, @@ -127,7 +127,7 @@ pub fn command_sync, Policy: HTTPAuthentication>( GixDownloadedProject::new(remote_git) }, ), - provided_iris, + provided_usages, )?; env.merge_lock(lock, ws); diff --git a/sysand/src/lib.rs b/sysand/src/lib.rs index 31d22bfcc..0289d92a6 100644 --- a/sysand/src/lib.rs +++ b/sysand/src/lib.rs @@ -38,7 +38,7 @@ use sysand_core::{ any::{AnyProject, OverrideProject}, local_src::LocalSrcProject, reference::ProjectReference, - utils::wrapfs, + utils::{Identifier, wrapfs}, }, resolve::net_utils::create_reqwest_client, stdlib::known_std_libs, @@ -409,7 +409,7 @@ pub fn run_cli(args: cli::Args) -> Result<()> { } } Command::Sync { resolution_opts } => { - let provided_iris = if !resolution_opts.include_std { + let provided_usages = if !resolution_opts.include_std { known_std_libs() } else { HashMap::default() @@ -451,7 +451,7 @@ pub fn run_cli(args: cli::Args) -> Result<()> { project_root, &mut local_environment, client, - &provided_iris, + &provided_usages, runtime, auth_policy, ctx.current_workspace.as_ref(), @@ -481,7 +481,7 @@ pub fn run_cli(args: cli::Args) -> Result<()> { default_index, )?) }; - let excluded_iris: HashSet<_> = if !include_std { + let excluded_usages: HashSet<_> = if !include_std { known_std_libs().into_keys().collect() } else { HashSet::default() @@ -562,7 +562,9 @@ pub fn run_cli(args: cli::Args) -> Result<()> { numbered, ) } - None => command_info_path(current_project.root_path(), &excluded_iris), + None => { + command_info_path(current_project.root_path(), &excluded_usages) + } } } else { bail!( @@ -575,7 +577,7 @@ pub fn run_cli(args: cli::Args) -> Result<()> { !no_normalise, client, index_urls, - &excluded_iris, + &excluded_usages, overrides, runtime, auth_policy, @@ -596,7 +598,7 @@ pub fn run_cli(args: cli::Args) -> Result<()> { ctx, ) } - (Location::Path(path), None) => command_info_path(&path, &excluded_iris), + (Location::Path(path), None) => command_info_path(&path, &excluded_usages), (Location::Path(path), Some(subcommand)) => { let numbered = subcommand.numbered(); @@ -785,7 +787,7 @@ fn get_log_level(verbose: bool, quiet: bool) -> log::LevelFilter { } } -pub type Overrides = Vec<(Iri, Vec>)>; +pub type Overrides = Vec<(Identifier, Vec>)>; pub fn get_overrides, Policy: HTTPAuthentication>( config: &Config, @@ -807,7 +809,10 @@ pub fn get_overrides, Policy: HTTPAuthentication>( runtime.clone(), )?)); } - overrides.push((Iri::parse(identifier.as_str())?.into(), projects)); + overrides.push(( + Identifier::from_iri(&Iri::parse(identifier.as_str())?), + projects, + )); } } Ok(overrides) diff --git a/sysand/tests/cli_info.rs b/sysand/tests/cli_info.rs index cfdef5efa..a4a3edae6 100644 --- a/sysand/tests/cli_info.rs +++ b/sysand/tests/cli_info.rs @@ -861,7 +861,7 @@ fn info_basic_index_url() -> Result<(), Box> { )?; out.assert().failure().stderr(predicate::str::contains( - "failed to resolve IRI `urn:kpar:other`: no resolver was able to resolve the project", + "IRI `urn:kpar:other` was not found: no resolver was able to resolve the project", )); config_mock.assert(); missing_versions_mock.assert(); @@ -1020,7 +1020,7 @@ fn info_multi_index_url_noauth() -> Result<(), Box> { )?; out.assert().failure().stderr(predicate::str::contains( - "failed to resolve IRI `urn:kpar:other`: no resolver was able to resolve the project", + "IRI `urn:kpar:other` was not found: no resolver was able to resolve the project", )); config_mock.assert(); config_mock_alt.assert(); @@ -1224,7 +1224,7 @@ fn info_multi_index_url_auth() -> Result<(), Box> { )?; out.assert().failure().stderr(predicate::str::contains( - "failed to resolve IRI `urn:kpar:other`", + "IRI `urn:kpar:other` was not found: no resolver was able to resolve the project", )); config_mock.assert(); config_mock_alt.assert();