]>
Commit | Line | Data |
---|---|---|
213afc02 | 1 | use std::fmt::{self, Debug, Formatter}; |
a6dad622 | 2 | use std::fs; |
a6dad622 AC |
3 | use std::path::{Path, PathBuf}; |
4 | ||
7d49029a | 5 | use filetime::FileTime; |
8dc7e8a4 | 6 | use git2; |
046a6c59 | 7 | use glob::Pattern; |
e05b4dc8 | 8 | |
3b77b2c7 | 9 | use core::{Package, PackageId, Summary, SourceId, Source, Dependency, Registry}; |
12f49111 | 10 | use ops; |
9c445b92 | 11 | use util::{self, CargoResult, internal, internal_error, human, ChainError}; |
7d49029a | 12 | use util::Config; |
62bff631 | 13 | |
2fe0bf83 | 14 | pub struct PathSource<'cfg> { |
9224a5ae | 15 | id: SourceId, |
a6dad622 | 16 | path: PathBuf, |
21b7418a | 17 | updated: bool, |
5d0cb3f2 | 18 | packages: Vec<Package>, |
2fe0bf83 | 19 | config: &'cfg Config, |
e56965fb | 20 | recursive: bool, |
98322afd | 21 | } |
62bff631 | 22 | |
2fe0bf83 | 23 | impl<'cfg> PathSource<'cfg> { |
64ff29ff | 24 | /// Invoked with an absolute path to a directory that contains a Cargo.toml. |
e56965fb AC |
25 | /// |
26 | /// This source will only return the package at precisely the `path` | |
27 | /// specified, and it will be an error if there's not a package at `path`. | |
2fe0bf83 AC |
28 | pub fn new(path: &Path, id: &SourceId, config: &'cfg Config) |
29 | -> PathSource<'cfg> { | |
9224a5ae YKCL |
30 | PathSource { |
31 | id: id.clone(), | |
a6dad622 | 32 | path: path.to_path_buf(), |
21b7418a | 33 | updated: false, |
5d0cb3f2 AC |
34 | packages: Vec::new(), |
35 | config: 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! | |
48 | pub fn new_recursive(root: &Path, id: &SourceId, config: &'cfg Config) | |
49 | -> PathSource<'cfg> { | |
50 | PathSource { | |
51 | recursive: true, | |
52 | .. PathSource::new(root, id, config) | |
9224a5ae | 53 | } |
62bff631 | 54 | } |
c57fef45 | 55 | |
339146de | 56 | pub fn root_package(&mut self) -> CargoResult<Package> { |
7a2facba | 57 | trace!("root_package; source={:?}", self); |
9224a5ae | 58 | |
82655b46 | 59 | self.update()?; |
21b7418a | 60 | |
a6dad622 | 61 | match self.packages.iter().find(|p| p.root() == &*self.path) { |
9224a5ae | 62 | Some(pkg) => Ok(pkg.clone()), |
af0f2733 | 63 | None => Err(internal("no package found in source")) |
9224a5ae YKCL |
64 | } |
65 | } | |
8d5acdfc | 66 | |
bc60f64b | 67 | pub fn read_packages(&self) -> CargoResult<Vec<Package>> { |
8d5acdfc AC |
68 | if self.updated { |
69 | Ok(self.packages.clone()) | |
e56965fb AC |
70 | } else if self.recursive { |
71 | ops::read_packages(&self.path, &self.id, self.config) | |
72 | } else { | |
3bac6e06 | 73 | let path = self.path.join("Cargo.toml"); |
82655b46 SG |
74 | let (pkg, _) = ops::read_package(&path, &self.id, |
75 | self.config)?; | |
3bac6e06 | 76 | Ok(vec![pkg]) |
8d5acdfc AC |
77 | } |
78 | } | |
8a19eb74 AC |
79 | |
80 | /// List all files relevant to building this package inside this source. | |
81 | /// | |
bacb6be3 | 82 | /// This function will use the appropriate methods to determine the |
8a19eb74 AC |
83 | /// set of files underneath this source's directory which are relevant for |
84 | /// building `pkg`. | |
85 | /// | |
86 | /// The basic assumption of this method is that all files in the directory | |
87 | /// are relevant for building this package, but it also contains logic to | |
88 | /// use other methods like .gitignore to filter the list of files. | |
a6dad622 AC |
89 | pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathBuf>> { |
90 | let root = pkg.root(); | |
8b07052b | 91 | |
a1faa7cb | 92 | let parse = |p: &String| { |
25e537aa | 93 | Pattern::new(p).map_err(|e| { |
d73a110d AC |
94 | human(format!("could not parse pattern `{}`: {}", p, e)) |
95 | }) | |
96 | }; | |
ed61ba20 SG |
97 | |
98 | let exclude = pkg.manifest() | |
99 | .exclude() | |
100 | .iter() | |
101 | .map(|p| parse(p)) | |
102 | .collect::<Result<Vec<_>, _>>()?; | |
103 | ||
104 | let include = pkg.manifest() | |
105 | .include() | |
106 | .iter() | |
107 | .map(|p| parse(p)) | |
108 | .collect::<Result<Vec<_>, _>>()?; | |
8b07052b | 109 | |
f2aa5b4c | 110 | let mut filter = |p: &Path| { |
80fe0e6d | 111 | let relative_path = util::without_prefix(p, &root).unwrap(); |
5935ec1d | 112 | include.iter().any(|p| p.matches_path(&relative_path)) || { |
8628dbeb | 113 | include.is_empty() && |
5935ec1d | 114 | !exclude.iter().any(|p| p.matches_path(&relative_path)) |
8b07052b | 115 | } |
8dc7e8a4 | 116 | }; |
0e8f8d50 | 117 | |
9185445a AC |
118 | // If this package is in a git repository, then we really do want to |
119 | // query the git repository as it takes into account items such as | |
120 | // .gitignore. We're not quite sure where the git repository is, | |
121 | // however, so we do a bit of a probe. | |
5935ec1d | 122 | // |
9185445a AC |
123 | // We walk this package's path upwards and look for a sibling |
124 | // Cargo.toml and .git folder. If we find one then we assume that we're | |
125 | // part of that repository. | |
126 | let mut cur = root; | |
127 | loop { | |
128 | if cur.join("Cargo.toml").is_file() { | |
129 | // If we find a git repository next to this Cargo.toml, we still | |
130 | // check to see if we are indeed part of the index. If not, then | |
131 | // this is likely an unrelated git repo, so keep going. | |
132 | if let Ok(repo) = git2::Repository::open(cur) { | |
82655b46 | 133 | let index = repo.index()?; |
9185445a AC |
134 | let path = util::without_prefix(root, cur) |
135 | .unwrap().join("Cargo.toml"); | |
136 | if index.get_path(&path, 0).is_some() { | |
137 | return self.list_files_git(pkg, repo, &mut filter); | |
138 | } | |
139 | } | |
140 | } | |
141 | // don't cross submodule boundaries | |
142 | if cur.join(".git").is_dir() { | |
143 | break | |
144 | } | |
145 | match cur.parent() { | |
146 | Some(parent) => cur = parent, | |
147 | None => break, | |
148 | } | |
5935ec1d | 149 | } |
9185445a | 150 | self.list_files_walk(pkg, &mut filter) |
0e8f8d50 AC |
151 | } |
152 | ||
a8e9ce22 AC |
153 | fn list_files_git(&self, pkg: &Package, repo: git2::Repository, |
154 | filter: &mut FnMut(&Path) -> bool) | |
155 | -> CargoResult<Vec<PathBuf>> { | |
7a2facba | 156 | warn!("list_files_git {}", pkg.package_id()); |
82655b46 SG |
157 | let index = repo.index()?; |
158 | let root = repo.workdir().chain_error(|| { | |
a8e9ce22 | 159 | internal_error("Can't list files on a bare repository.", "") |
82655b46 | 160 | })?; |
a6dad622 | 161 | let pkg_path = pkg.root(); |
8b07052b | 162 | |
9185445a | 163 | let mut ret = Vec::<PathBuf>::new(); |
a8e9ce22 | 164 | |
bacb6be3 | 165 | // We use information from the git repository to guide us in traversing |
a8e9ce22 AC |
166 | // its tree. The primary purpose of this is to take advantage of the |
167 | // .gitignore and auto-ignore files that don't matter. | |
168 | // | |
bacb6be3 | 169 | // Here we're also careful to look at both tracked and untracked files as |
a8e9ce22 AC |
170 | // the untracked files are often part of a build and may become relevant |
171 | // as part of a future commit. | |
046a6c59 | 172 | let index_files = index.iter().map(|entry| { |
e0dff0f2 | 173 | use libgit2_sys::GIT_FILEMODE_COMMIT; |
632780eb | 174 | let is_dir = entry.mode == GIT_FILEMODE_COMMIT as u32; |
046a6c59 AC |
175 | (join(&root, &entry.path), Some(is_dir)) |
176 | }); | |
a8e9ce22 AC |
177 | let mut opts = git2::StatusOptions::new(); |
178 | opts.include_untracked(true); | |
b6ad6fb4 AC |
179 | if let Some(suffix) = util::without_prefix(pkg_path, &root) { |
180 | opts.pathspec(suffix); | |
181 | } | |
82655b46 | 182 | let statuses = repo.statuses(Some(&mut opts))?; |
e2cd4cdb FC |
183 | let untracked = statuses.iter().filter_map(|entry| { |
184 | match entry.status() { | |
185 | git2::STATUS_WT_NEW => Some((join(&root, entry.path_bytes()), None)), | |
186 | _ => None | |
187 | } | |
a8e9ce22 AC |
188 | }); |
189 | ||
9185445a AC |
190 | let mut subpackages_found = Vec::new(); |
191 | ||
046a6c59 | 192 | 'outer: for (file_path, is_dir) in index_files.chain(untracked) { |
82655b46 | 193 | let file_path = file_path?; |
8b07052b | 194 | |
9185445a AC |
195 | // Filter out files blatantly outside this package. This is helped a |
196 | // bit obove via the `pathspec` function call, but we need to filter | |
197 | // the entries in the index as well. | |
198 | if !file_path.starts_with(pkg_path) { | |
199 | continue | |
a8e9ce22 | 200 | } |
8dc7e8a4 | 201 | |
9185445a AC |
202 | match file_path.file_name().and_then(|s| s.to_str()) { |
203 | // Filter out Cargo.lock and target always, we don't want to | |
204 | // package a lock file no one will ever read and we also avoid | |
205 | // build artifacts | |
206 | Some("Cargo.lock") | | |
207 | Some("target") => continue, | |
208 | ||
209 | // Keep track of all sub-packages found and also strip out all | |
210 | // matches we've found so far. Note, though, that if we find | |
211 | // our own `Cargo.toml` we keep going. | |
212 | Some("Cargo.toml") => { | |
213 | let path = file_path.parent().unwrap(); | |
214 | if path != pkg_path { | |
215 | warn!("subpackage found: {}", path.display()); | |
216 | ret.retain(|p| !p.starts_with(path)); | |
217 | subpackages_found.push(path.to_path_buf()); | |
218 | continue | |
219 | } | |
8b07052b | 220 | } |
9185445a AC |
221 | |
222 | _ => {} | |
8a19eb74 AC |
223 | } |
224 | ||
9185445a AC |
225 | // If this file is part of any other sub-package we've found so far, |
226 | // skip it. | |
227 | if subpackages_found.iter().any(|p| file_path.starts_with(p)) { | |
228 | continue | |
229 | } | |
230 | ||
231 | if is_dir.unwrap_or_else(|| file_path.is_dir()) { | |
5935ec1d | 232 | warn!(" found submodule {}", file_path.display()); |
80fe0e6d | 233 | let rel = util::without_prefix(&file_path, &root).unwrap(); |
82655b46 | 234 | let rel = rel.to_str().chain_error(|| { |
5935ec1d | 235 | human(format!("invalid utf-8 filename: {}", rel.display())) |
82655b46 | 236 | })?; |
1e0b04a0 AC |
237 | // Git submodules are currently only named through `/` path |
238 | // separators, explicitly not `\` which windows uses. Who knew? | |
239 | let rel = rel.replace(r"\", "/"); | |
a8e9ce22 AC |
240 | match repo.find_submodule(&rel).and_then(|s| s.open()) { |
241 | Ok(repo) => { | |
82655b46 | 242 | let files = self.list_files_git(pkg, repo, filter)?; |
a8e9ce22 AC |
243 | ret.extend(files.into_iter()); |
244 | } | |
245 | Err(..) => { | |
82655b46 SG |
246 | PathSource::walk(&file_path, &mut ret, false, |
247 | filter)?; | |
a8e9ce22 AC |
248 | } |
249 | } | |
5935ec1d AC |
250 | } else if (*filter)(&file_path) { |
251 | // We found a file! | |
252 | warn!(" found {}", file_path.display()); | |
253 | ret.push(file_path); | |
254 | } | |
8dc7e8a4 | 255 | } |
a6dad622 AC |
256 | return Ok(ret); |
257 | ||
258 | #[cfg(unix)] | |
259 | fn join(path: &Path, data: &[u8]) -> CargoResult<PathBuf> { | |
260 | use std::os::unix::prelude::*; | |
261 | use std::ffi::OsStr; | |
262 | Ok(path.join(<OsStr as OsStrExt>::from_bytes(data))) | |
263 | } | |
264 | #[cfg(windows)] | |
265 | fn join(path: &Path, data: &[u8]) -> CargoResult<PathBuf> { | |
266 | use std::str; | |
267 | match str::from_utf8(data) { | |
268 | Ok(s) => Ok(path.join(s)), | |
269 | Err(..) => Err(internal("cannot process path in git with a non \ | |
270 | unicode filename")), | |
271 | } | |
272 | } | |
0e8f8d50 AC |
273 | } |
274 | ||
a8e9ce22 AC |
275 | fn list_files_walk(&self, pkg: &Package, filter: &mut FnMut(&Path) -> bool) |
276 | -> CargoResult<Vec<PathBuf>> { | |
0e8f8d50 | 277 | let mut ret = Vec::new(); |
82655b46 | 278 | PathSource::walk(pkg.root(), &mut ret, true, filter)?; |
3a852a0f | 279 | Ok(ret) |
a8e9ce22 | 280 | } |
8a19eb74 | 281 | |
a8e9ce22 AC |
282 | fn walk(path: &Path, ret: &mut Vec<PathBuf>, |
283 | is_root: bool, filter: &mut FnMut(&Path) -> bool) -> CargoResult<()> | |
284 | { | |
285 | if !fs::metadata(&path).map(|m| m.is_dir()).unwrap_or(false) { | |
286 | if (*filter)(path) { | |
287 | ret.push(path.to_path_buf()); | |
8a19eb74 | 288 | } |
0e8f8d50 | 289 | return Ok(()) |
8a19eb74 | 290 | } |
a8e9ce22 AC |
291 | // Don't recurse into any sub-packages that we have |
292 | if !is_root && fs::metadata(&path.join("Cargo.toml")).is_ok() { | |
293 | return Ok(()) | |
294 | } | |
82655b46 SG |
295 | for dir in fs::read_dir(path)? { |
296 | let dir = dir?.path(); | |
11144645 LB |
297 | let name = dir.file_name().and_then(|s| s.to_str()); |
298 | // Skip dotfile directories | |
4673f0a6 | 299 | if name.map(|s| s.starts_with('.')) == Some(true) { |
11144645 LB |
300 | continue |
301 | } else if is_root { | |
302 | // Skip cargo artifacts | |
303 | match name { | |
304 | Some("target") | Some("Cargo.lock") => continue, | |
305 | _ => {} | |
306 | } | |
a8e9ce22 | 307 | } |
82655b46 | 308 | PathSource::walk(&dir, ret, false, filter)?; |
a8e9ce22 | 309 | } |
3a852a0f | 310 | Ok(()) |
8a19eb74 | 311 | } |
62bff631 C |
312 | } |
313 | ||
2fe0bf83 | 314 | impl<'cfg> Debug for PathSource<'cfg> { |
50f110a4 | 315 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { |
cfeabbc2 | 316 | write!(f, "the paths source") |
50f110a4 YK |
317 | } |
318 | } | |
319 | ||
2fe0bf83 | 320 | impl<'cfg> Registry for PathSource<'cfg> { |
3b77b2c7 | 321 | fn query(&mut self, dep: &Dependency) -> CargoResult<Vec<Summary>> { |
348c7389 | 322 | self.packages.query(dep) |
3b77b2c7 AC |
323 | } |
324 | } | |
325 | ||
2fe0bf83 | 326 | impl<'cfg> Source for PathSource<'cfg> { |
c665938d | 327 | fn update(&mut self) -> CargoResult<()> { |
21b7418a | 328 | if !self.updated { |
82655b46 | 329 | let packages = self.read_packages()?; |
3cdca46b | 330 | self.packages.extend(packages.into_iter()); |
8d5acdfc | 331 | self.updated = true; |
21b7418a CL |
332 | } |
333 | ||
98322afd YKCL |
334 | Ok(()) |
335 | } | |
62bff631 | 336 | |
9cf70517 AC |
337 | fn download(&mut self, id: &PackageId) -> CargoResult<Package> { |
338 | trace!("getting packages; id={}", id); | |
c57fef45 | 339 | |
9cf70517 AC |
340 | let pkg = self.packages.iter().find(|pkg| pkg.package_id() == id); |
341 | pkg.cloned().ok_or_else(|| { | |
342 | internal(format!("failed to find {} in path source", id)) | |
343 | }) | |
62bff631 | 344 | } |
e05b4dc8 | 345 | |
8d5acdfc | 346 | fn fingerprint(&self, pkg: &Package) -> CargoResult<String> { |
5451f95d CL |
347 | if !self.updated { |
348 | return Err(internal_error("BUG: source was not updated", "")); | |
349 | } | |
350 | ||
7d49029a | 351 | let mut max = FileTime::zero(); |
c0595fbc | 352 | let mut max_path = PathBuf::from(""); |
82655b46 | 353 | for file in self.list_files(pkg)? { |
8a19eb74 AC |
354 | // An fs::stat error here is either because path is a |
355 | // broken symlink, a permissions error, or a race | |
356 | // condition where this path was rm'ed - either way, | |
357 | // we can ignore the error and treat the path's mtime | |
358 | // as 0. | |
c0595fbc | 359 | let mtime = fs::metadata(&file).map(|meta| { |
7d49029a AC |
360 | FileTime::from_last_modification_time(&meta) |
361 | }).unwrap_or(FileTime::zero()); | |
a6dad622 | 362 | warn!("{} {}", mtime, file.display()); |
c0595fbc AC |
363 | if mtime > max { |
364 | max = mtime; | |
365 | max_path = file; | |
366 | } | |
e05b4dc8 | 367 | } |
98854f6f | 368 | trace!("fingerprint {}: {}", self.path.display(), max); |
c0595fbc | 369 | Ok(format!("{} ({})", max, max_path.display())) |
e05b4dc8 | 370 | } |
62bff631 | 371 | } |