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