]> git.proxmox.com Git - cargo.git/blame - src/cargo/sources/path.rs
Auto merge of #5761 - dwijnand:project-food, r=alexcrichton
[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;
8dc7e8a4 6use git2;
046a6c59 7use glob::Pattern;
c072ba42
BE
8use ignore::Match;
9use ignore::gitignore::GitignoreBuilder;
e05b4dc8 10
29f738fe 11use core::{Dependency, Package, PackageId, Source, SourceId, Summary};
12f49111 12use ops;
1e682848 13use util::{self, internal, CargoResult};
8c5c9e39 14use util::paths;
7d49029a 15use util::Config;
62bff631 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> {
64ff29ff 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`.
1e682848 31 pub fn new(path: &Path, id: &SourceId, config: &'cfg Config) -> PathSource<'cfg> {
9224a5ae 32 PathSource {
b02023ee 33 source_id: id.clone(),
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!
1e682848 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()),
1e682848 72 None => Err(internal("no package found in source")),
9224a5ae
YKCL
73 }
74 }
8d5acdfc 75
bc60f64b 76 pub fn read_packages(&self) -> CargoResult<Vec<Package>> {
8d5acdfc
AC
77 if self.updated {
78 Ok(self.packages.clone())
e56965fb 79 } else if self.recursive {
b02023ee 80 ops::read_packages(&self.path, &self.source_id, self.config)
e56965fb 81 } else {
3bac6e06 82 let path = self.path.join("Cargo.toml");
b02023ee 83 let (pkg, _) = ops::read_package(&path, &self.source_id, self.config)?;
3bac6e06 84 Ok(vec![pkg])
8d5acdfc
AC
85 }
86 }
8a19eb74
AC
87
88 /// List all files relevant to building this package inside this source.
89 ///
bacb6be3 90 /// This function will use the appropriate methods to determine the
8a19eb74
AC
91 /// set of files underneath this source's directory which are relevant for
92 /// building `pkg`.
93 ///
94 /// The basic assumption of this method is that all files in the directory
95 /// are relevant for building this package, but it also contains logic to
96 /// use other methods like .gitignore to filter the list of files.
c072ba42
BE
97 ///
98 /// ## Pattern matching strategy
99 ///
100 /// Migrating from a glob-like pattern matching (using `glob` crate) to a
101 /// gitignore-like pattern matching (using `ignore` crate). The migration
102 /// stages are:
103 ///
104 /// 1) Only warn users about the future change iff their matching rules are
105 /// affected. (CURRENT STAGE)
106 ///
670a3df4 107 /// 2) Switch to the new strategy and update documents. Still keep warning
c072ba42
BE
108 /// affected users.
109 ///
670a3df4 110 /// 3) Drop the old strategy and no more warnings.
c072ba42
BE
111 ///
112 /// See <https://github.com/rust-lang/cargo/issues/4268> for more info.
a6dad622
AC
113 pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathBuf>> {
114 let root = pkg.root();
c072ba42
BE
115 let no_include_option = pkg.manifest().include().is_empty();
116
117 // glob-like matching rules
8b07052b 118
c072ba42 119 let glob_parse = |p: &String| {
5641cbde
BE
120 let pattern: &str = if p.starts_with('/') {
121 &p[1..p.len()]
122 } else {
23591fe5 123 p
5641cbde 124 };
1e682848
AC
125 Pattern::new(pattern)
126 .map_err(|e| format_err!("could not parse glob pattern `{}`: {}", p, e))
d73a110d 127 };
ed61ba20 128
c072ba42
BE
129 let glob_exclude = pkg.manifest()
130 .exclude()
131 .iter()
132 .map(|p| glob_parse(p))
133 .collect::<Result<Vec<_>, _>>()?;
134
135 let glob_include = pkg.manifest()
136 .include()
137 .iter()
138 .map(|p| glob_parse(p))
139 .collect::<Result<Vec<_>, _>>()?;
140
141 let glob_should_package = |relative_path: &Path| -> bool {
5641cbde 142 fn glob_match(patterns: &Vec<Pattern>, relative_path: &Path) -> bool {
1e682848
AC
143 patterns
144 .iter()
145 .any(|pattern| pattern.matches_path(relative_path))
5641cbde
BE
146 }
147
c072ba42
BE
148 // include and exclude options are mutually exclusive.
149 if no_include_option {
5641cbde 150 !glob_match(&glob_exclude, relative_path)
c072ba42 151 } else {
5641cbde 152 glob_match(&glob_include, relative_path)
c072ba42
BE
153 }
154 };
155
156 // ignore-like matching rules
157
158 let mut exclude_builder = GitignoreBuilder::new(root);
159 for rule in pkg.manifest().exclude() {
160 exclude_builder.add_line(None, rule)?;
161 }
162 let ignore_exclude = exclude_builder.build()?;
163
164 let mut include_builder = GitignoreBuilder::new(root);
165 for rule in pkg.manifest().include() {
166 include_builder.add_line(None, rule)?;
167 }
168 let ignore_include = include_builder.build()?;
169
170 let ignore_should_package = |relative_path: &Path| -> CargoResult<bool> {
171 // include and exclude options are mutually exclusive.
172 if no_include_option {
1e682848
AC
173 match ignore_exclude
174 .matched_path_or_any_parents(relative_path, /* is_dir */ false)
175 {
c072ba42
BE
176 Match::None => Ok(true),
177 Match::Ignore(_) => Ok(false),
37cffbe0 178 Match::Whitelist(pattern) => Err(format_err!(
c072ba42
BE
179 "exclude rules cannot start with `!`: {}",
180 pattern.original()
37cffbe0 181 )),
c072ba42
BE
182 }
183 } else {
1e682848
AC
184 match ignore_include
185 .matched_path_or_any_parents(relative_path, /* is_dir */ false)
186 {
c072ba42
BE
187 Match::None => Ok(false),
188 Match::Ignore(_) => Ok(true),
37cffbe0 189 Match::Whitelist(pattern) => Err(format_err!(
c072ba42
BE
190 "include rules cannot start with `!`: {}",
191 pattern.original()
37cffbe0 192 )),
c072ba42
BE
193 }
194 }
195 };
196
197 // matching to paths
198
199 let mut filter = |path: &Path| -> CargoResult<bool> {
200 let relative_path = util::without_prefix(path, root).unwrap();
201 let glob_should_package = glob_should_package(relative_path);
202 let ignore_should_package = ignore_should_package(relative_path)?;
203
204 if glob_should_package != ignore_should_package {
205 if glob_should_package {
206 if no_include_option {
1e682848 207 self.config.shell().warn(format!(
24ba7c80 208 "Pattern matching for Cargo's include/exclude fields is changing and \
1e682848
AC
209 file `{}` WILL be excluded in a future Cargo version.\n\
210 See https://github.com/rust-lang/cargo/issues/4268 for more info",
24ba7c80
LL
211 relative_path.display()
212 ))?;
1e682848
AC
213 } else {
214 self.config.shell().warn(format!(
23591fe5 215 "Pattern matching for Cargo's include/exclude fields is changing and \
1e682848
AC
216 file `{}` WILL NOT be included in a future Cargo version.\n\
217 See https://github.com/rust-lang/cargo/issues/4268 for more info",
23591fe5
LL
218 relative_path.display()
219 ))?;
1e682848
AC
220 }
221 } else if no_include_option {
222 self.config.shell().warn(format!(
223 "Pattern matching for Cargo's include/exclude fields is changing and \
224 file `{}` WILL NOT be excluded in a future Cargo version.\n\
225 See https://github.com/rust-lang/cargo/issues/4268 for more info",
226 relative_path.display()
227 ))?;
228 } else {
229 self.config.shell().warn(format!(
230 "Pattern matching for Cargo's include/exclude fields is changing and \
231 file `{}` WILL be included in a future Cargo version.\n\
232 See https://github.com/rust-lang/cargo/issues/4268 for more info",
233 relative_path.display()
234 ))?;
c072ba42 235 }
8b07052b 236 }
c072ba42
BE
237
238 // Update to ignore_should_package for Stage 2
239 Ok(glob_should_package)
8dc7e8a4 240 };
0e8f8d50 241
6eaa964d 242 // attempt git-prepopulate only if no `include` (rust-lang/cargo#4135)
c072ba42 243 if no_include_option {
6eaa964d
FKI
244 if let Some(result) = self.discover_git_and_list_files(pkg, root, &mut filter) {
245 return result;
246 }
1c588524 247 }
1c588524
FKI
248 self.list_files_walk(pkg, &mut filter)
249 }
250
251 // Returns Some(_) if found sibling Cargo.toml and .git folder;
252 // otherwise caller should fall back on full file list.
1e682848
AC
253 fn discover_git_and_list_files(
254 &self,
255 pkg: &Package,
256 root: &Path,
257 filter: &mut FnMut(&Path) -> CargoResult<bool>,
258 ) -> Option<CargoResult<Vec<PathBuf>>> {
9185445a
AC
259 // If this package is in a git repository, then we really do want to
260 // query the git repository as it takes into account items such as
261 // .gitignore. We're not quite sure where the git repository is,
262 // however, so we do a bit of a probe.
5935ec1d 263 //
9185445a
AC
264 // We walk this package's path upwards and look for a sibling
265 // Cargo.toml and .git folder. If we find one then we assume that we're
266 // part of that repository.
267 let mut cur = root;
268 loop {
269 if cur.join("Cargo.toml").is_file() {
270 // If we find a git repository next to this Cargo.toml, we still
271 // check to see if we are indeed part of the index. If not, then
272 // this is likely an unrelated git repo, so keep going.
273 if let Ok(repo) = git2::Repository::open(cur) {
1c588524
FKI
274 let index = match repo.index() {
275 Ok(index) => index,
276 Err(err) => return Some(Err(err.into())),
277 };
1e682848 278 let path = util::without_prefix(root, cur).unwrap().join("Cargo.toml");
9185445a 279 if index.get_path(&path, 0).is_some() {
1c588524 280 return Some(self.list_files_git(pkg, repo, filter));
9185445a
AC
281 }
282 }
283 }
284 // don't cross submodule boundaries
285 if cur.join(".git").is_dir() {
1e682848 286 break;
9185445a
AC
287 }
288 match cur.parent() {
289 Some(parent) => cur = parent,
290 None => break,
291 }
5935ec1d 292 }
23591fe5 293 None
0e8f8d50
AC
294 }
295
1e682848
AC
296 fn list_files_git(
297 &self,
298 pkg: &Package,
299 repo: git2::Repository,
300 filter: &mut FnMut(&Path) -> CargoResult<bool>,
301 ) -> CargoResult<Vec<PathBuf>> {
7a2facba 302 warn!("list_files_git {}", pkg.package_id());
82655b46 303 let index = repo.index()?;
1e682848
AC
304 let root = repo.workdir()
305 .ok_or_else(|| internal("Can't list files on a bare repository."))?;
a6dad622 306 let pkg_path = pkg.root();
8b07052b 307
9185445a 308 let mut ret = Vec::<PathBuf>::new();
a8e9ce22 309
bacb6be3 310 // We use information from the git repository to guide us in traversing
a8e9ce22
AC
311 // its tree. The primary purpose of this is to take advantage of the
312 // .gitignore and auto-ignore files that don't matter.
313 //
bacb6be3 314 // Here we're also careful to look at both tracked and untracked files as
a8e9ce22
AC
315 // the untracked files are often part of a build and may become relevant
316 // as part of a future commit.
046a6c59 317 let index_files = index.iter().map(|entry| {
e0dff0f2 318 use libgit2_sys::GIT_FILEMODE_COMMIT;
632780eb 319 let is_dir = entry.mode == GIT_FILEMODE_COMMIT as u32;
c5611a32 320 (join(root, &entry.path), Some(is_dir))
046a6c59 321 });
a8e9ce22
AC
322 let mut opts = git2::StatusOptions::new();
323 opts.include_untracked(true);
c5611a32 324 if let Some(suffix) = util::without_prefix(pkg_path, root) {
b6ad6fb4
AC
325 opts.pathspec(suffix);
326 }
82655b46 327 let statuses = repo.statuses(Some(&mut opts))?;
1e682848
AC
328 let untracked = statuses.iter().filter_map(|entry| match entry.status() {
329 git2::Status::WT_NEW => Some((join(root, entry.path_bytes()), None)),
330 _ => None,
a8e9ce22
AC
331 });
332
9185445a
AC
333 let mut subpackages_found = Vec::new();
334
c5611a32 335 for (file_path, is_dir) in index_files.chain(untracked) {
82655b46 336 let file_path = file_path?;
8b07052b 337
9185445a
AC
338 // Filter out files blatantly outside this package. This is helped a
339 // bit obove via the `pathspec` function call, but we need to filter
340 // the entries in the index as well.
341 if !file_path.starts_with(pkg_path) {
1e682848 342 continue;
a8e9ce22 343 }
8dc7e8a4 344
9185445a
AC
345 match file_path.file_name().and_then(|s| s.to_str()) {
346 // Filter out Cargo.lock and target always, we don't want to
347 // package a lock file no one will ever read and we also avoid
348 // build artifacts
1e682848 349 Some("Cargo.lock") | Some("target") => continue,
9185445a
AC
350
351 // Keep track of all sub-packages found and also strip out all
352 // matches we've found so far. Note, though, that if we find
353 // our own `Cargo.toml` we keep going.
354 Some("Cargo.toml") => {
355 let path = file_path.parent().unwrap();
356 if path != pkg_path {
357 warn!("subpackage found: {}", path.display());
358 ret.retain(|p| !p.starts_with(path));
359 subpackages_found.push(path.to_path_buf());
1e682848 360 continue;
9185445a 361 }
8b07052b 362 }
9185445a
AC
363
364 _ => {}
8a19eb74
AC
365 }
366
9185445a
AC
367 // If this file is part of any other sub-package we've found so far,
368 // skip it.
369 if subpackages_found.iter().any(|p| file_path.starts_with(p)) {
1e682848 370 continue;
9185445a
AC
371 }
372
373 if is_dir.unwrap_or_else(|| file_path.is_dir()) {
5935ec1d 374 warn!(" found submodule {}", file_path.display());
c5611a32 375 let rel = util::without_prefix(&file_path, root).unwrap();
1e682848
AC
376 let rel = rel.to_str()
377 .ok_or_else(|| format_err!("invalid utf-8 filename: {}", rel.display()))?;
1e0b04a0
AC
378 // Git submodules are currently only named through `/` path
379 // separators, explicitly not `\` which windows uses. Who knew?
380 let rel = rel.replace(r"\", "/");
a8e9ce22
AC
381 match repo.find_submodule(&rel).and_then(|s| s.open()) {
382 Ok(repo) => {
82655b46 383 let files = self.list_files_git(pkg, repo, filter)?;
a8e9ce22
AC
384 ret.extend(files.into_iter());
385 }
386 Err(..) => {
c072ba42 387 PathSource::walk(&file_path, &mut ret, false, filter)?;
a8e9ce22
AC
388 }
389 }
c072ba42 390 } else if (*filter)(&file_path)? {
5935ec1d
AC
391 // We found a file!
392 warn!(" found {}", file_path.display());
393 ret.push(file_path);
394 }
8dc7e8a4 395 }
a6dad622
AC
396 return Ok(ret);
397
398 #[cfg(unix)]
399 fn join(path: &Path, data: &[u8]) -> CargoResult<PathBuf> {
400 use std::os::unix::prelude::*;
401 use std::ffi::OsStr;
402 Ok(path.join(<OsStr as OsStrExt>::from_bytes(data)))
403 }
404 #[cfg(windows)]
405 fn join(path: &Path, data: &[u8]) -> CargoResult<PathBuf> {
406 use std::str;
407 match str::from_utf8(data) {
408 Ok(s) => Ok(path.join(s)),
1e682848
AC
409 Err(..) => Err(internal(
410 "cannot process path in git with a non \
411 unicode filename",
412 )),
a6dad622
AC
413 }
414 }
0e8f8d50
AC
415 }
416
1e682848
AC
417 fn list_files_walk(
418 &self,
419 pkg: &Package,
420 filter: &mut FnMut(&Path) -> CargoResult<bool>,
421 ) -> CargoResult<Vec<PathBuf>> {
0e8f8d50 422 let mut ret = Vec::new();
82655b46 423 PathSource::walk(pkg.root(), &mut ret, true, filter)?;
3a852a0f 424 Ok(ret)
a8e9ce22 425 }
8a19eb74 426
1e682848
AC
427 fn walk(
428 path: &Path,
429 ret: &mut Vec<PathBuf>,
430 is_root: bool,
431 filter: &mut FnMut(&Path) -> CargoResult<bool>,
432 ) -> CargoResult<()> {
a8e9ce22 433 if !fs::metadata(&path).map(|m| m.is_dir()).unwrap_or(false) {
c072ba42 434 if (*filter)(path)? {
a8e9ce22 435 ret.push(path.to_path_buf());
8a19eb74 436 }
1e682848 437 return Ok(());
8a19eb74 438 }
a8e9ce22
AC
439 // Don't recurse into any sub-packages that we have
440 if !is_root && fs::metadata(&path.join("Cargo.toml")).is_ok() {
1e682848 441 return Ok(());
a8e9ce22 442 }
c072ba42
BE
443
444 // For package integration tests, we need to sort the paths in a deterministic order to
445 // be able to match stdout warnings in the same order.
446 //
57a7e126 447 // TODO: Drop collect and sort after transition period and dropping warning tests.
c072ba42
BE
448 // See <https://github.com/rust-lang/cargo/issues/4268>
449 // and <https://github.com/rust-lang/cargo/pull/4270>
c74aa949
E
450 let mut entries: Vec<PathBuf> = fs::read_dir(path)?.map(|e| e.unwrap().path()).collect();
451 entries.sort_unstable_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
452 for path in entries {
c072ba42 453 let name = path.file_name().and_then(|s| s.to_str());
11144645 454 // Skip dotfile directories
4673f0a6 455 if name.map(|s| s.starts_with('.')) == Some(true) {
1e682848 456 continue;
676edacf
E
457 }
458 if is_root {
11144645
LB
459 // Skip cargo artifacts
460 match name {
461 Some("target") | Some("Cargo.lock") => continue,
462 _ => {}
463 }
a8e9ce22 464 }
c072ba42 465 PathSource::walk(&path, ret, false, filter)?;
a8e9ce22 466 }
3a852a0f 467 Ok(())
8a19eb74 468 }
9c1124fb
GF
469
470 pub fn last_modified_file(&self, pkg: &Package) -> CargoResult<(FileTime, PathBuf)> {
471 if !self.updated {
472 return Err(internal("BUG: source was not updated"));
473 }
474
475 let mut max = FileTime::zero();
476 let mut max_path = PathBuf::new();
477 for file in self.list_files(pkg)? {
478 // An fs::stat error here is either because path is a
479 // broken symlink, a permissions error, or a race
480 // condition where this path was rm'ed - either way,
481 // we can ignore the error and treat the path's mtime
482 // as 0.
483 let mtime = paths::mtime(&file).unwrap_or(FileTime::zero());
484 if mtime > max {
485 max = mtime;
486 max_path = file;
487 }
488 }
489 trace!("last modified file {}: {}", self.path.display(), max);
490 Ok((max, max_path))
491 }
62bff631
C
492}
493
2fe0bf83 494impl<'cfg> Debug for PathSource<'cfg> {
50f110a4 495 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
cfeabbc2 496 write!(f, "the paths source")
50f110a4
YK
497 }
498}
499
f947326a 500impl<'cfg> Source for PathSource<'cfg> {
6acedd8a
DW
501 fn query(&mut self, dep: &Dependency, f: &mut FnMut(Summary)) -> CargoResult<()> {
502 for s in self.packages.iter().map(|p| p.summary()) {
503 if dep.matches(s) {
504 f(s.clone())
505 }
506 }
507 Ok(())
f947326a 508 }
5b08b8fe 509
84cc3d8b
E
510 fn fuzzy_query(&mut self, _dep: &Dependency, f: &mut FnMut(Summary)) -> CargoResult<()> {
511 for s in self.packages.iter().map(|p| p.summary()) {
512 f(s.clone())
513 }
514 Ok(())
515 }
516
5b08b8fe
AC
517 fn supports_checksums(&self) -> bool {
518 false
519 }
520
521 fn requires_precise(&self) -> bool {
522 false
523 }
b02023ee 524
6acedd8a
DW
525 fn source_id(&self) -> &SourceId {
526 &self.source_id
53bd095f
DW
527 }
528
c665938d 529 fn update(&mut self) -> CargoResult<()> {
21b7418a 530 if !self.updated {
82655b46 531 let packages = self.read_packages()?;
3cdca46b 532 self.packages.extend(packages.into_iter());
8d5acdfc 533 self.updated = true;
21b7418a
CL
534 }
535
98322afd
YKCL
536 Ok(())
537 }
62bff631 538
9cf70517
AC
539 fn download(&mut self, id: &PackageId) -> CargoResult<Package> {
540 trace!("getting packages; id={}", id);
c57fef45 541
9cf70517 542 let pkg = self.packages.iter().find(|pkg| pkg.package_id() == id);
1e682848
AC
543 pkg.cloned()
544 .ok_or_else(|| internal(format!("failed to find {} in path source", id)))
62bff631 545 }
e05b4dc8 546
8d5acdfc 547 fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
9c1124fb 548 let (max, max_path) = self.last_modified_file(pkg)?;
c0595fbc 549 Ok(format!("{} ({})", max, max_path.display()))
e05b4dc8 550 }
62bff631 551}