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 = 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    let target_dir = if gctx.cli_unstable().build_dir {
70        Some(ws.build_dir())
71    } else {
72        None
73    };
74
75    let mut ws = Workspace::ephemeral(new_pkg, gctx, target_dir, true)?;
76    if let Some(local_reg) = local_reg {
77        ws.add_local_overlay(
78            local_reg.upstream,
79            local_reg.root.as_path_unlocked().to_owned(),
80        );
81    }
82
83    let rustc_args = if pkg
84        .manifest()
85        .unstable_features()
86        .require(Feature::public_dependency())
87        .is_ok()
88        || ws.gctx().cli_unstable().public_dependency
89    {
90        // FIXME: Turn this on at some point in the future
91        //Some(vec!["-D exported_private_dependencies".to_string()])
92        Some(vec![])
93    } else {
94        None
95    };
96
97    let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
98    ops::compile_with_exec(
99        &ws,
100        &ops::CompileOptions {
101            build_config: BuildConfig::new(
102                gctx,
103                opts.jobs.clone(),
104                opts.keep_going,
105                &opts.targets,
106                CompileMode::Build,
107            )?,
108            cli_features: opts.cli_features.clone(),
109            spec: ops::Packages::Packages(Vec::new()),
110            filter: ops::CompileFilter::Default {
111                required_features_filterable: true,
112            },
113            target_rustdoc_args: None,
114            target_rustc_args: rustc_args,
115            target_rustc_crate_types: None,
116            rustdoc_document_private_items: false,
117            honor_rust_version: None,
118        },
119        &exec,
120    )?;
121
122    // Check that `build.rs` didn't modify any files in the `src` directory.
123    let ws_fingerprint = hash_all(&dst)?;
124    if pkg_fingerprint != ws_fingerprint {
125        let changes = report_hash_difference(&pkg_fingerprint, &ws_fingerprint);
126        anyhow::bail!(
127            "Source directory was modified by build.rs during cargo publish. \
128             Build scripts should not modify anything outside of OUT_DIR.\n\
129             {}\n\n\
130             To proceed despite this, pass the `--no-verify` flag.",
131            changes
132        )
133    }
134
135    Ok(())
136}
137
138/// Hashes everything under a given directory.
139///
140/// This is for checking if any source file inside a `.crate` file has changed
141/// durint the compilation. It is usually caused by bad build scripts or proc
142/// macros trying to modify source files. Cargo disallows that.
143fn hash_all(path: &Path) -> CargoResult<HashMap<PathBuf, u64>> {
144    fn wrap(path: &Path) -> CargoResult<HashMap<PathBuf, u64>> {
145        let mut result = HashMap::new();
146        let walker = walkdir::WalkDir::new(path).into_iter();
147        for entry in walker.filter_entry(|e| !(e.depth() == 1 && e.file_name() == "target")) {
148            let entry = entry?;
149            let file_type = entry.file_type();
150            if file_type.is_file() {
151                let file = File::open(entry.path())?;
152                let hash = util::hex::hash_u64_file(&file)?;
153                result.insert(entry.path().to_path_buf(), hash);
154            } else if file_type.is_symlink() {
155                let hash = util::hex::hash_u64(&fs::read_link(entry.path())?);
156                result.insert(entry.path().to_path_buf(), hash);
157            } else if file_type.is_dir() {
158                let hash = util::hex::hash_u64(&());
159                result.insert(entry.path().to_path_buf(), hash);
160            }
161        }
162        Ok(result)
163    }
164    let result = wrap(path).with_context(|| format!("failed to verify output at {:?}", path))?;
165    Ok(result)
166}
167
168/// Reports the hash difference before and after the compilation computed by  [`hash_all`].
169fn report_hash_difference(orig: &HashMap<PathBuf, u64>, after: &HashMap<PathBuf, u64>) -> String {
170    let mut changed = Vec::new();
171    let mut removed = Vec::new();
172    for (key, value) in orig {
173        match after.get(key) {
174            Some(after_value) => {
175                if value != after_value {
176                    changed.push(key.to_string_lossy());
177                }
178            }
179            None => removed.push(key.to_string_lossy()),
180        }
181    }
182    let mut added: Vec<_> = after
183        .keys()
184        .filter(|key| !orig.contains_key(*key))
185        .map(|key| key.to_string_lossy())
186        .collect();
187    let mut result = Vec::new();
188    if !changed.is_empty() {
189        changed.sort_unstable();
190        result.push(format!("Changed: {}", changed.join("\n\t")));
191    }
192    if !added.is_empty() {
193        added.sort_unstable();
194        result.push(format!("Added: {}", added.join("\n\t")));
195    }
196    if !removed.is_empty() {
197        removed.sort_unstable();
198        result.push(format!("Removed: {}", removed.join("\n\t")));
199    }
200    assert!(!result.is_empty(), "unexpected empty change detection");
201    result.join("\n")
202}