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