1#![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
36pub struct Project {
38 name: String,
39
40 target: Option<String>,
41
42 target_dir: PathBuf,
43
44 toml: PathBuf,
45}
46
47#[derive(Debug, Error)]
49pub enum Error {
50 #[error("not a Cargo project")]
52 NotACargoProject,
53 #[error("workspace member path is not valid: {0}")]
55 InvalidWorkspaceMember(String),
56 #[error("rustc: {0}")]
58 RustcCfg(#[from] rustc_cfg::Error),
59 #[error("IO: {0}")]
61 Io(#[from] std::io::Error),
62 #[error("Parse: {0}")]
64 Parse(#[from] toml::de::Error),
65}
66
67impl Project {
68 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 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 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 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 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 workspace = Some(outer_root);
139 break;
140 }
141 }
142 }
143 }
144
145 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 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 pub fn name(&self) -> &str {
235 &self.name
236 }
237
238 pub fn target(&self) -> Option<&str> {
240 self.target.as_ref().map(|s| &**s)
241 }
242
243 pub fn toml(&self) -> &Path {
245 &self.toml
246 }
247
248 pub fn target_dir(&self) -> &Path {
252 &self.target_dir
253 }
254}
255
256#[derive(Clone, Copy)]
258pub enum Artifact<'a> {
259 Bin(&'a str),
261 Example(&'a str),
263 Lib,
265 #[doc(hidden)]
266 __HIDDEN__,
267}
268
269#[derive(Clone, Copy, PartialEq)]
271pub enum Profile {
272 Dev,
274 Release,
276 #[doc(hidden)]
277 __HIDDEN__,
278}
279
280impl Profile {
281 pub fn is_release(&self) -> bool {
283 *self == Profile::Release
284 }
285}
286
287fn 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}