1use cargo_metadata::PackageId;
5use log::debug;
6use sha2::{Digest, Sha256};
7use std::{
8 cmp::Ordering,
9 collections::BTreeSet,
10 fmt::Display,
11 path::{Path, PathBuf},
12 time::Instant,
13};
14
15use crate::{
16 action_step,
17 dist_target::{BuildOptions, BuildResult, DistTarget},
18 ignore_step,
19 metadata::Metadata,
20 Error, Result,
21};
22
23#[derive(Default)]
26pub struct ContextBuilder {
27 manifest_path: Option<PathBuf>,
28}
29
30impl ContextBuilder {
31 pub fn build(&self) -> Result<Context> {
33 debug!("Building context.");
34
35 let metadata = self.get_metadata()?;
36 let target_root = Self::get_target_root(&metadata);
37
38 debug!("Using target directory: {}", target_root.display());
39
40 let packages = Self::scan_packages(&metadata)?;
41 let dist_targets = self.resolve_dist_targets(&metadata, &target_root, packages)?;
42
43 Ok(Context::new(dist_targets))
44 }
45
46 pub fn with_manifest_path(mut self, manifest_path: impl Into<PathBuf>) -> Self {
51 self.manifest_path = Some(manifest_path.into());
52
53 self
54 }
55
56 fn get_dependencies(
57 &self,
58 metadata: &cargo_metadata::Metadata,
59 package_id: &PackageId,
60 ) -> Result<Dependencies> {
61 let resolve = match &metadata.resolve {
62 Some(resolve) => resolve,
63 None => {
64 return Err(Error::new("`resolve` section not found in the workspace")
65 .with_explanation(format!(
66 "The `resolve` section is missing for workspace {}\
67 which prevents the resolution of dependencies.",
68 metadata.workspace_root
69 )))
70 }
71 };
72
73 Ok(self
74 .get_dependencies_from_resolve(resolve, package_id)?
75 .map(|package_id| {
76 let package = &metadata[package_id];
77 Dependency {
78 name: package.name.clone(),
79 version: package.version.to_string(),
80 }
81 })
82 .collect())
83 }
84
85 fn get_dependencies_from_resolve<'a>(
86 &self,
87 resolve: &'a cargo_metadata::Resolve,
88 package_id: &'a PackageId,
89 ) -> Result<impl Iterator<Item = &'a PackageId>> {
90 let node = resolve
91 .nodes
92 .iter()
93 .find(|node| node.id == *package_id)
94 .ok_or_else(|| {
95 Error::new("could not resolve dependencies").with_explanation(format!(
96 "Unable to resolve dependencies for package {}.",
97 package_id
98 ))
99 })?;
100
101 let deps: Result<Vec<&PackageId>> = node
102 .dependencies
103 .iter()
104 .map(
105 |package_id| match self.get_dependencies_from_resolve(resolve, package_id) {
106 Ok(deps) => Ok(deps),
107 Err(err) => Err(Error::new("transitive dependency failure").with_source(err)),
108 },
109 )
110 .flat_map(|v| match v {
111 Ok(v) => v.map(Ok).collect(),
112 Err(e) => vec![Err(e)],
113 })
114 .collect();
115
116 Ok(std::iter::once(package_id).chain(deps?.into_iter()))
117 }
118
119 fn resolve_dist_targets(
120 &self,
121 metadata: &cargo_metadata::Metadata,
122 target_root: &Path,
123 packages: impl IntoIterator<Item = (PackageId, Metadata)>,
124 ) -> Result<Vec<Box<dyn DistTarget>>> {
125 packages
126 .into_iter()
127 .map(|(package_id, package_metadata)| {
128 let package = &metadata[&package_id];
129
130 debug!("Resolving package {} {}", package.name, package.version);
131
132 if let Some(deps_hash) = package_metadata.deps_hash {
133 debug!("Package has a dependency hash specified: making sure it is up-to-date.");
134
135 let dependencies = self.get_dependencies(metadata, &package.id)?;
136
137 match dependencies.len() {
138 0 => debug!("Package has no dependencies"),
139 1 => debug!("Package has one dependency"),
140 x => debug!(
141 "Package has {} dependencies: {}",
142 x,
143 dependencies
144 .iter()
145 .map(Dependency::to_string)
146 .collect::<Vec<String>>()
147 .join(", "),
148 ),
149 };
150
151 let current_deps_hash = get_dependencies_hash(&dependencies);
152
153 if current_deps_hash != deps_hash {
154 return Err(
155 Error::new("dependencies hash does not match")
156 .with_explanation("The specified dependency hash does not match the actual computed version.\n\n\
157 This may indicate that some dependencies have changed and may require a major/minor version bump. \n\n\
158 Please validate this and update the dependencies hash to confirm the new dependencies.")
159 .with_output(format!(
160 "Expected: {}\n \
161 Actual: {}",
162 deps_hash,
163 current_deps_hash
164 ))
165 );
166 }
167
168 debug!("Package dependency hash is up-to-date. Moving on.");
169 }
170
171 let mut dist_targets: Vec<Box<dyn DistTarget>> = vec![];
172
173 for (name, target) in package_metadata.targets {
174 dist_targets.push(target.into_dist_target(name.clone(), target_root, package)?);
175 }
176
177 Ok(dist_targets)
178 })
179 .flat_map(|v| match v {
180 Ok(v) => v.into_iter().map(Ok).collect(),
181 Err(e) => vec![Err(e)],
182 })
183 .collect()
184 }
185
186 fn scan_packages(metadata: &cargo_metadata::Metadata) -> Result<Vec<(PackageId, Metadata)>> {
187 metadata
188 .workspace_members
189 .iter()
190 .filter_map(|package_id| {
191 let package = &metadata[package_id];
192
193 if package.metadata.is_null() {
194 debug!("Ignoring package without metadata: {}", package_id);
195
196 return None;
197 }
198
199 let metadata = match package.metadata.as_object() {
200 Some(metadata) => metadata,
201 None => {
202 return Some(Err(Error::new("package metadata is not an object")
203 .with_explanation(format!(
204 "Metadata was found for package {} but it was unexpectedly not a JSON object.",
205 package_id,
206 ))));
207 }
208 };
209
210 let metadata = if let Some(metadata) = metadata.get("build-dist") {
211 metadata
212 } else {
213 debug!(
214 "Ignoring package without `build-dist` metadata: {}",
215 package_id
216 );
217
218 return None;
219 };
220
221 debug!("Considering package {} {}", package.name, package.version);
222
223 let metadata = match serde_path_to_error::deserialize(metadata) {
224 Ok(metadata) => metadata,
225 Err(e) => {
226 return Some(Err(Error::new("failed to parse `build-dist` metadata")
227 .with_source(e)
228 .with_explanation(format!(
229 "The metadata for package {} does not seem to be valid.",
230 package_id
231 ))));
232 }
233 };
234
235 Some(Ok((package_id.clone(), metadata)))
236 })
237 .collect()
238 }
239
240 fn get_target_root(metadata: &cargo_metadata::Metadata) -> PathBuf {
241 PathBuf::from(metadata.target_directory.as_path())
242 }
243
244 fn get_metadata(&self) -> Result<cargo_metadata::Metadata> {
245 let mut cmd = cargo_metadata::MetadataCommand::new();
246
247 let cargo = Self::get_cargo_path()?;
250
251 debug!("Using `cargo` at: {}", cargo.display());
252
253 cmd.cargo_path(cargo);
254
255 if let Some(manifest_path) = &self.manifest_path {
256 cmd.manifest_path(manifest_path);
257 }
258
259 cmd.exec()
260 .map_err(|e| Error::new("failed to query cargo metadata").with_source(e))
261 }
262
263 fn get_cargo_path() -> Result<PathBuf> {
264 match std::env::var("CARGO") {
265 Ok(cargo) => Ok(PathBuf::from(&cargo)),
266 Err(e) => {
267 Err(
268 Error::new("`cargo` not found")
269 .with_source(e)
270 .with_explanation("The `CARGO` environment variable was not set: it is usually set by `cargo` itself.\nMake sure that `cargo build-dist` is run through `cargo` by putting its containing folder in your `PATH`."),
271 )
272 }
273 }
274 }
275}
276pub struct Context {
278 dist_targets: Vec<Box<dyn DistTarget>>,
279}
280
281impl Context {
282 pub fn builder() -> ContextBuilder {
284 ContextBuilder::default()
285 }
286
287 fn new(dist_targets: Vec<Box<dyn DistTarget>>) -> Self {
288 match dist_targets.len() {
289 0 => debug!("Context built successfully but has no distribution targets"),
290 1 => debug!(
291 "Context built successfully with one distribution target: {}",
292 dist_targets[0],
293 ),
294 x => debug!(
295 "Context built successfully with {} distribution targets: {}",
296 x,
297 dist_targets
298 .iter()
299 .map(ToString::to_string)
300 .collect::<Vec<String>>()
301 .join(", "),
302 ),
303 };
304
305 Self { dist_targets }
306 }
307
308 pub fn build_dist_targets(&self, options: &BuildOptions) -> Result<()> {
310 match self.dist_targets.len() {
311 0 => {}
312 1 => action_step!("Processing", "one distribution target",),
313 x => action_step!("Processing", "{} distribution targets", x),
314 };
315
316 for dist_target in &self.dist_targets {
317 action_step!("Building", dist_target.to_string());
318 let now = Instant::now();
319
320 match dist_target.build(options)? {
321 BuildResult::Success => {
322 action_step!(
323 "Finished",
324 "{} in {:.2}s",
325 dist_target,
326 now.elapsed().as_secs_f64()
327 );
328 }
329 BuildResult::Ignored(reason) => {
330 ignore_step!("Ignored", "{}", reason,);
331 }
332 }
333 }
334
335 Ok(())
336 }
337}
338
339type Dependencies = BTreeSet<Dependency>;
340
341#[derive(Debug, Eq, Clone)]
342struct Dependency {
343 pub name: String,
344 pub version: String,
345}
346
347impl Display for Dependency {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 write!(f, "{} {}", self.name, self.version)
350 }
351}
352
353impl Ord for Dependency {
354 fn cmp(&self, other: &Self) -> Ordering {
355 self.name
356 .cmp(&other.name)
357 .then(self.version.cmp(&other.version))
358 }
359}
360
361impl PartialOrd for Dependency {
362 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
363 Some(self.cmp(other))
364 }
365}
366
367impl PartialEq for Dependency {
368 fn eq(&self, other: &Self) -> bool {
369 self.name == other.name && self.version == other.version
370 }
371}
372
373fn get_dependencies_hash(dependencies: &Dependencies) -> String {
374 let mut deps_hasher = Sha256::new();
375
376 for dep in dependencies {
377 deps_hasher.update(&dep.name);
378 deps_hasher.update(" ");
379 deps_hasher.update(&dep.version);
380 }
381
382 format!("{:x}", deps_hasher.finalize())
383}