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