cargo_build_dist/
context.rs

1//! Gathers all the environment information and build a Context containing
2//! all relevant information for the rest of the commands.
3
4use 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/// Build a context from the current environment and optionally provided
24/// attributes.
25#[derive(Default)]
26pub struct ContextBuilder {
27    manifest_path: Option<PathBuf>,
28}
29
30impl ContextBuilder {
31    /// Create a new `Context` using the current parameters.
32    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    /// Specify the path to the manifest file to use.
47    ///
48    /// If not called, the default is to use the manifest file in the current
49    /// working directory.
50    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        // MetadataCommand::new() would actually perform the same logic, but we
248        // want the error to be explicit if it happens.
249        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}
276/// A build context.
277pub struct Context {
278    dist_targets: Vec<Box<dyn DistTarget>>,
279}
280
281impl Context {
282    /// Create a new `ContextBuilder`.
283    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    /// Build all the collected distribution targets.
309    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}