]> git.proxmox.com Git - cargo.git/blame - src/cargo/sources/path.rs
Auto merge of #9356 - ehuss:clippy-allow, r=Eh2406
[cargo.git] / src / cargo / sources / path.rs
CommitLineData
213afc02 1use std::fmt::{self, Debug, Formatter};
a6dad622 2use std::fs;
a6dad622
AC
3use std::path::{Path, PathBuf};
4
1dae5acb
EH
5use crate::core::source::MaybePackage;
6use crate::core::{Dependency, Package, PackageId, Source, SourceId, Summary};
7use crate::ops;
8use crate::util::{internal, CargoResult, CargoResultExt, Config};
9use cargo_util::paths;
7d49029a 10use filetime::FileTime;
c072ba42 11use ignore::gitignore::GitignoreBuilder;
e5a11190 12use ignore::Match;
9ed82b57 13use log::{trace, warn};
e05b4dc8 14
2fe0bf83 15pub struct PathSource<'cfg> {
b02023ee 16 source_id: SourceId,
a6dad622 17 path: PathBuf,
21b7418a 18 updated: bool,
5d0cb3f2 19 packages: Vec<Package>,
2fe0bf83 20 config: &'cfg Config,
e56965fb 21 recursive: bool,
98322afd 22}
62bff631 23
2fe0bf83 24impl<'cfg> PathSource<'cfg> {
f7c91ba6 25 /// Invoked with an absolute path to a directory that contains a `Cargo.toml`.
e56965fb
AC
26 ///
27 /// This source will only return the package at precisely the `path`
28 /// specified, and it will be an error if there's not a package at `path`.
e5a11190 29 pub fn new(path: &Path, source_id: SourceId, config: &'cfg Config) -> PathSource<'cfg> {
9224a5ae 30 PathSource {
e5a11190 31 source_id,
a6dad622 32 path: path.to_path_buf(),
21b7418a 33 updated: false,
5d0cb3f2 34 packages: Vec::new(),
0247dc42 35 config,
e56965fb
AC
36 recursive: false,
37 }
38 }
39
40 /// Creates a new source which is walked recursively to discover packages.
41 ///
42 /// This is similar to the `new` method except that instead of requiring a
43 /// valid package to be present at `root` the folder is walked entirely to
44 /// crawl for packages.
45 ///
46 /// Note that this should be used with care and likely shouldn't be chosen
47 /// by default!
e5a11190 48 pub fn new_recursive(root: &Path, id: SourceId, config: &'cfg Config) -> PathSource<'cfg> {
e56965fb
AC
49 PathSource {
50 recursive: true,
1e682848 51 ..PathSource::new(root, id, config)
9224a5ae 52 }
62bff631 53 }
c57fef45 54
51d23560
AC
55 pub fn preload_with(&mut self, pkg: Package) {
56 assert!(!self.updated);
57 assert!(!self.recursive);
58 assert!(self.packages.is_empty());
59 self.updated = true;
60 self.packages.push(pkg);
61 }
62
339146de 63 pub fn root_package(&mut self) -> CargoResult<Package> {
7a2facba 64 trace!("root_package; source={:?}", self);
9224a5ae 65
82655b46 66 self.update()?;
21b7418a 67
a6dad622 68 match self.packages.iter().find(|p| p.root() == &*self.path) {
9224a5ae 69 Some(pkg) => Ok(pkg.clone()),
0d44a826
EH
70 None => Err(internal(format!(
71 "no package found in source {:?}",
72 self.path
73 ))),
9224a5ae
YKCL
74 }
75 }
8d5acdfc 76
bc60f64b 77 pub fn read_packages(&self) -> CargoResult<Vec<Package>> {
8d5acdfc
AC
78 if self.updated {
79 Ok(self.packages.clone())
e56965fb 80 } else if self.recursive {
e5a11190 81 ops::read_packages(&self.path, self.source_id, self.config)
e56965fb 82 } else {
3bac6e06 83 let path = self.path.join("Cargo.toml");
e5a11190 84 let (pkg, _) = ops::read_package(&path, self.source_id, self.config)?;
3bac6e06 85 Ok(vec![pkg])
8d5acdfc
AC
86 }
87 }
8a19eb74
AC
88
89 /// List all files relevant to building this package inside this source.
90 ///
bacb6be3 91 /// This function will use the appropriate methods to determine the
8a19eb74
AC
92 /// set of files underneath this source's directory which are relevant for
93 /// building `pkg`.
94 ///
95 /// The basic assumption of this method is that all files in the directory
96 /// are relevant for building this package, but it also contains logic to
97 /// use other methods like .gitignore to filter the list of files.
a6dad622 98 pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathBuf>> {
9ed56cad
EH
99 self._list_files(pkg).chain_err(|| {
100 format!(
101 "failed to determine list of files in {}",
102 pkg.root().display()
103 )
104 })
105 }
106
107 fn _list_files(&self, pkg: &Package) -> CargoResult<Vec<PathBuf>> {
a6dad622 108 let root = pkg.root();
c072ba42
BE
109 let no_include_option = pkg.manifest().include().is_empty();
110
c072ba42
BE
111 let mut exclude_builder = GitignoreBuilder::new(root);
112 for rule in pkg.manifest().exclude() {
113 exclude_builder.add_line(None, rule)?;
114 }
115 let ignore_exclude = exclude_builder.build()?;
116
117 let mut include_builder = GitignoreBuilder::new(root);
118 for rule in pkg.manifest().include() {
119 include_builder.add_line(None, rule)?;
120 }
121 let ignore_include = include_builder.build()?;
122
51e0c71c 123 let ignore_should_package = |relative_path: &Path, is_dir: bool| -> CargoResult<bool> {
f7c91ba6 124 // "Include" and "exclude" options are mutually exclusive.
c072ba42 125 if no_include_option {
51e0c71c 126 match ignore_exclude.matched_path_or_any_parents(relative_path, is_dir) {
c072ba42
BE
127 Match::None => Ok(true),
128 Match::Ignore(_) => Ok(false),
3ca96e90 129 Match::Whitelist(_) => Ok(true),
c072ba42
BE
130 }
131 } else {
51e0c71c
EH
132 if is_dir {
133 // Generally, include directives don't list every
134 // directory (nor should they!). Just skip all directory
135 // checks, and only check files.
136 return Ok(true);
137 }
1e682848
AC
138 match ignore_include
139 .matched_path_or_any_parents(relative_path, /* is_dir */ false)
140 {
c072ba42
BE
141 Match::None => Ok(false),
142 Match::Ignore(_) => Ok(true),
3ca96e90 143 Match::Whitelist(_) => Ok(false),
c072ba42
BE
144 }
145 }
146 };
147
51e0c71c 148 let mut filter = |path: &Path, is_dir: bool| -> CargoResult<bool> {
092c88c4 149 let relative_path = path.strip_prefix(root)?;
49e37f80
EH
150
151 let rel = relative_path.as_os_str();
152 if rel == "Cargo.lock" {
153 return Ok(pkg.include_lockfile());
154 } else if rel == "Cargo.toml" {
155 return Ok(true);
156 }
157
51e0c71c 158 ignore_should_package(relative_path, is_dir)
8dc7e8a4 159 };
0e8f8d50 160
f7c91ba6 161 // Attempt Git-prepopulate only if no `include` (see rust-lang/cargo#4135).
c072ba42 162 if no_include_option {
3a5af295
EH
163 if let Some(result) = self.discover_git_and_list_files(pkg, root, &mut filter)? {
164 return Ok(result);
6eaa964d 165 }
6cee10b0
SH
166 // no include option and not git repo discovered (see rust-lang/cargo#7183).
167 return self.list_files_walk_except_dot_files_and_dirs(pkg, &mut filter);
1c588524 168 }
1c588524
FKI
169 self.list_files_walk(pkg, &mut filter)
170 }
171
f7c91ba6
AR
172 // Returns `Some(_)` if found sibling `Cargo.toml` and `.git` directory;
173 // otherwise, caller should fall back on full file list.
1e682848
AC
174 fn discover_git_and_list_files(
175 &self,
176 pkg: &Package,
177 root: &Path,
51e0c71c 178 filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
3a5af295 179 ) -> CargoResult<Option<Vec<PathBuf>>> {
1232ad3c
EH
180 let repo = match git2::Repository::discover(root) {
181 Ok(repo) => repo,
3a5af295
EH
182 Err(e) => {
183 log::debug!(
184 "could not discover git repo at or above {}: {}",
185 root.display(),
186 e
187 );
188 return Ok(None);
9185445a 189 }
1232ad3c 190 };
3a5af295
EH
191 let index = repo
192 .index()
193 .chain_err(|| format!("failed to open git index at {}", repo.path().display()))?;
194 let repo_root = repo.workdir().ok_or_else(|| {
195 anyhow::format_err!(
196 "did not expect repo at {} to be bare",
197 repo.path().display()
198 )
199 })?;
5bd74c41
EH
200 let repo_relative_path = match paths::strip_prefix_canonical(root, repo_root) {
201 Ok(p) => p,
202 Err(e) => {
203 log::warn!(
204 "cannot determine if path `{:?}` is in git repo `{:?}`: {:?}",
205 root,
206 repo_root,
207 e
208 );
209 return Ok(None);
210 }
211 };
25715e4f
EH
212 let manifest_path = repo_relative_path.join("Cargo.toml");
213 if index.get_path(&manifest_path, 0).is_some() {
3a5af295 214 return Ok(Some(self.list_files_git(pkg, &repo, filter)?));
5935ec1d 215 }
1232ad3c 216 // Package Cargo.toml is not in git, don't use git to guide our selection.
3a5af295 217 Ok(None)
0e8f8d50
AC
218 }
219
1e682848
AC
220 fn list_files_git(
221 &self,
222 pkg: &Package,
385b54b3 223 repo: &git2::Repository,
51e0c71c 224 filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
1e682848 225 ) -> CargoResult<Vec<PathBuf>> {
7a2facba 226 warn!("list_files_git {}", pkg.package_id());
82655b46 227 let index = repo.index()?;
e5a11190
E
228 let root = repo
229 .workdir()
0d44a826 230 .ok_or_else(|| anyhow::format_err!("can't list files on a bare repository"))?;
a6dad622 231 let pkg_path = pkg.root();
8b07052b 232
9185445a 233 let mut ret = Vec::<PathBuf>::new();
a8e9ce22 234
f7c91ba6 235 // We use information from the Git repository to guide us in traversing
a8e9ce22 236 // its tree. The primary purpose of this is to take advantage of the
f7c91ba6 237 // `.gitignore` and auto-ignore files that don't matter.
a8e9ce22 238 //
bacb6be3 239 // Here we're also careful to look at both tracked and untracked files as
a8e9ce22
AC
240 // the untracked files are often part of a build and may become relevant
241 // as part of a future commit.
046a6c59 242 let index_files = index.iter().map(|entry| {
50a24ff2
TW
243 use libgit2_sys::{GIT_FILEMODE_COMMIT, GIT_FILEMODE_LINK};
244 // ``is_dir`` is an optimization to avoid calling
245 // ``fs::metadata`` on every file.
246 let is_dir = if entry.mode == GIT_FILEMODE_LINK as u32 {
247 // Let the code below figure out if this symbolic link points
248 // to a directory or not.
249 None
250 } else {
251 Some(entry.mode == GIT_FILEMODE_COMMIT as u32)
252 };
253 (join(root, &entry.path), is_dir)
046a6c59 254 });
a8e9ce22
AC
255 let mut opts = git2::StatusOptions::new();
256 opts.include_untracked(true);
092c88c4 257 if let Ok(suffix) = pkg_path.strip_prefix(root) {
b6ad6fb4
AC
258 opts.pathspec(suffix);
259 }
82655b46 260 let statuses = repo.statuses(Some(&mut opts))?;
1e682848 261 let untracked = statuses.iter().filter_map(|entry| match entry.status() {
7d202307
EH
262 // Don't include Cargo.lock if it is untracked. Packaging will
263 // generate a new one as needed.
264 git2::Status::WT_NEW if entry.path() != Some("Cargo.lock") => {
265 Some((join(root, entry.path_bytes()), None))
266 }
1e682848 267 _ => None,
a8e9ce22
AC
268 });
269
9185445a
AC
270 let mut subpackages_found = Vec::new();
271
c5611a32 272 for (file_path, is_dir) in index_files.chain(untracked) {
82655b46 273 let file_path = file_path?;
8b07052b 274
9185445a 275 // Filter out files blatantly outside this package. This is helped a
7d202307 276 // bit above via the `pathspec` function call, but we need to filter
9185445a
AC
277 // the entries in the index as well.
278 if !file_path.starts_with(pkg_path) {
1e682848 279 continue;
a8e9ce22 280 }
8dc7e8a4 281
9185445a 282 match file_path.file_name().and_then(|s| s.to_str()) {
7d202307
EH
283 // The `target` directory is never included.
284 Some("target") => continue,
9185445a
AC
285
286 // Keep track of all sub-packages found and also strip out all
287 // matches we've found so far. Note, though, that if we find
f7c91ba6 288 // our own `Cargo.toml`, we keep going.
9185445a
AC
289 Some("Cargo.toml") => {
290 let path = file_path.parent().unwrap();
291 if path != pkg_path {
292 warn!("subpackage found: {}", path.display());
293 ret.retain(|p| !p.starts_with(path));
294 subpackages_found.push(path.to_path_buf());
1e682848 295 continue;
9185445a 296 }
8b07052b 297 }
9185445a
AC
298
299 _ => {}
8a19eb74
AC
300 }
301
9185445a
AC
302 // If this file is part of any other sub-package we've found so far,
303 // skip it.
304 if subpackages_found.iter().any(|p| file_path.starts_with(p)) {
1e682848 305 continue;
9185445a
AC
306 }
307
51e0c71c
EH
308 // `is_dir` is None for symlinks. The `unwrap` checks if the
309 // symlink points to a directory.
310 let is_dir = is_dir.unwrap_or_else(|| file_path.is_dir());
311 if is_dir {
5935ec1d 312 warn!(" found submodule {}", file_path.display());
092c88c4 313 let rel = file_path.strip_prefix(root)?;
54c42142 314 let rel = rel.to_str().ok_or_else(|| {
3a18c89a 315 anyhow::format_err!("invalid utf-8 filename: {}", rel.display())
54c42142 316 })?;
1e0b04a0
AC
317 // Git submodules are currently only named through `/` path
318 // separators, explicitly not `\` which windows uses. Who knew?
319 let rel = rel.replace(r"\", "/");
a8e9ce22
AC
320 match repo.find_submodule(&rel).and_then(|s| s.open()) {
321 Ok(repo) => {
385b54b3 322 let files = self.list_files_git(pkg, &repo, filter)?;
a8e9ce22
AC
323 ret.extend(files.into_iter());
324 }
325 Err(..) => {
c072ba42 326 PathSource::walk(&file_path, &mut ret, false, filter)?;
a8e9ce22
AC
327 }
328 }
51e0c71c
EH
329 } else if (*filter)(&file_path, is_dir)? {
330 assert!(!is_dir);
5935ec1d
AC
331 // We found a file!
332 warn!(" found {}", file_path.display());
333 ret.push(file_path);
334 }
8dc7e8a4 335 }
a6dad622
AC
336 return Ok(ret);
337
338 #[cfg(unix)]
339 fn join(path: &Path, data: &[u8]) -> CargoResult<PathBuf> {
a6dad622 340 use std::ffi::OsStr;
e5a11190 341 use std::os::unix::prelude::*;
a6dad622
AC
342 Ok(path.join(<OsStr as OsStrExt>::from_bytes(data)))
343 }
344 #[cfg(windows)]
345 fn join(path: &Path, data: &[u8]) -> CargoResult<PathBuf> {
346 use std::str;
347 match str::from_utf8(data) {
348 Ok(s) => Ok(path.join(s)),
0d44a826
EH
349 Err(e) => Err(anyhow::format_err!(
350 "cannot process path in git with a non utf8 filename: {}\n{:?}",
351 e,
352 data
1e682848 353 )),
a6dad622
AC
354 }
355 }
0e8f8d50
AC
356 }
357
6cee10b0
SH
358 fn list_files_walk_except_dot_files_and_dirs(
359 &self,
360 pkg: &Package,
51e0c71c 361 filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
6cee10b0
SH
362 ) -> CargoResult<Vec<PathBuf>> {
363 let root = pkg.root();
364 let mut exclude_dot_files_dir_builder = GitignoreBuilder::new(root);
365 exclude_dot_files_dir_builder.add_line(None, ".*")?;
6cee10b0
SH
366 let ignore_dot_files_and_dirs = exclude_dot_files_dir_builder.build()?;
367
51e0c71c
EH
368 let mut filter_ignore_dot_files_and_dirs =
369 |path: &Path, is_dir: bool| -> CargoResult<bool> {
370 let relative_path = path.strip_prefix(root)?;
371 match ignore_dot_files_and_dirs.matched_path_or_any_parents(relative_path, is_dir) {
372 Match::Ignore(_) => Ok(false),
373 _ => filter(path, is_dir),
374 }
375 };
6cee10b0
SH
376 self.list_files_walk(pkg, &mut filter_ignore_dot_files_and_dirs)
377 }
378
1e682848
AC
379 fn list_files_walk(
380 &self,
381 pkg: &Package,
51e0c71c 382 filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
1e682848 383 ) -> CargoResult<Vec<PathBuf>> {
0e8f8d50 384 let mut ret = Vec::new();
82655b46 385 PathSource::walk(pkg.root(), &mut ret, true, filter)?;
3a852a0f 386 Ok(ret)
a8e9ce22 387 }
8a19eb74 388
1e682848
AC
389 fn walk(
390 path: &Path,
391 ret: &mut Vec<PathBuf>,
392 is_root: bool,
51e0c71c 393 filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
1e682848 394 ) -> CargoResult<()> {
51e0c71c
EH
395 let is_dir = path.is_dir();
396 if !is_root && !(*filter)(path, is_dir)? {
397 return Ok(());
398 }
399 if !is_dir {
400 ret.push(path.to_path_buf());
1e682848 401 return Ok(());
8a19eb74 402 }
f7c91ba6 403 // Don't recurse into any sub-packages that we have.
4367ec4d 404 if !is_root && path.join("Cargo.toml").exists() {
1e682848 405 return Ok(());
a8e9ce22 406 }
c072ba42
BE
407
408 // For package integration tests, we need to sort the paths in a deterministic order to
409 // be able to match stdout warnings in the same order.
410 //
f7c91ba6
AR
411 // TODO: drop `collect` and sort after transition period and dropping warning tests.
412 // See rust-lang/cargo#4268 and rust-lang/cargo#4270.
dcad83d5
EH
413 let mut entries: Vec<PathBuf> = fs::read_dir(path)
414 .chain_err(|| format!("cannot read {:?}", path))?
415 .map(|e| e.unwrap().path())
416 .collect();
c74aa949
E
417 entries.sort_unstable_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
418 for path in entries {
c072ba42 419 let name = path.file_name().and_then(|s| s.to_str());
ec21e12d 420 if is_root && name == Some("target") {
f7c91ba6 421 // Skip Cargo artifacts.
ec21e12d 422 continue;
a8e9ce22 423 }
c072ba42 424 PathSource::walk(&path, ret, false, filter)?;
a8e9ce22 425 }
3a852a0f 426 Ok(())
8a19eb74 427 }
9c1124fb
GF
428
429 pub fn last_modified_file(&self, pkg: &Package) -> CargoResult<(FileTime, PathBuf)> {
430 if !self.updated {
0d44a826
EH
431 return Err(internal(format!(
432 "BUG: source `{:?}` was not updated",
433 self.path
434 )));
9c1124fb
GF
435 }
436
437 let mut max = FileTime::zero();
438 let mut max_path = PathBuf::new();
9ed56cad
EH
439 for file in self.list_files(pkg).chain_err(|| {
440 format!(
441 "failed to determine the most recently modified file in {}",
442 pkg.root().display()
443 )
444 })? {
f7c91ba6 445 // An `fs::stat` error here is either because path is a
9c1124fb 446 // broken symlink, a permissions error, or a race
f7c91ba6
AR
447 // condition where this path was `rm`-ed -- either way,
448 // we can ignore the error and treat the path's `mtime`
449 // as `0`.
385b54b3 450 let mtime = paths::mtime(&file).unwrap_or_else(|_| FileTime::zero());
9c1124fb
GF
451 if mtime > max {
452 max = mtime;
453 max_path = file;
454 }
455 }
456 trace!("last modified file {}: {}", self.path.display(), max);
457 Ok((max, max_path))
458 }
8921abd7
DW
459
460 pub fn path(&self) -> &Path {
461 &self.path
462 }
62bff631
C
463}
464
2fe0bf83 465impl<'cfg> Debug for PathSource<'cfg> {
b8b7faee 466 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
cfeabbc2 467 write!(f, "the paths source")
50f110a4
YK
468 }
469}
470
f947326a 471impl<'cfg> Source for PathSource<'cfg> {
b8b7faee 472 fn query(&mut self, dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
6acedd8a
DW
473 for s in self.packages.iter().map(|p| p.summary()) {
474 if dep.matches(s) {
475 f(s.clone())
476 }
477 }
478 Ok(())
f947326a 479 }
5b08b8fe 480
b8b7faee 481 fn fuzzy_query(&mut self, _dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
84cc3d8b
E
482 for s in self.packages.iter().map(|p| p.summary()) {
483 f(s.clone())
484 }
485 Ok(())
486 }
487
5b08b8fe
AC
488 fn supports_checksums(&self) -> bool {
489 false
490 }
491
492 fn requires_precise(&self) -> bool {
493 false
494 }
b02023ee 495
e5a11190
E
496 fn source_id(&self) -> SourceId {
497 self.source_id
53bd095f
DW
498 }
499
c665938d 500 fn update(&mut self) -> CargoResult<()> {
21b7418a 501 if !self.updated {
82655b46 502 let packages = self.read_packages()?;
3cdca46b 503 self.packages.extend(packages.into_iter());
8d5acdfc 504 self.updated = true;
21b7418a
CL
505 }
506
98322afd
YKCL
507 Ok(())
508 }
62bff631 509
dae87a26 510 fn download(&mut self, id: PackageId) -> CargoResult<MaybePackage> {
9cf70517 511 trace!("getting packages; id={}", id);
c57fef45 512
9cf70517 513 let pkg = self.packages.iter().find(|pkg| pkg.package_id() == id);
1e682848 514 pkg.cloned()
c94804bd 515 .map(MaybePackage::Ready)
1e682848 516 .ok_or_else(|| internal(format!("failed to find {} in path source", id)))
62bff631 517 }
e05b4dc8 518
dae87a26 519 fn finish_download(&mut self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
c94804bd
AC
520 panic!("no download should have started")
521 }
522
8d5acdfc 523 fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
9c1124fb 524 let (max, max_path) = self.last_modified_file(pkg)?;
64a46826
AC
525 // Note that we try to strip the prefix of this package to get a
526 // relative path to ensure that the fingerprint remains consistent
527 // across entire project directory renames.
528 let max_path = max_path.strip_prefix(&self.path).unwrap_or(&max_path);
c0595fbc 529 Ok(format!("{} ({})", max, max_path.display()))
e05b4dc8 530 }
20cfb41e
AC
531
532 fn describe(&self) -> String {
533 match self.source_id.url().to_file_path() {
534 Ok(path) => path.display().to_string(),
535 Err(_) => self.source_id.to_string(),
536 }
537 }
4a2f810d
AC
538
539 fn add_to_yanked_whitelist(&mut self, _pkgs: &[PackageId]) {}
5f616eb1
EH
540
541 fn is_yanked(&mut self, _pkg: PackageId) -> CargoResult<bool> {
542 Ok(false)
543 }
62bff631 544}