Skip to main content

crap4rust/
cargo_package_resolver.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::collections::BTreeSet;
6use std::path::{Path, PathBuf};
7
8use anyhow::{Context, Result, bail};
9use cargo_metadata::{Metadata, MetadataCommand, Package};
10
11use crate::config::Config;
12use crate::package_context::PackageContext;
13use crate::source_root_collector::SourceRootCollector;
14use crate::traits::package_resolver::PackageResolver;
15
16#[derive(Debug, Clone, Copy, Default)]
17pub struct CargoPackageResolver;
18
19impl CargoPackageResolver {
20    #[must_use]
21    pub const fn new() -> Self {
22        Self
23    }
24
25    fn build_package_context(
26        package: &Package,
27        workspace_root: &Path,
28        include_test_targets: bool,
29        exclude_paths: Vec<String>,
30    ) -> Result<PackageContext> {
31        let manifest_dir = package
32            .manifest_path
33            .clone()
34            .into_std_path_buf()
35            .parent()
36            .map(PathBuf::from)
37            .context("package manifest has no parent directory")?;
38
39        let mut collector = SourceRootCollector::new(include_test_targets, &manifest_dir);
40        collector.collect(&package.targets);
41        let source_roots = collector.finalize();
42
43        Ok(PackageContext {
44            name: package.name.to_string(),
45            manifest_dir,
46            workspace_root: workspace_root.to_path_buf(),
47            source_roots,
48            include_test_targets,
49            exclude_paths,
50        })
51    }
52
53    fn select_packages<'a>(
54        metadata: &'a Metadata,
55        requested: &[String],
56    ) -> Result<Vec<&'a Package>> {
57        if !requested.is_empty() {
58            let mut selected = Vec::new();
59            for package_name in requested {
60                let package = metadata
61                    .packages
62                    .iter()
63                    .find(|package| package.name == package_name)
64                    .with_context(|| {
65                        format!("package {package_name} was not found in the manifest")
66                    })?;
67                selected.push(package);
68            }
69            return Ok(selected);
70        }
71
72        if let Some(root) = metadata.root_package()
73            && metadata.workspace_members.len() <= 1
74        {
75            return Ok(vec![root]);
76        }
77
78        let workspace_member_ids = metadata
79            .workspace_members
80            .iter()
81            .cloned()
82            .collect::<BTreeSet<_>>();
83        let selected = metadata
84            .packages
85            .iter()
86            .filter(|package| workspace_member_ids.contains(&package.id))
87            .collect::<Vec<_>>();
88        if selected.is_empty() {
89            bail!("manifest contains no workspace member packages");
90        }
91
92        Ok(selected)
93    }
94}
95
96impl PackageResolver for CargoPackageResolver {
97    fn resolve(&self, config: &Config) -> Result<Vec<PackageContext>> {
98        let mut command = MetadataCommand::new();
99        command.no_deps();
100        if let Some(manifest_path) = &config.manifest_path {
101            command.manifest_path(manifest_path);
102        }
103
104        let metadata = command.exec().context("failed to read Cargo metadata")?;
105        let workspace_root = metadata.workspace_root.clone().into_std_path_buf();
106        let packages = Self::select_packages(&metadata, &config.packages)?;
107
108        packages
109            .into_iter()
110            .map(|package| {
111                Self::build_package_context(
112                    package,
113                    &workspace_root,
114                    config.include_test_targets,
115                    config.exclude_paths.clone(),
116                )
117            })
118            .collect()
119    }
120}