cargo/ops/cargo_package/
verify.rs1use 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
34pub 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 archive.set_preserve_mtime(false);
60 archive.unpack(dst.parent().unwrap())?;
61
62 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 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 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
138fn 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
168fn 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}