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