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