1#![deny(missing_docs)]
2#![allow(dead_code)]
3//! Structured access to the output of `cargo metadata`
4//! Usually used from within a `cargo-*` executable
5
6// Forked from `https://github.com/oli-obk/cargo_metadata`
7// Modifications:
8// 1. Remove `resolve` from Metadata because it was causing parse failures
9// 2. Fix the `manifest-path` argument
10// 3. Add `--all-features` argument
11// 4. Remove the `--no-deps` argument
12
13use std::borrow::{Borrow, Cow};
14use std::collections::{HashMap, HashSet};
15use std::env;
16use std::error;
17use std::fmt;
18use std::hash::{Hash, Hasher};
19use std::io;
20use std::path::Path;
21use std::process::{Command, Output};
22use std::str::Utf8Error;
23
24#[derive(Clone, Deserialize, Debug)]
25/// Starting point for metadata returned by `cargo metadata`
26pub struct Metadata {
27 /// A list of all crates referenced by this crate (and the crate itself)
28 pub packages: HashSet<Package>,
29 version: usize,
30 /// path to the workspace containing the `Cargo.lock`
31 pub workspace_root: String,
32}
33
34/// A reference to a package including it's name and the specific version.
35#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
36pub struct PackageRef {
37 pub name: String,
38 pub version: Option<String>,
39}
40
41#[derive(Clone, Deserialize, Debug)]
42/// A crate
43pub struct Package {
44 #[serde(flatten)]
45 pub name_and_version: PackageRef,
46 id: String,
47 source: Option<String>,
48 /// List of dependencies of this particular package
49 pub dependencies: HashSet<Dependency>,
50 /// Targets provided by the crate (lib, bin, example, test, ...)
51 pub targets: Vec<Target>,
52 features: HashMap<String, Vec<String>>,
53 /// path containing the `Cargo.toml`
54 pub manifest_path: String,
55}
56
57#[derive(Clone, Deserialize, Debug)]
58/// A dependency of the main crate
59pub struct Dependency {
60 /// Name as given in the `Cargo.toml`
61 pub name: String,
62 source: Option<String>,
63 /// Whether this is required or optional
64 pub req: String,
65 kind: Option<String>,
66 optional: bool,
67 uses_default_features: bool,
68 features: Vec<String>,
69 pub target: Option<String>,
70}
71
72#[derive(Clone, Deserialize, Debug)]
73/// A single target (lib, bin, example, ...) provided by a crate
74pub struct Target {
75 /// Name as given in the `Cargo.toml` or generated from the file name
76 pub name: String,
77 /// Kind of target ("bin", "example", "test", "bench", "lib")
78 pub kind: Vec<String>,
79 /// Almost the same as `kind`, except when an example is a library instad of an executable.
80 /// In that case `crate_types` contains things like `rlib` and `dylib` while `kind` is `example`
81 #[serde(default)]
82 pub crate_types: Vec<String>,
83 /// Path to the main source file of the target
84 pub src_path: String,
85}
86
87#[derive(Debug)]
88/// Possible errors that can occur during metadata parsing.
89pub enum Error {
90 /// Error during execution of `cargo metadata`
91 Io(io::Error),
92 /// Metadata extraction failure
93 Metadata(Output),
94 /// Output of `cargo metadata` was not valid utf8
95 Utf8(Utf8Error),
96 /// Deserialization error (structure of json did not match expected structure)
97 Json(serde_json::Error),
98}
99
100impl From<io::Error> for Error {
101 fn from(err: io::Error) -> Self {
102 Error::Io(err)
103 }
104}
105impl From<Utf8Error> for Error {
106 fn from(err: Utf8Error) -> Self {
107 Error::Utf8(err)
108 }
109}
110impl From<serde_json::Error> for Error {
111 fn from(err: serde_json::Error) -> Self {
112 Error::Json(err)
113 }
114}
115
116impl fmt::Display for Error {
117 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
118 match self {
119 Error::Io(ref err: &Error) => err.fmt(f),
120 Error::Metadata(_) => write!(f, "Metadata error"),
121 Error::Utf8(ref err: &Utf8Error) => err.fmt(f),
122 Error::Json(ref err: &Error) => err.fmt(f),
123 }
124 }
125}
126
127impl error::Error for Error {
128 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
129 match self {
130 Error::Io(ref err: &Error) => Some(err),
131 Error::Metadata(_) => None,
132 Error::Utf8(ref err: &Utf8Error) => Some(err),
133 Error::Json(ref err: &Error) => Some(err),
134 }
135 }
136}
137
138// Implementations that let us lookup Packages and Dependencies by name (string)
139
140impl Borrow<PackageRef> for Package {
141 fn borrow(&self) -> &PackageRef {
142 &self.name_and_version
143 }
144}
145
146impl Hash for Package {
147 fn hash<H: Hasher>(&self, state: &mut H) {
148 self.name_and_version.hash(state);
149 }
150}
151
152impl PartialEq for Package {
153 fn eq(&self, other: &Self) -> bool {
154 self.name_and_version == other.name_and_version
155 }
156}
157
158impl Eq for Package {}
159
160impl Borrow<str> for Dependency {
161 fn borrow(&self) -> &str {
162 &self.name
163 }
164}
165
166impl Hash for Dependency {
167 fn hash<H: Hasher>(&self, state: &mut H) {
168 self.name.hash(state);
169 }
170}
171
172impl PartialEq for Dependency {
173 fn eq(&self, other: &Self) -> bool {
174 self.name == other.name
175 }
176}
177
178impl Eq for Dependency {}
179
180fn discover_target(manifest_path: &Path) -> Option<String> {
181 if let Ok(target) = std::env::var("TARGET") {
182 return Some(target);
183 }
184
185 // We must be running as a standalone script, not under cargo.
186 // Let's use the host platform instead.
187 // We figure out the host platform through rustc and use that.
188 // We unfortunatelly cannot go through cargo, since cargo rustc _also_ builds.
189 // If `rustc` fails to run, we just fall back to not passing --filter-platforms.
190 //
191 // NOTE: We set the current directory in case of rustup shenanigans.
192 let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc"));
193 debug!("Discovering host platform by {:?}", rustc);
194
195 let rustc_output = Command::new(rustc)
196 .current_dir(manifest_path.parent().unwrap())
197 .arg("-vV")
198 .output();
199 let rustc_output = match rustc_output {
200 Ok(ref out) => String::from_utf8_lossy(&out.stdout),
201 Err(..) => return None,
202 };
203
204 let field = "host: ";
205 rustc_output
206 .lines()
207 .find_map(|l| l.strip_prefix(field).map(|stripped| stripped.to_string()))
208}
209
210/// The main entry point to obtaining metadata
211pub fn metadata(
212 manifest_path: &Path,
213 existing_metadata_file: Option<&Path>,
214 only_target: bool,
215) -> Result<Metadata, Error> {
216 let output;
217 let metadata = match existing_metadata_file {
218 Some(path) => Cow::Owned(std::fs::read_to_string(path)?),
219 None => {
220 let target = if only_target {
221 let target = discover_target(manifest_path);
222 if target.is_none() {
223 warn!(
224 "Failed to discover host platform for cargo metadata; \
225 will fetch dependencies for all platforms."
226 );
227 }
228 target
229 } else {
230 None
231 };
232
233 let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo"));
234 let mut cmd = Command::new(cargo);
235 cmd.arg("metadata");
236 cmd.arg("--all-features");
237 cmd.arg("--format-version").arg("1");
238 if let Some(target) = target {
239 cmd.arg("--filter-platform").arg(target);
240 }
241 cmd.arg("--manifest-path");
242 cmd.arg(manifest_path);
243 output = cmd.output()?;
244 if !output.status.success() {
245 return Err(Error::Metadata(output));
246 }
247 Cow::Borrowed(std::str::from_utf8(&output.stdout)?)
248 }
249 };
250
251 let meta: Metadata = serde_json::from_str(&metadata)?;
252 Ok(meta)
253}
254