cargo/ops/cargo_package/
verify.rs

1//! Helpers to verify a packaged `.crate` file.
2
3use std::collections::HashMap;
4use std::fs;
5use std::fs::File;
6use std::io::prelude::*;
7use std::io::SeekFrom;
8use std::path::Path;
9use std::path::PathBuf;
10use std::sync::Arc;
11
12use anyhow::Context as _;
13use cargo_util::paths;
14use flate2::read::GzDecoder;
15use tar::Archive;
16
17use crate::core::compiler::BuildConfig;
18use crate::core::compiler::CompileMode;
19use crate::core::compiler::DefaultExecutor;
20use crate::core::compiler::Executor;
21use crate::core::Feature;
22use crate::core::Package;
23use crate::core::SourceId;
24use crate::core::Workspace;
25use crate::ops;
26use crate::sources::PathSource;
27use crate::util;
28use crate::util::FileLock;
29use crate::CargoResult;
30
31use super::PackageOpts;
32use super::TmpRegistry;
33
34/// Verifies whether a `.crate` file is able to compile.
35pub fn run_verify(
36    ws: &Workspace<'_>,
37    pkg: &Package,
38    tar: &FileLock,
39    local_reg: Option<&TmpRegistry<'_>>,
40    opts: &PackageOpts<'_>,
41) -> CargoResult<()> {
42    let gctx = ws.gctx();
43
44    gctx.shell().status("Verifying", pkg)?;
45
46    tar.file().seek(SeekFrom::Start(0))?;
47    let f = GzDecoder::new(tar.file());
48    let dst = tar
49        .parent()
50        .join(&format!("{}-{}", pkg.name(), pkg.version()));
51    if dst.exists() {
52        paths::remove_dir_all(&dst)?;
53    }
54    let mut archive = Archive::new(f);
55    // We don't need to set the Modified Time, as it's not relevant to verification
56    // and it errors on filesystems that don't support setting a modified timestamp
57    archive.set_preserve_mtime(false);
58    archive.unpack(dst.parent().unwrap())?;
59
60    // Manufacture an ephemeral workspace to ensure that even if the top-level
61    // package has a workspace we can still build our new crate.
62    let id = SourceId::for_path(&dst)?;
63    let mut src = PathSource::new(&dst, id, ws.gctx());
64    let new_pkg = src.root_package()?;
65    let pkg_fingerprint = hash_all(&dst)?;
66    let mut ws = Workspace::ephemeral(new_pkg, gctx, None, true)?;
67    if let Some(local_reg) = local_reg {
68        ws.add_local_overlay(
69            local_reg.upstream,
70            local_reg.root.as_path_unlocked().to_owned(),
71        );
72    }
73
74    let rustc_args = if pkg
75        .manifest()
76        .unstable_features()
77        .require(Feature::public_dependency())
78        .is_ok()
79        || ws.gctx().cli_unstable().public_dependency
80    {
81        // FIXME: Turn this on at some point in the future
82        //Some(vec!["-D exported_private_dependencies".to_string()])
83        Some(vec![])
84    } else {
85        None
86    };
87
88    let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
89    ops::compile_with_exec(
90        &ws,
91        &ops::CompileOptions {
92            build_config: BuildConfig::new(
93                gctx,
94                opts.jobs.clone(),
95                opts.keep_going,
96                &opts.targets,
97                CompileMode::Build,
98            )?,
99            cli_features: opts.cli_features.clone(),
100            spec: ops::Packages::Packages(Vec::new()),
101            filter: ops::CompileFilter::Default {
102                required_features_filterable: true,
103            },
104            target_rustdoc_args: None,
105            target_rustc_args: rustc_args,
106            target_rustc_crate_types: None,
107            rustdoc_document_private_items: false,
108            honor_rust_version: None,
109        },
110        &exec,
111    )?;
112
113    // Check that `build.rs` didn't modify any files in the `src` directory.
114    let ws_fingerprint = hash_all(&dst)?;
115    if pkg_fingerprint != ws_fingerprint {
116        let changes = report_hash_difference(&pkg_fingerprint, &ws_fingerprint);
117        anyhow::bail!(
118            "Source directory was modified by build.rs during cargo publish. \
119             Build scripts should not modify anything outside of OUT_DIR.\n\
120             {}\n\n\
121             To proceed despite this, pass the `--no-verify` flag.",
122            changes
123        )
124    }
125
126    Ok(())
127}
128
129/// Hashes everything under a given directory.
130///
131/// This is for checking if any source file inside a `.crate` file has changed
132/// durint the compilation. It is usually caused by bad build scripts or proc
133/// macros trying to modify source files. Cargo disallows that.
134fn hash_all(path: &Path) -> CargoResult<HashMap<PathBuf, u64>> {
135    fn wrap(path: &Path) -> CargoResult<HashMap<PathBuf, u64>> {
136        let mut result = HashMap::new();
137        let walker = walkdir::WalkDir::new(path).into_iter();
138        for entry in walker.filter_entry(|e| !(e.depth() == 1 && e.file_name() == "target")) {
139            let entry = entry?;
140            let file_type = entry.file_type();
141            if file_type.is_file() {
142                let file = File::open(entry.path())?;
143                let hash = util::hex::hash_u64_file(&file)?;
144                result.insert(entry.path().to_path_buf(), hash);
145            } else if file_type.is_symlink() {
146                let hash = util::hex::hash_u64(&fs::read_link(entry.path())?);
147                result.insert(entry.path().to_path_buf(), hash);
148            } else if file_type.is_dir() {
149                let hash = util::hex::hash_u64(&());
150                result.insert(entry.path().to_path_buf(), hash);
151            }
152        }
153        Ok(result)
154    }
155    let result = wrap(path).with_context(|| format!("failed to verify output at {:?}", path))?;
156    Ok(result)
157}
158
159/// Reports the hash difference before and after the compilation computed by  [`hash_all`].
160fn report_hash_difference(orig: &HashMap<PathBuf, u64>, after: &HashMap<PathBuf, u64>) -> String {
161    let mut changed = Vec::new();
162    let mut removed = Vec::new();
163    for (key, value) in orig {
164        match after.get(key) {
165            Some(after_value) => {
166                if value != after_value {
167                    changed.push(key.to_string_lossy());
168                }
169            }
170            None => removed.push(key.to_string_lossy()),
171        }
172    }
173    let mut added: Vec<_> = after
174        .keys()
175        .filter(|key| !orig.contains_key(*key))
176        .map(|key| key.to_string_lossy())
177        .collect();
178    let mut result = Vec::new();
179    if !changed.is_empty() {
180        changed.sort_unstable();
181        result.push(format!("Changed: {}", changed.join("\n\t")));
182    }
183    if !added.is_empty() {
184        added.sort_unstable();
185        result.push(format!("Added: {}", added.join("\n\t")));
186    }
187    if !removed.is_empty() {
188        removed.sort_unstable();
189        result.push(format!("Removed: {}", removed.join("\n\t")));
190    }
191    assert!(!result.is_empty(), "unexpected empty change detection");
192    result.join("\n")
193}