cargo_project/
lib.rs

1//! Library to retrieve information about a Cargo project
2//!
3//! Useful for building Cargo subcommands that require building the project.
4
5#![deny(missing_docs)]
6#![deny(warnings)]
7
8#[macro_use]
9extern crate thiserror;
10extern crate serde;
11extern crate toml;
12#[macro_use]
13extern crate serde_derive;
14extern crate log;
15extern crate rustc_cfg;
16
17mod config;
18mod manifest;
19mod workspace;
20
21use std::{
22    env,
23    fs::File,
24    io::Read,
25    path::{Path, PathBuf},
26};
27
28use log::{debug, trace};
29use rustc_cfg::Cfg;
30use serde::Deserialize;
31use toml::de;
32
33use config::Config;
34use manifest::Manifest;
35
36/// Information about a Cargo project
37pub struct Project {
38    name: String,
39
40    target: Option<String>,
41
42    target_dir: PathBuf,
43
44    toml: PathBuf,
45}
46
47/// Errors
48#[derive(Debug, Error)]
49pub enum Error {
50    /// error: not a Cargo project
51    #[error("not a Cargo project")]
52    NotACargoProject,
53    /// Invalid syntax in [workspace.members]
54    #[error("workspace member path is not valid: {0}")]
55    InvalidWorkspaceMember(String),
56    /// rustc-cfg error
57    #[error("rustc: {0}")]
58    RustcCfg(#[from] rustc_cfg::Error),
59    /// IO error
60    #[error("IO: {0}")]
61    Io(#[from] std::io::Error),
62    /// TOML parse error
63    #[error("Parse: {0}")]
64    Parse(#[from] toml::de::Error),
65}
66
67impl Project {
68    /// Retrieves information about the Cargo project at the given `path`
69    ///
70    /// `path` doesn't need to be the directory that contains the `Cargo.toml` file; it can be any
71    /// point within the Cargo project.
72    pub fn query<P>(path: P) -> Result<Self, Error>
73    where
74        P: AsRef<Path>,
75    {
76        let path = path.as_ref().canonicalize()?;
77        let root = search(&path, "Cargo.toml").ok_or(Error::NotACargoProject)?;
78        debug!(
79            "Project::query(path={}): root={}",
80            path.display(),
81            root.display()
82        );
83
84        // parse Cargo.toml
85        let toml = root.join("Cargo.toml");
86        let cargo_config = Path::new(".cargo").join("config");
87        let cargo_config_toml = cargo_config.with_extension("toml");
88        let manifest = parse::<Manifest>(&toml)?;
89
90        // parse .cargo/config
91        let mut target = None;
92        let mut target_dir = env::var_os("CARGO_TARGET_DIR").map(PathBuf::from);
93        if let Some(path) = path.ancestors().find_map(|dir| {
94            let path = dir.join(&cargo_config);
95            if path.exists() {
96                return Some(path);
97            }
98
99            let path = dir.join(&cargo_config_toml);
100            if path.exists() {
101                return Some(path);
102            }
103
104            None
105        }) {
106            let config: Config = parse(&path)?;
107
108            if let Some(build) = config.build {
109                target = build.target;
110                target_dir = target_dir.or(build.target_dir.map(PathBuf::from));
111            }
112        }
113
114        // is this project member of a workspace?
115        let mut cwd = root.parent();
116        let mut workspace = None;
117        while let Some(path) = cwd {
118            debug!("workspace search: cwd={}", path.display());
119            if let Some(outer_root) = search(path, "Cargo.toml") {
120                if let Ok(manifest) = parse::<workspace::Manifest>(&outer_root.join("Cargo.toml")) {
121                    debug!(
122                        "found workspace: cwd={}, outer_root={}, members={:?}",
123                        path.display(),
124                        outer_root.display(),
125                        manifest.workspace.members,
126                    );
127                    // this is indeed a workspace
128                    for member_glob in &manifest.workspace.members {
129                        let abs_glob = outer_root.join(member_glob);
130                        let abs_glob = abs_glob
131                            .to_str()
132                            .ok_or_else(|| Error::InvalidWorkspaceMember(member_glob.clone()))?;
133                        for member_dir in glob::glob(abs_glob).map_err(|_| Error::InvalidWorkspaceMember(member_glob.clone()))? {
134                            let member_dir = member_dir.map_err(|e| Error::Io(e.into_error()))?;
135                            trace!("member_dir={}", member_dir.display());
136                            if outer_root.join(member_dir) == root {
137                                // we are a member of this workspace
138                                workspace = Some(outer_root);
139                                break;
140                            }
141                        }
142                    }
143                }
144
145                // this is not a workspace; keep looking
146                cwd = outer_root.parent();
147                continue;
148            }
149
150            break;
151        }
152
153        target_dir = target_dir.or_else(|| workspace.map(|path| path.join("target")));
154
155        Ok(Project {
156            name: manifest.package.name,
157            target,
158            target_dir: target_dir.unwrap_or(root.join("target")),
159            toml,
160        })
161    }
162
163    /// Returns the path to a build artifact
164    ///
165    /// # Inputs
166    ///
167    /// - `artifact` is the kind of build artifact: `Bin` (`--bin`), `Example` (`--example`), `Lib`
168    /// (`--lib`)
169    /// - `profile` is the compilation profile: `Dev` or `Release` (`--release`)
170    /// - `target` is the specified compilation target (`--target`)
171    /// - `host` is the triple of host -- this is used as the compilation target when no `target` is
172    /// specified and the project has no default build target
173    pub fn path(
174        &self,
175        artifact: Artifact,
176        profile: Profile,
177        target: Option<&str>,
178        host: &str,
179    ) -> Result<PathBuf, Error> {
180        let mut path = self.target_dir().to_owned();
181
182        if let Some(target) = target.or(self.target()) {
183            path.push(target);
184        }
185
186        let cfg = Cfg::of(target.or(self.target()).unwrap_or(host))?;
187
188        match profile {
189            Profile::Dev => path.push("debug"),
190            Profile::Release => path.push("release"),
191            Profile::__HIDDEN__ => unreachable!(),
192        }
193
194        match artifact {
195            Artifact::Bin(bin) => {
196                path.push(bin);
197
198                if cfg.target_arch == "wasm32" {
199                    path.set_extension("wasm");
200                } else if cfg
201                    .target_family
202                    .as_ref()
203                    .map(|f| f == "windows")
204                    .unwrap_or(false)
205                {
206                    path.set_extension("exe");
207                }
208            }
209            Artifact::Example(example) => {
210                path.push("examples");
211                path.push(example);
212
213                if cfg.target_arch == "wasm32" {
214                    path.set_extension("wasm");
215                } else if cfg
216                    .target_family
217                    .as_ref()
218                    .map(|f| f == "windows")
219                    .unwrap_or(false)
220                {
221                    path.set_extension("exe");
222                }
223            }
224            Artifact::Lib => {
225                path.push(format!("lib{}.rlib", self.name().replace("-", "_")));
226            }
227            Artifact::__HIDDEN__ => unreachable!(),
228        }
229
230        Ok(path)
231    }
232
233    /// Returns the name of the project (`package.name`)
234    pub fn name(&self) -> &str {
235        &self.name
236    }
237
238    /// Returns the default compilation target
239    pub fn target(&self) -> Option<&str> {
240        self.target.as_ref().map(|s| &**s)
241    }
242
243    /// Returns the path to the project's `Cargo.toml`
244    pub fn toml(&self) -> &Path {
245        &self.toml
246    }
247
248    /// Returns the target directory path
249    ///
250    /// This is where build artifacts are placed
251    pub fn target_dir(&self) -> &Path {
252        &self.target_dir
253    }
254}
255
256/// Build artifact
257#[derive(Clone, Copy)]
258pub enum Artifact<'a> {
259    /// Binary (`--bin`)
260    Bin(&'a str),
261    /// Example (`--example`)
262    Example(&'a str),
263    /// Library (`--lib`)
264    Lib,
265    #[doc(hidden)]
266    __HIDDEN__,
267}
268
269/// Build profile
270#[derive(Clone, Copy, PartialEq)]
271pub enum Profile {
272    /// Development profile
273    Dev,
274    /// Release profile (`--release`)
275    Release,
276    #[doc(hidden)]
277    __HIDDEN__,
278}
279
280impl Profile {
281    /// Is this the release profile?
282    pub fn is_release(&self) -> bool {
283        *self == Profile::Release
284    }
285}
286
287/// Search for `file` in `path` and its parent directories
288fn search<'p, P: AsRef<Path>>(path: &'p Path, file: P) -> Option<&'p Path> {
289    path.ancestors().find(|dir| dir.join(&file).exists())
290}
291
292fn parse<T>(path: &Path) -> Result<T, Error>
293where
294    T: for<'de> Deserialize<'de>,
295{
296    let mut s = String::new();
297    File::open(path)?.read_to_string(&mut s)?;
298    Ok(de::from_str(&s)?)
299}
300
301#[cfg(test)]
302mod tests {
303    use std::env;
304
305    use super::{Artifact, Profile, Project};
306
307    #[test]
308    fn path() {
309        let project = Project::query(env::current_dir().unwrap()).unwrap();
310
311        let thumb = "thumbv7m-none-eabi";
312        let wasm = "wasm32-unknown-unknown";
313        let windows = "x86_64-pc-windows-msvc";
314        let linux = "x86_64-unknown-linux-gnu";
315
316        let p = project
317            .path(Artifact::Bin("foo"), Profile::Dev, None, windows)
318            .unwrap();
319
320        assert!(p.ends_with("target/debug/foo.exe"));
321
322        let p = project
323            .path(Artifact::Example("bar"), Profile::Dev, None, windows)
324            .unwrap();
325
326        assert!(p.ends_with("target/debug/examples/bar.exe"));
327
328        let p = project
329            .path(Artifact::Bin("foo"), Profile::Dev, Some(thumb), windows)
330            .unwrap();
331
332        assert!(p.ends_with(&format!("target/{}/debug/foo", thumb)));
333
334        let p = project
335            .path(Artifact::Example("bar"), Profile::Dev, Some(thumb), windows)
336            .unwrap();
337
338        assert!(p.ends_with(&format!("target/{}/debug/examples/bar", thumb)));
339
340        let p = project
341            .path(Artifact::Bin("foo"), Profile::Dev, Some(wasm), linux)
342            .unwrap();
343
344        assert!(p.ends_with(&format!("target/{}/debug/foo.wasm", wasm)));
345
346        let p = project
347            .path(Artifact::Example("bar"), Profile::Dev, Some(wasm), linux)
348            .unwrap();
349
350        assert!(p.ends_with(&format!("target/{}/debug/examples/bar.wasm", wasm)));
351    }
352}