Skip to main content

cargo/ops/
cargo_read_manifest.rs

1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::io;
4use std::path::{Path, PathBuf};
5
6use log::{info, trace};
7
8use crate::core::{EitherManifest, Package, PackageId, SourceId};
9use crate::util::errors::CargoResult;
10use crate::util::important_paths::find_project_manifest_exact;
11use crate::util::toml::read_manifest;
12use crate::util::{self, Config};
13
14pub fn read_package(
15    path: &Path,
16    source_id: SourceId,
17    config: &Config,
18) -> CargoResult<(Package, Vec<PathBuf>)> {
19    trace!(
20        "read_package; path={}; source-id={}",
21        path.display(),
22        source_id
23    );
24    let (manifest, nested) = read_manifest(path, source_id, config)?;
25    let manifest = match manifest {
26        EitherManifest::Real(manifest) => manifest,
27        EitherManifest::Virtual(..) => anyhow::bail!(
28            "found a virtual manifest at `{}` instead of a package \
29             manifest",
30            path.display()
31        ),
32    };
33
34    Ok((Package::new(manifest, path), nested))
35}
36
37pub fn read_packages(
38    path: &Path,
39    source_id: SourceId,
40    config: &Config,
41) -> CargoResult<Vec<Package>> {
42    let mut all_packages = HashMap::new();
43    let mut visited = HashSet::<PathBuf>::new();
44    let mut errors = Vec::<anyhow::Error>::new();
45
46    trace!(
47        "looking for root package: {}, source_id={}",
48        path.display(),
49        source_id
50    );
51
52    walk(path, &mut |dir| {
53        trace!("looking for child package: {}", dir.display());
54
55        // Don't recurse into hidden/dot directories unless we're at the toplevel
56        if dir != path {
57            let name = dir.file_name().and_then(|s| s.to_str());
58            if name.map(|s| s.starts_with('.')) == Some(true) {
59                return Ok(false);
60            }
61
62            // Don't automatically discover packages across git submodules
63            if fs::metadata(&dir.join(".git")).is_ok() {
64                return Ok(false);
65            }
66        }
67
68        // Don't ever look at target directories
69        if dir.file_name().and_then(|s| s.to_str()) == Some("target")
70            && has_manifest(dir.parent().unwrap())
71        {
72            return Ok(false);
73        }
74
75        if has_manifest(dir) {
76            read_nested_packages(
77                dir,
78                &mut all_packages,
79                source_id,
80                config,
81                &mut visited,
82                &mut errors,
83            )?;
84        }
85        Ok(true)
86    })?;
87
88    if all_packages.is_empty() {
89        match errors.pop() {
90            Some(err) => Err(err),
91            None => Err(anyhow::format_err!(
92                "Could not find Cargo.toml in `{}`",
93                path.display()
94            )),
95        }
96    } else {
97        Ok(all_packages.into_iter().map(|(_, v)| v).collect())
98    }
99}
100
101fn walk(path: &Path, callback: &mut dyn FnMut(&Path) -> CargoResult<bool>) -> CargoResult<()> {
102    if !callback(path)? {
103        trace!("not processing {}", path.display());
104        return Ok(());
105    }
106
107    // Ignore any permission denied errors because temporary directories
108    // can often have some weird permissions on them.
109    let dirs = match fs::read_dir(path) {
110        Ok(dirs) => dirs,
111        Err(ref e) if e.kind() == io::ErrorKind::PermissionDenied => return Ok(()),
112        Err(e) => {
113            let cx = format!("failed to read directory `{}`", path.display());
114            let e = anyhow::Error::from(e);
115            return Err(e.context(cx).into());
116        }
117    };
118    for dir in dirs {
119        let dir = dir?;
120        if dir.file_type()?.is_dir() {
121            walk(&dir.path(), callback)?;
122        }
123    }
124    Ok(())
125}
126
127fn has_manifest(path: &Path) -> bool {
128    find_project_manifest_exact(path, "Cargo.toml").is_ok()
129}
130
131fn read_nested_packages(
132    path: &Path,
133    all_packages: &mut HashMap<PackageId, Package>,
134    source_id: SourceId,
135    config: &Config,
136    visited: &mut HashSet<PathBuf>,
137    errors: &mut Vec<anyhow::Error>,
138) -> CargoResult<()> {
139    if !visited.insert(path.to_path_buf()) {
140        return Ok(());
141    }
142
143    let manifest_path = find_project_manifest_exact(path, "Cargo.toml")?;
144
145    let (manifest, nested) = match read_manifest(&manifest_path, source_id, config) {
146        Err(err) => {
147            // Ignore malformed manifests found on git repositories
148            //
149            // git source try to find and read all manifests from the repository
150            // but since it's not possible to exclude folders from this search
151            // it's safer to ignore malformed manifests to avoid
152            //
153            // TODO: Add a way to exclude folders?
154            info!(
155                "skipping malformed package found at `{}`",
156                path.to_string_lossy()
157            );
158            errors.push(err.into());
159            return Ok(());
160        }
161        Ok(tuple) => tuple,
162    };
163
164    let manifest = match manifest {
165        EitherManifest::Real(manifest) => manifest,
166        EitherManifest::Virtual(..) => return Ok(()),
167    };
168    let pkg = Package::new(manifest, &manifest_path);
169
170    let pkg_id = pkg.package_id();
171    use std::collections::hash_map::Entry;
172    match all_packages.entry(pkg_id) {
173        Entry::Vacant(v) => {
174            v.insert(pkg);
175        }
176        Entry::Occupied(_) => {
177            info!(
178                "skipping nested package `{}` found at `{}`",
179                pkg.name(),
180                path.to_string_lossy()
181            );
182        }
183    }
184
185    // Registry sources are not allowed to have `path=` dependencies because
186    // they're all translated to actual registry dependencies.
187    //
188    // We normalize the path here ensure that we don't infinitely walk around
189    // looking for crates. By normalizing we ensure that we visit this crate at
190    // most once.
191    //
192    // TODO: filesystem/symlink implications?
193    if !source_id.is_registry() {
194        for p in nested.iter() {
195            let path = util::normalize_path(&path.join(p));
196            let result =
197                read_nested_packages(&path, all_packages, source_id, config, visited, errors);
198            // Ignore broken manifests found on git repositories.
199            //
200            // A well formed manifest might still fail to load due to reasons
201            // like referring to a "path" that requires an extra build step.
202            //
203            // See https://github.com/rust-lang/cargo/issues/6822.
204            if let Err(err) = result {
205                if source_id.is_git() {
206                    info!(
207                        "skipping nested package found at `{}`: {:?}",
208                        path.display(),
209                        &err,
210                    );
211                    errors.push(err);
212                } else {
213                    return Err(err);
214                }
215            }
216        }
217    }
218
219    Ok(())
220}