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