Skip to main content

sqlx_cli/
metadata.rs

1use std::{
2    collections::{btree_map, BTreeMap, BTreeSet},
3    ffi::OsStr,
4    path::{Path, PathBuf},
5    process::Command,
6    str::FromStr,
7};
8
9use anyhow::Context;
10use cargo_metadata::{
11    Metadata as CargoMetadata, Package as MetadataPackage, PackageId as MetadataId,
12    PackageName as MetadataPackageName,
13};
14
15/// The minimal amount of package information we care about
16///
17/// The package's `name` is used to `cargo clean -p` specific crates while the `src_paths` are
18/// are used to trigger recompiles of packages within the workspace
19#[derive(Debug)]
20pub struct Package {
21    name: MetadataPackageName,
22    src_paths: Vec<PathBuf>,
23}
24
25impl Package {
26    pub fn name(&self) -> &str {
27        self.name.as_str()
28    }
29
30    pub fn src_paths(&self) -> &[PathBuf] {
31        &self.src_paths
32    }
33}
34
35impl From<&MetadataPackage> for Package {
36    fn from(package: &MetadataPackage) -> Self {
37        let name = package.name.clone();
38        let src_paths = package
39            .targets
40            .iter()
41            .map(|target| target.src_path.clone().into_std_path_buf())
42            .collect();
43
44        Self { name, src_paths }
45    }
46}
47
48/// Contains metadata for the current project
49pub struct Metadata {
50    /// Maps packages metadata id to the package
51    ///
52    /// Currently `MetadataId` is used over `PkgId` because pkgid is not a UUID
53    packages: BTreeMap<MetadataId, Package>,
54    /// All of the crates in the current workspace
55    workspace_members: Vec<MetadataId>,
56    /// Workspace root path.
57    workspace_root: PathBuf,
58    /// Maps each dependency to its set of dependents
59    reverse_deps: BTreeMap<MetadataId, BTreeSet<MetadataId>>,
60    /// The target directory of the project
61    ///
62    /// Typically `target` at the workspace root, but can be overridden
63    target_directory: PathBuf,
64    /// Crate in the current working directory, empty if run from a
65    /// virtual workspace root.
66    current_package: Option<Package>,
67}
68
69impl Metadata {
70    /// Parse the manifest from the current working directory using `cargo metadata`.
71    pub fn from_current_directory(cargo: &OsStr) -> anyhow::Result<Self> {
72        let output = Command::new(cargo)
73            .args(["metadata", "--format-version=1"])
74            .output()
75            .context("Could not fetch metadata")?;
76
77        std::str::from_utf8(&output.stdout)
78            .context("Invalid `cargo metadata` output")?
79            .parse()
80            .context("Issue parsing `cargo metadata` output - consider manually running it to check for issues")
81    }
82
83    pub fn package(&self, id: &MetadataId) -> Option<&Package> {
84        self.packages.get(id)
85    }
86
87    pub fn entries(&self) -> btree_map::Iter<'_, MetadataId, Package> {
88        self.packages.iter()
89    }
90
91    pub fn workspace_members(&self) -> &[MetadataId] {
92        &self.workspace_members
93    }
94
95    pub fn workspace_root(&self) -> &Path {
96        &self.workspace_root
97    }
98
99    pub fn target_directory(&self) -> &Path {
100        &self.target_directory
101    }
102
103    pub fn current_package(&self) -> Option<&Package> {
104        self.current_package.as_ref()
105    }
106
107    /// Gets all dependents (direct and transitive) of `id`
108    pub fn all_dependents_of(&self, id: &MetadataId) -> BTreeSet<&MetadataId> {
109        let mut dependents = BTreeSet::new();
110        self.all_dependents_of_helper(id, &mut dependents);
111        dependents
112    }
113
114    fn all_dependents_of_helper<'this>(
115        &'this self,
116        id: &MetadataId,
117        dependents: &mut BTreeSet<&'this MetadataId>,
118    ) {
119        if let Some(immediate_dependents) = self.reverse_deps.get(id) {
120            for immediate_dependent in immediate_dependents {
121                if dependents.insert(immediate_dependent) {
122                    self.all_dependents_of_helper(immediate_dependent, dependents);
123                }
124            }
125        }
126    }
127}
128
129impl FromStr for Metadata {
130    type Err = anyhow::Error;
131
132    fn from_str(s: &str) -> Result<Self, Self::Err> {
133        let cargo_metadata: CargoMetadata = serde_json::from_str(s)?;
134
135        // Extract the package in the current working directory, empty if run from a
136        // virtual workspace root.
137        let current_package: Option<Package> = cargo_metadata.root_package().map(Package::from);
138
139        let CargoMetadata {
140            packages: metadata_packages,
141            workspace_members,
142            workspace_root,
143            resolve,
144            target_directory,
145            ..
146        } = cargo_metadata;
147
148        let mut packages = BTreeMap::new();
149        for metadata_package in metadata_packages {
150            let package = Package::from(&metadata_package);
151            packages.insert(metadata_package.id, package);
152        }
153
154        let mut reverse_deps: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
155        let resolve =
156            resolve.context("Resolving the dependency graph failed (old version of cargo)")?;
157        for node in resolve.nodes {
158            for dep in node.deps {
159                let dependent = node.id.clone();
160                let dependency = dep.pkg;
161                reverse_deps
162                    .entry(dependency)
163                    .or_default()
164                    .insert(dependent);
165            }
166        }
167
168        let workspace_root = workspace_root.into_std_path_buf();
169        let target_directory = target_directory.into_std_path_buf();
170
171        Ok(Self {
172            packages,
173            workspace_members,
174            workspace_root,
175            reverse_deps,
176            target_directory,
177            current_package,
178        })
179    }
180}
181
182/// The absolute path to the directory containing the `Cargo.toml` manifest.
183/// Depends on the current working directory.
184pub(crate) fn manifest_dir(cargo: &OsStr) -> anyhow::Result<PathBuf> {
185    let stdout = Command::new(cargo)
186        .args(["locate-project", "--message-format=plain"])
187        .output()
188        .context("could not locate manifest directory")?
189        .stdout;
190
191    let mut manifest_path: PathBuf = std::str::from_utf8(&stdout)
192        .context("output of `cargo locate-project` was not valid UTF-8")?
193        // remove trailing newline
194        .trim()
195        .into();
196
197    manifest_path.pop();
198    Ok(manifest_path)
199}