Skip to main content

cargo_review_deps/
lib.rs

1#[macro_use]
2extern crate failure;
3extern crate cargo_metadata;
4extern crate copy_dir;
5extern crate semver;
6extern crate tempdir;
7
8use std::{
9    collections::HashMap,
10    ffi::OsString,
11    fmt, fs,
12    path::{Path, PathBuf},
13    process::{Command, Stdio},
14    str::FromStr,
15};
16
17use copy_dir::copy_dir;
18use semver::Version;
19use tempdir::TempDir;
20
21pub use failure::Error;
22pub type Result<T> = ::std::result::Result<T, Error>;
23
24/// Mirrors `PackageId` from Cargo. `PackageId` is an unambiguous reference to a
25/// package version.
26///
27/// Future work: support git dependencies and alternative registries.
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct PackageId {
30    name: String,
31    version: Version,
32}
33
34impl fmt::Display for PackageId {
35    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
36        self.name.fmt(fmt)?;
37        fmt.write_str(":")?;
38        self.version.fmt(fmt)
39    }
40}
41
42impl FromStr for PackageId {
43    type Err = Error;
44    fn from_str(s: &str) -> Result<PackageId> {
45        let colon_idx = s.find(':').ok_or_else(|| {
46            format_err!(
47                "invalid package specification: {:?}; expected \"name:x.y.z\"",
48                s
49            )
50        })?;
51        let name = s[..colon_idx].to_string();
52        let version: Version = s[colon_idx + 1..].parse()?;
53        Ok(PackageId { name, version })
54    }
55}
56
57#[derive(Debug)]
58pub struct Diff {
59    pub first: PackageId,
60    pub second: PackageId,
61    pub dest: Option<PathBuf>,
62}
63
64impl Diff {
65    pub fn run(self) -> Result<()> {
66        let first_src = fetch(&self.first)?;
67        let second_src = fetch(&self.second)?;
68        if let Some(dir) = self.dest {
69            fs::create_dir_all(&dir)?;
70            copy_dir(&first_src, &dir.join(self.first.to_string()))?;
71            copy_dir(&second_src, &dir.join(self.second.to_string()))?;
72        } else {
73            run_diff_cmd(&first_src, &second_src)?;
74        }
75        Ok(())
76    }
77}
78
79pub fn run_diff_cmd(a: &Path, b: &Path) -> Result<()> {
80    let mut diff_cmd = Command::new("diff");
81    let diff_status = diff_cmd
82        .args(&["--color=auto", "-r"])
83        .arg(a)
84        .arg(b)
85        .stdout(Stdio::inherit())
86        .stderr(Stdio::inherit())
87        .status();
88    if diff_status.is_err() {
89        if !has_diff_cmd() {
90            bail!("looks like you don't have a suitable diff command installed.\n\
91                   Try using --destination flag to run a custom diff tool or to compare sources manually.")
92        }
93    }
94    diff_status?;
95    Ok(())
96}
97
98#[derive(Debug)]
99pub struct Current {
100    pub dest: PathBuf,
101}
102
103impl Current {
104    pub fn run(self) -> Result<()> {
105        let metadata = Metadata {
106            manifest_path: None,
107        }
108        .run()?;
109
110        fs::create_dir_all(&self.dest)?;
111        for pkg in crates_io_packages(&metadata) {
112            let src = pkg_dir(&pkg)?;
113            let dst = self.dest.join(format!("{}:{}", pkg.name, pkg.version));
114            copy_dir(&src, &dst)?;
115        }
116        Ok(())
117    }
118}
119
120#[derive(Debug)]
121pub struct UpdateDiff {
122    pub dest: Option<PathBuf>,
123    pub args: Vec<OsString>,
124}
125
126impl UpdateDiff {
127    pub fn run(self) -> Result<()> {
128        let before_metadata = Metadata {
129            manifest_path: None,
130        }
131        .run()?;
132        let workspace_root = Path::new(&before_metadata.workspace_root);
133        let lockfile = workspace_root.join("Cargo.lock");
134        let mut lockfile_guard = LockfileGuard::new(lockfile)?;
135
136        let status = Command::new("cargo")
137            .arg("update")
138            .args(&self.args)
139            .stdout(Stdio::inherit())
140            .stderr(Stdio::inherit())
141            .status()?;
142
143        if !status.success() {
144            bail!("running cargo update failed");
145        }
146        let after_metadata = Metadata {
147            manifest_path: None,
148        }
149        .run()?;
150        let tmpdir;
151        let dest = match self.dest.as_ref() {
152            Some(it) => it.as_path(),
153            None => {
154                tmpdir = TempDir::new("cargo-diff-fetches")?;
155                tmpdir.path()
156            }
157        };
158        let before_dir = dest.join("before");
159        let after_dir = dest.join("after");
160        fs::create_dir_all(&before_dir)?;
161        fs::create_dir_all(&after_dir)?;
162        for pdiff in metadata_diff(&before_metadata, &after_metadata)? {
163            pdiff.dump_to(&dest)?;
164        }
165
166        if self.dest.is_none() {
167            run_diff_cmd(&before_dir, &after_dir)?
168        }
169        lockfile_guard.restore_lockfile()?;
170        Ok(())
171    }
172}
173
174#[derive(Debug)]
175struct PackageDiff {
176    name: String,
177    before: Option<PathBuf>,
178    after: Option<PathBuf>,
179}
180
181impl PackageDiff {
182    fn dump_to(&self, dest: &Path) -> Result<()> {
183        if let Some(src) = self.before.as_ref() {
184            let dst = dest.join("before").join(&self.name);
185            copy_dir(&src, &dst)?;
186        }
187        if let Some(src) = self.after.as_ref() {
188            let dst = dest.join("after").join(&self.name);
189            copy_dir(&src, &dst)?;
190        }
191        Ok(())
192    }
193}
194
195fn metadata_diff(
196    before: &cargo_metadata::Metadata,
197    after: &cargo_metadata::Metadata,
198) -> Result<Vec<PackageDiff>> {
199    let before = extract_packages(before)?;
200    let after = extract_packages(after)?;
201    let mut res = Vec::new();
202    for (name, before_path) in before.iter() {
203        if !after.contains_key(name) {
204            res.push(PackageDiff {
205                name: name.clone(),
206                before: Some(before_path.clone()),
207                after: None,
208            })
209        }
210    }
211    for (name, after_path) in after.iter() {
212        if !before.contains_key(name) {
213            res.push(PackageDiff {
214                name: name.clone(),
215                before: None,
216                after: Some(after_path.clone()),
217            })
218        }
219    }
220    for (name, before_path) in before.iter() {
221        if let Some(after_path) = after.get(name) {
222            if before_path == after_path {
223                continue;
224            }
225            res.push(PackageDiff {
226                name: name.clone(),
227                before: Some(before_path.clone()),
228                after: Some(after_path.clone()),
229            });
230        }
231    }
232
233    Ok(res)
234}
235
236fn extract_packages(meta: &cargo_metadata::Metadata) -> Result<HashMap<String, PathBuf>> {
237    let mut res = HashMap::new();
238    for pkg in crates_io_packages(meta) {
239        let version = Version::parse(&pkg.version)?;
240        let semver_compatible_version = if version.major == 0 {
241            format!("0.{}", version.minor)
242        } else {
243            format!("{}", version.major)
244        };
245        let name = format!("{}:{}", pkg.name, semver_compatible_version);
246        res.insert(name, pkg_dir(pkg)?);
247    }
248    Ok(res)
249}
250
251fn crates_io_packages<'a>(
252    meta: &'a cargo_metadata::Metadata,
253) -> impl Iterator<Item = &'a cargo_metadata::Package> {
254    meta.packages.iter().filter(|pkg| {
255        // Ideally we should look at the `source`, but that is private.
256        let is_cratesio_dep = pkg.id.contains("crates.io-index");
257        if !is_cratesio_dep {
258            eprintln!(
259                "Skipping package `{}`: not a crates.io dependency",
260                pkg.name
261            );
262        }
263        is_cratesio_dep
264    })
265}
266
267/// We run real `cargo update` which writes to the lockfile. This struct makes sure (in
268/// Drop), that we restore it propertly afterwards.
269#[derive(Debug)]
270struct LockfileGuard {
271    lockfile_path: PathBuf,
272    lockfile_copy_path: PathBuf,
273    lockfile_contents: String,
274    restored: bool,
275}
276
277impl LockfileGuard {
278    fn new(path: impl Into<PathBuf>) -> Result<LockfileGuard> {
279        let lockfile_path = path.into();
280        let lockfile_copy_path = lockfile_path.with_extension(".lock.back");
281        let lockfile_contents = fs::read_to_string(&lockfile_path)?;
282        fs::write(&lockfile_copy_path, &lockfile_contents)?;
283        let res = LockfileGuard {
284            lockfile_path,
285            lockfile_copy_path,
286            lockfile_contents,
287            restored: false,
288        };
289        Ok(res)
290    }
291
292    fn restore_lockfile(&mut self) -> Result<()> {
293        self.restored = true;
294        fs::write(&self.lockfile_path, &self.lockfile_contents)?;
295        fs::remove_file(self.lockfile_copy_path.as_path())?;
296        Ok(())
297    }
298}
299
300impl Drop for LockfileGuard {
301    fn drop(&mut self) {
302        if !self.restored {
303            let _ = self.restore_lockfile();
304        }
305    }
306}
307
308struct Metadata<'a> {
309    manifest_path: Option<&'a Path>,
310}
311
312impl<'a> Metadata<'a> {
313    fn run(self) -> Result<cargo_metadata::Metadata> {
314        let metadata = cargo_metadata::metadata_deps(
315            self.manifest_path,
316            true, // include dependencies
317        )
318        .map_err(|err| format_err!("cargo metadata failed: {}", err))?; // error_chain is not sync :-(
319        Ok(metadata)
320    }
321}
322
323fn has_diff_cmd() -> bool {
324    match Command::new("diff").arg("--version").status() {
325        Err(_) => false,
326        Ok(status) => status.success(),
327    }
328}
329
330/// Shells out to Cargo to download `pkg_id` from crates io.
331/// Returns the directory with the downloaded package;
332fn fetch(pkg_id: &PackageId) -> Result<PathBuf> {
333    let dir = TempDir::new("cargo-diff-fetches")?;
334    let temp_manifest = dir.path().join("Cargo.toml");
335    fs::write(&temp_manifest, format_cargo_toml(pkg_id))?;
336    let metadata = Metadata {
337        manifest_path: Some(temp_manifest.as_path()),
338    }
339    .run()?;
340
341    let package = metadata
342        .packages
343        .iter()
344        .find(|it| it.name == pkg_id.name && it.version == pkg_id.version.to_string())
345        .ok_or_else(|| format_err!("unexpected error: can't find package {:?}", pkg_id))?;
346    pkg_dir(&package)
347}
348
349fn pkg_dir(pkg: &cargo_metadata::Package) -> Result<PathBuf> {
350    let res = PathBuf::from(&pkg.manifest_path)
351        .parent()
352        .ok_or_else(|| {
353            format_err!(
354                "unexpected error: bad manifest path {:?}",
355                pkg.manifest_path
356            )
357        })?
358        .to_path_buf();
359    Ok(res)
360}
361
362/// Conjures up a Cargo.toml with `pkg_id` as a dependency.
363fn format_cargo_toml(pkg_id: &PackageId) -> String {
364    format!(
365        r#"
366[package]
367name = "cargo-diff-temp-pkg"
368version = "0.0.0"
369
370[lib]
371path = "./Cargo.toml"
372
373[dependencies]
374{} = "={}"
375"#,
376        pkg_id.name, pkg_id.version
377    )
378}