mod build_dependencies;
mod cargo_workspace;
mod env;
mod manifest_path;
pub mod project_json;
mod rustc_cfg;
mod sysroot;
pub mod target_data_layout;
mod workspace;
#[cfg(test)]
mod tests;
use std::{
fmt,
fs::{self, read_dir, ReadDir},
io,
process::Command,
};
use anyhow::{bail, format_err, Context};
use paths::{AbsPath, AbsPathBuf, Utf8PathBuf};
use rustc_hash::FxHashSet;
pub use crate::{
build_dependencies::WorkspaceBuildScripts,
cargo_workspace::{
CargoConfig, CargoFeatures, CargoWorkspace, Package, PackageData, PackageDependency,
RustLibSource, Target, TargetData, TargetKind,
},
manifest_path::ManifestPath,
project_json::{ProjectJson, ProjectJsonData},
sysroot::Sysroot,
workspace::{FileLoader, PackageRoot, ProjectWorkspace, ProjectWorkspaceKind},
};
pub use cargo_metadata::Metadata;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectJsonFromCommand {
pub data: ProjectJsonData,
pub buildfile: AbsPathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum ProjectManifest {
ProjectJson(ManifestPath),
CargoToml(ManifestPath),
CargoScript(ManifestPath),
}
impl ProjectManifest {
pub fn from_manifest_file(path: AbsPathBuf) -> anyhow::Result<ProjectManifest> {
let path = ManifestPath::try_from(path)
.map_err(|path| format_err!("bad manifest path: {path}"))?;
if path.file_name().unwrap_or_default() == "rust-project.json" {
return Ok(ProjectManifest::ProjectJson(path));
}
if path.file_name().unwrap_or_default() == ".rust-project.json" {
return Ok(ProjectManifest::ProjectJson(path));
}
if path.file_name().unwrap_or_default() == "Cargo.toml" {
return Ok(ProjectManifest::CargoToml(path));
}
if path.extension().unwrap_or_default() == "rs" {
return Ok(ProjectManifest::CargoScript(path));
}
bail!("project root must point to a Cargo.toml, rust-project.json or <script>.rs file: {path}");
}
pub fn discover_single(path: &AbsPath) -> anyhow::Result<ProjectManifest> {
let mut candidates = ProjectManifest::discover(path)?;
let res = match candidates.pop() {
None => bail!("no projects"),
Some(it) => it,
};
if !candidates.is_empty() {
bail!("more than one project");
}
Ok(res)
}
pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> {
if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
}
if let Some(project_json) = find_in_parent_dirs(path, ".rust-project.json") {
return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
}
return find_cargo_toml(path)
.map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<ManifestPath>> {
match find_in_parent_dirs(path, "Cargo.toml") {
Some(it) => Ok(vec![it]),
None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)),
}
}
fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<ManifestPath> {
if path.file_name().unwrap_or_default() == target_file_name {
if let Ok(manifest) = ManifestPath::try_from(path.to_path_buf()) {
return Some(manifest);
}
}
let mut curr = Some(path);
while let Some(path) = curr {
let candidate = path.join(target_file_name);
if fs::metadata(&candidate).is_ok() {
if let Ok(manifest) = ManifestPath::try_from(candidate) {
return Some(manifest);
}
}
curr = path.parent();
}
None
}
fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<ManifestPath> {
entities
.filter_map(Result::ok)
.map(|it| it.path().join("Cargo.toml"))
.filter(|it| it.exists())
.map(Utf8PathBuf::from_path_buf)
.filter_map(Result::ok)
.map(AbsPathBuf::try_from)
.filter_map(Result::ok)
.filter_map(|it| it.try_into().ok())
.collect()
}
}
pub fn discover_all(paths: &[AbsPathBuf]) -> Vec<ProjectManifest> {
let mut res = paths
.iter()
.filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
.flatten()
.collect::<FxHashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
res.sort();
res
}
pub fn manifest_path(&self) -> &ManifestPath {
match self {
ProjectManifest::ProjectJson(it)
| ProjectManifest::CargoToml(it)
| ProjectManifest::CargoScript(it) => it,
}
}
}
impl fmt::Display for ProjectManifest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.manifest_path(), f)
}
}
fn utf8_stdout(mut cmd: Command) -> anyhow::Result<String> {
let output = cmd.output().with_context(|| format!("{cmd:?} failed"))?;
if !output.status.success() {
match String::from_utf8(output.stderr) {
Ok(stderr) if !stderr.is_empty() => {
bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr)
}
_ => bail!("{:?} failed, {}", cmd, output.status),
}
}
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout.trim().to_owned())
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum InvocationStrategy {
Once,
#[default]
PerWorkspace,
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct CfgOverrides {
pub global: cfg::CfgDiff,
pub selective: rustc_hash::FxHashMap<String, cfg::CfgDiff>,
}
impl CfgOverrides {
pub fn len(&self) -> usize {
self.global.len() + self.selective.values().map(|it| it.len()).sum::<usize>()
}
pub fn apply(&self, cfg_options: &mut cfg::CfgOptions, name: &str) {
if !self.global.is_empty() {
cfg_options.apply_diff(self.global.clone());
};
if let Some(diff) = self.selective.get(name) {
cfg_options.apply_diff(diff.clone());
};
}
}
fn parse_cfg(s: &str) -> Result<cfg::CfgAtom, String> {
let res = match s.split_once('=') {
Some((key, value)) => {
if !(value.starts_with('"') && value.ends_with('"')) {
return Err(format!("Invalid cfg ({s:?}), value should be in quotes"));
}
let key = intern::Symbol::intern(key);
let value = intern::Symbol::intern(&value[1..value.len() - 1]);
cfg::CfgAtom::KeyValue { key, value }
}
None => cfg::CfgAtom::Flag(intern::Symbol::intern(s)),
};
Ok(res)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SysrootQueryMetadata {
#[default]
CargoMetadata,
None,
}