Skip to content
Draft
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
11 changes: 11 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 core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ tokio = { version = "1.48.0", default-features = false, features = ["rt", "io-ut
bytes = { version = "1.11.0", default-features = false }
toml_edit = { version = "0.23.9", features = ["serde"] }
globset = { version = "0.4.18", default-features = false }
packageurl = "0.6.0"

# Use native TLS only on Windows and Apple OSs
[target.'cfg(any(target_os = "windows", target_vendor = "apple"))'.dependencies]
Expand Down
7 changes: 6 additions & 1 deletion core/src/commands/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,10 @@ pub fn enumerate_projects_lock<Env: ReadEnvironment>(
Vec<<Env as ReadEnvironment>::InterchangeProjectRead>,
ResolutionError<<Env as ReadEnvironment>::ReadError>,
> {
lock.resolve_projects(env)
let projects = lock
.resolve_projects(env)?
.into_iter()
.filter_map(|(_, project_read)| project_read)
.collect();
Ok(projects)
}
157 changes: 157 additions & 0 deletions core/src/env/local_directory/manifest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// SPDX-FileCopyrightText: © 2025 Sysand contributors <opensource@sensmetry.com>
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::{collections::HashMap, fmt::Display, num::TryFromIntError};

use camino::{Utf8Path, Utf8PathBuf};
use thiserror::Error;
use toml_edit::{Array, ArrayOfTables, DocumentMut, Item, Table, Value, value};

use crate::{
commands::sources::{LocalSourcesError, do_sources_local_src_project_no_deps},
env::local_directory::{LocalDirectoryEnvironment, LocalReadError},
lock::{Lock, ResolutionError, Source, multiline_list},
project::{
local_src::LocalSrcProject,
utils::{FsIoError, wrapfs},
},
};

#[derive(Debug, Error)]
pub enum ResolvedManifestError {
#[error(transparent)]
ResolutionError(#[from] ResolutionError<LocalReadError>),
#[error("too many dependencies, unable to convert to i64: {0}")]
TooManyDependencies(TryFromIntError),
Comment on lines +24 to +25

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is achievable in practice as memory would run out first. If an integer conversion fails, it's far more likely because the manifest is corrupted. Additionally, negative values should also be an error.

Suggested change
#[error("too many dependencies, unable to convert to i64: {0}")]
TooManyDependencies(TryFromIntError),
#[error("invalid dependency index, expected an unsigned integer: {0}")]
InvalidDependencyIndex(TryFromIntError),

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'r right that it can't really happen on any normal file system, but if it does somehow get triggered I think it's likely because someone messed up the Lock::resolve_projects logic. Might be better to just panic here to indicate that it's almost certainly a bug and not a user error.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd agree that panic is fine here since the manifest should never be edited manually. And if the manifest is already corrupted, there's little we can do anyway but to ask the user to regenerate it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is effectively unreachable in any remotely reasonable scenario, so panic is the correct behaviour.

#[error(transparent)]
LocalSources(#[from] LocalSourcesError),
#[error(transparent)]
Canonicalization(#[from] Box<FsIoError>),
}

impl Lock {
pub fn to_resolved_manifest<P: AsRef<Utf8Path>>(
&self,
env: &LocalDirectoryEnvironment,
root_path: P,
) -> Result<ResolvedManifest, ResolvedManifestError> {
let resolved_projects = self.resolve_projects(env)?;

let indices = resolved_projects
.iter()
.map(|(p, _)| p)
.enumerate()
.flat_map(|(num, p)| p.identifiers.iter().map(move |iri| (iri.clone(), num)))
.map(|(iri, num)| i64::try_from(num).map(|num| (iri, num)))
.collect::<Result<Vec<_>, _>>()
.map_err(ResolvedManifestError::TooManyDependencies)?;
let indices = HashMap::<String, i64>::from_iter(indices);

let mut projects = vec![];
for (project, storage) in resolved_projects {
let usages = project
.usages
.iter()
.filter_map(|usage| indices.get(&usage.resource))
.copied()
.collect();
let purl = project.get_package_url();
let publisher = purl
.as_ref()
.and_then(|p| p.namespace().map(|ns| ns.to_owned()));
let name = purl.as_ref().map(|p| p.name().to_owned()).or(project.name);

if let Some(storage) = storage {
let directory = storage.root_path();
projects.push(ResolvedProject {
publisher,
name,
location: ResolvedLocation::Directory(directory),
usages,
});
} else if let [Source::Editable { editable }, ..] = project.sources.as_slice() {
let project_path = root_path.as_ref().join(editable.as_str());
let editable_project = LocalSrcProject {
project_path: wrapfs::canonicalize(project_path)?,
};
let files = do_sources_local_src_project_no_deps(&editable_project, true)?
.into_iter()
.collect();
projects.push(ResolvedProject {
publisher,
name,
location: ResolvedLocation::Files(files),
usages,
});
}
}

Ok(ResolvedManifest { projects })
}
}

#[derive(Debug)]
pub struct ResolvedManifest {
pub projects: Vec<ResolvedProject>,
}

impl Display for ResolvedManifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_toml())
}
}

impl ResolvedManifest {
pub fn to_toml(&self) -> DocumentMut {
let mut doc = DocumentMut::new();
let mut projects = ArrayOfTables::new();
for project in &self.projects {
projects.push(project.to_toml());
}
doc.insert("project", Item::ArrayOfTables(projects));

doc
}
}

#[derive(Debug)]
pub enum ResolvedLocation {
Directory(Utf8PathBuf),
Files(Vec<Utf8PathBuf>),
}

#[derive(Debug)]
pub struct ResolvedProject {
pub publisher: Option<String>,
pub name: Option<String>,
pub location: ResolvedLocation,
pub usages: Vec<i64>,
}

impl ResolvedProject {
pub fn to_toml(&self) -> Table {
let mut table = Table::new();
if let Some(publisher) = &self.publisher {
table.insert("publisher", value(publisher));
}
if let Some(name) = &self.name {
table.insert("name", value(name));
}
match &self.location {
ResolvedLocation::Directory(dir) => {
table.insert("directory", value(dir.as_str()));
}
ResolvedLocation::Files(files) => {
let file_iter = files.iter().map(|f| Value::from(f.as_str()));
if !files.is_empty() {
table.insert("files", value(multiline_list(file_iter)));
} else {
table.insert("files", value(Array::from_iter(file_iter)));
}
}
}
let usages = Array::from_iter(self.usages.iter().copied().map(Value::from));
table.insert("usages", value(usages));
table
}
}
Loading
Loading