#![deny(missing_docs)]
#![deny(warnings)]
#[macro_use]
extern crate thiserror;
extern crate serde;
extern crate toml;
#[macro_use]
extern crate serde_derive;
extern crate log;
extern crate rustc_cfg;
mod config;
mod manifest;
mod workspace;
use std::{
env,
fs::File,
io::Read,
path::{Path, PathBuf},
};
use log::{debug, trace};
use rustc_cfg::Cfg;
use serde::Deserialize;
use toml::de;
use config::Config;
use manifest::Manifest;
pub struct Project {
name: String,
target: Option<String>,
target_dir: PathBuf,
toml: PathBuf,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("not a Cargo project")]
NotACargoProject,
#[error("workspace member path is not valid: {0}")]
InvalidWorkspaceMember(String),
#[error("rustc: {0}")]
RustcCfg(#[from] rustc_cfg::Error),
#[error("IO: {0}")]
Io(#[from] std::io::Error),
#[error("Parse: {0}")]
Parse(#[from] toml::de::Error),
}
impl Project {
pub fn query<P>(path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
let path = path.as_ref().canonicalize()?;
let root = search(&path, "Cargo.toml").ok_or(Error::NotACargoProject)?;
debug!(
"Project::query(path={}): root={}",
path.display(),
root.display()
);
let toml = root.join("Cargo.toml");
let cargo_config = Path::new(".cargo").join("config");
let cargo_config_toml = cargo_config.with_extension("toml");
let manifest = parse::<Manifest>(&toml)?;
let mut target = None;
let mut target_dir = env::var_os("CARGO_TARGET_DIR").map(PathBuf::from);
if let Some(path) = path.ancestors().find_map(|dir| {
let path = dir.join(&cargo_config);
if path.exists() {
return Some(path);
}
let path = dir.join(&cargo_config_toml);
if path.exists() {
return Some(path);
}
None
}) {
let config: Config = parse(&path)?;
if let Some(build) = config.build {
target = build.target;
target_dir = target_dir.or(build.target_dir.map(PathBuf::from));
}
}
let mut cwd = root.parent();
let mut workspace = None;
while let Some(path) = cwd {
debug!("workspace search: cwd={}", path.display());
if let Some(outer_root) = search(path, "Cargo.toml") {
if let Ok(manifest) = parse::<workspace::Manifest>(&outer_root.join("Cargo.toml")) {
debug!(
"found workspace: cwd={}, outer_root={}, members={:?}",
path.display(),
outer_root.display(),
manifest.workspace.members,
);
for member_glob in &manifest.workspace.members {
let abs_glob = outer_root.join(member_glob);
let abs_glob = abs_glob
.to_str()
.ok_or_else(|| Error::InvalidWorkspaceMember(member_glob.clone()))?;
for member_dir in glob::glob(abs_glob).map_err(|_| Error::InvalidWorkspaceMember(member_glob.clone()))? {
let member_dir = member_dir.map_err(|e| Error::Io(e.into_error()))?;
trace!("member_dir={}", member_dir.display());
if outer_root.join(member_dir) == root {
workspace = Some(outer_root);
break;
}
}
}
}
cwd = outer_root.parent();
continue;
}
break;
}
target_dir = target_dir.or_else(|| workspace.map(|path| path.join("target")));
Ok(Project {
name: manifest.package.name,
target,
target_dir: target_dir.unwrap_or(root.join("target")),
toml,
})
}
pub fn path(
&self,
artifact: Artifact,
profile: Profile,
target: Option<&str>,
host: &str,
) -> Result<PathBuf, Error> {
let mut path = self.target_dir().to_owned();
if let Some(target) = target.or(self.target()) {
path.push(target);
}
let cfg = Cfg::of(target.or(self.target()).unwrap_or(host))?;
match profile {
Profile::Dev => path.push("debug"),
Profile::Release => path.push("release"),
Profile::__HIDDEN__ => unreachable!(),
}
match artifact {
Artifact::Bin(bin) => {
path.push(bin);
if cfg.target_arch == "wasm32" {
path.set_extension("wasm");
} else if cfg
.target_family
.as_ref()
.map(|f| f == "windows")
.unwrap_or(false)
{
path.set_extension("exe");
}
}
Artifact::Example(example) => {
path.push("examples");
path.push(example);
if cfg.target_arch == "wasm32" {
path.set_extension("wasm");
} else if cfg
.target_family
.as_ref()
.map(|f| f == "windows")
.unwrap_or(false)
{
path.set_extension("exe");
}
}
Artifact::Lib => {
path.push(format!("lib{}.rlib", self.name().replace("-", "_")));
}
Artifact::__HIDDEN__ => unreachable!(),
}
Ok(path)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn target(&self) -> Option<&str> {
self.target.as_ref().map(|s| &**s)
}
pub fn toml(&self) -> &Path {
&self.toml
}
pub fn target_dir(&self) -> &Path {
&self.target_dir
}
}
#[derive(Clone, Copy)]
pub enum Artifact<'a> {
Bin(&'a str),
Example(&'a str),
Lib,
#[doc(hidden)]
__HIDDEN__,
}
#[derive(Clone, Copy, PartialEq)]
pub enum Profile {
Dev,
Release,
#[doc(hidden)]
__HIDDEN__,
}
impl Profile {
pub fn is_release(&self) -> bool {
*self == Profile::Release
}
}
fn search<'p, P: AsRef<Path>>(path: &'p Path, file: P) -> Option<&'p Path> {
path.ancestors().find(|dir| dir.join(&file).exists())
}
fn parse<T>(path: &Path) -> Result<T, Error>
where
T: for<'de> Deserialize<'de>,
{
let mut s = String::new();
File::open(path)?.read_to_string(&mut s)?;
Ok(de::from_str(&s)?)
}
#[cfg(test)]
mod tests {
use std::env;
use super::{Artifact, Profile, Project};
#[test]
fn path() {
let project = Project::query(env::current_dir().unwrap()).unwrap();
let thumb = "thumbv7m-none-eabi";
let wasm = "wasm32-unknown-unknown";
let windows = "x86_64-pc-windows-msvc";
let linux = "x86_64-unknown-linux-gnu";
let p = project
.path(Artifact::Bin("foo"), Profile::Dev, None, windows)
.unwrap();
assert!(p.ends_with("target/debug/foo.exe"));
let p = project
.path(Artifact::Example("bar"), Profile::Dev, None, windows)
.unwrap();
assert!(p.ends_with("target/debug/examples/bar.exe"));
let p = project
.path(Artifact::Bin("foo"), Profile::Dev, Some(thumb), windows)
.unwrap();
assert!(p.ends_with(&format!("target/{}/debug/foo", thumb)));
let p = project
.path(Artifact::Example("bar"), Profile::Dev, Some(thumb), windows)
.unwrap();
assert!(p.ends_with(&format!("target/{}/debug/examples/bar", thumb)));
let p = project
.path(Artifact::Bin("foo"), Profile::Dev, Some(wasm), linux)
.unwrap();
assert!(p.ends_with(&format!("target/{}/debug/foo.wasm", wasm)));
let p = project
.path(Artifact::Example("bar"), Profile::Dev, Some(wasm), linux)
.unwrap();
assert!(p.ends_with(&format!("target/{}/debug/examples/bar.wasm", wasm)));
}
}