verifier/
resolver.rs

1use camino::{Utf8Path, Utf8PathBuf};
2use itertools::Itertools;
3use log::debug;
4use scarb_metadata::{Metadata, MetadataCommand, PackageMetadata};
5use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
6use thiserror::Error;
7use url::Url;
8use walkdir::WalkDir;
9
10#[derive(Debug, Error)]
11pub enum Error {
12    #[error("Couldn't parse {name} path: {path}")]
13    DependencyPath { name: String, path: String },
14
15    #[error("scarb metadata failed for {name}: {path}")]
16    MetadataError { name: String, path: PathBuf },
17
18    #[error(transparent)]
19    Utf8(#[from] camino::FromPathBufError),
20}
21
22/// # Errors
23///
24/// Will return `Err` if it can't read files from the directory that
25/// metadata points to.
26pub fn gather_packages(
27    metadata: &Metadata,
28    packages: &mut Vec<PackageMetadata>,
29) -> Result<(), Error> {
30    let mut workspace_packages: Vec<PackageMetadata> = metadata
31        .packages
32        .clone()
33        .into_iter()
34        .filter(|package_meta| metadata.workspace.members.contains(&package_meta.id))
35        .filter(|package_meta| !packages.contains(package_meta))
36        .collect();
37
38    let workspace_packages_names = workspace_packages
39        .iter()
40        .map(|package| package.name.clone())
41        .collect_vec();
42
43    // find all dependencies listed by path
44    let mut dependencies: HashMap<String, PathBuf> = HashMap::new();
45    for package in &workspace_packages {
46        for dependency in &package.dependencies {
47            let name = &dependency.name;
48            let url = Url::parse(&dependency.source.repr).map_err(|_| Error::DependencyPath {
49                name: name.clone(),
50                path: dependency.source.repr.clone(),
51            })?;
52
53            if url.scheme().starts_with("path") {
54                let path = url.to_file_path().map_err(|()| Error::DependencyPath {
55                    name: name.clone(),
56                    path: dependency.source.repr.clone(),
57                })?;
58                dependencies.insert(name.clone(), path);
59            }
60        }
61    }
62
63    packages.append(&mut workspace_packages);
64
65    // filter out dependencies already covered by workspace
66    let out_of_workspace_dependencies: HashMap<&String, &PathBuf> = dependencies
67        .iter()
68        .filter(|&(k, _)| !workspace_packages_names.contains(k))
69        .collect();
70
71    for (name, manifest) in out_of_workspace_dependencies {
72        let new_meta = MetadataCommand::new()
73            .json()
74            .manifest_path(manifest)
75            .exec()
76            .map_err(|_| Error::MetadataError {
77                name: name.clone(),
78                path: manifest.clone(),
79            })?;
80        gather_packages(&new_meta, packages)?;
81    }
82
83    Ok(())
84}
85
86/// # Errors
87///
88/// Will return `Err` if it can't read files from the directory that
89/// metadata points to.
90pub fn package_sources(package_metadata: &PackageMetadata) -> Result<Vec<Utf8PathBuf>, Error> {
91    debug!("Collecting sources for package: {}", package_metadata.name);
92    debug!("Package root: {}", package_metadata.root);
93    debug!("Package manifest: {}", package_metadata.manifest_path);
94
95    let mut sources: Vec<Utf8PathBuf> = WalkDir::new(package_metadata.root.clone())
96        .into_iter()
97        .filter_map(std::result::Result::ok)
98        .filter(|f| f.file_type().is_file())
99        .filter(|f| {
100            // Include Cairo files
101            if let Some(ext) = f.path().extension() {
102                if ext == OsStr::new(CAIRO_EXT) {
103                    return true;
104                }
105            }
106
107            // Include Scarb.toml files (being more explicit)
108            if f.file_name() == OsStr::new("Scarb.toml") {
109                return true;
110            }
111
112            false
113        })
114        .map(walkdir::DirEntry::into_path)
115        .map(Utf8PathBuf::try_from)
116        .try_collect()?;
117
118    // Ensure the package's own manifest is included
119    if !sources.contains(&package_metadata.manifest_path) {
120        sources.push(package_metadata.manifest_path.clone());
121    }
122
123    let package_root = &package_metadata.root;
124
125    if let Some(lic) = package_metadata
126        .manifest_metadata
127        .license_file
128        .as_ref()
129        .map(Utf8Path::new)
130        .map(Utf8Path::to_path_buf)
131    {
132        sources.push(package_root.join(lic));
133    }
134
135    if let Some(readme) = package_metadata
136        .manifest_metadata
137        .readme
138        .as_deref()
139        .map(Utf8Path::new)
140        .map(Utf8Path::to_path_buf)
141    {
142        sources.push(package_root.join(readme));
143    }
144
145    Ok(sources)
146}
147
148pub fn biggest_common_prefix<P: AsRef<Utf8Path> + Clone>(
149    paths: &[Utf8PathBuf],
150    first_guess: P,
151) -> Utf8PathBuf {
152    let ancestors = Utf8Path::ancestors(first_guess.as_ref());
153    let mut biggest_prefix: &Utf8Path = first_guess.as_ref();
154    for prefix in ancestors {
155        if paths.iter().all(|src| src.starts_with(prefix)) {
156            biggest_prefix = prefix;
157            break;
158        }
159    }
160    biggest_prefix.to_path_buf()
161}
162
163const CAIRO_EXT: &str = "cairo";