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