cargo/ops/cargo_package/
verify.rs
1use 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 = 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 archive.set_preserve_mtime(false);
58 archive.unpack(dst.parent().unwrap())?;
59
60 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 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 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
129fn 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
159fn 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}