#![feature(pattern)]
use std::{path::Path, process::Stdio};
use anyhow::anyhow;
use cargo_metadata::Metadata;
use cargo_metadata::{MetadataCommand, Package};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::process::ExitCode;
use std::str::pattern::Pattern;
use clap::Parser;
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("Unexpected error: `{0}`")]
Unexpected(#[from] anyhow::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Parser)]
struct CommandlineArguments {
#[arg(long)]
workspace: bool,
}
fn parse_cli() -> CommandlineArguments {
const CARGO_COMMAND_NAME: &str = "customs";
let mut args = std::env::args().peekable();
let executable = args
.next()
.expect("exec must be invoked with at least the executable");
if let Some(first_arg) = args.peek()
&& first_arg == CARGO_COMMAND_NAME {
let _customs = args.next();
}
let args = std::iter::once(executable).chain(args);
CommandlineArguments::parse_from(args)
}
fn main() -> ExitCode {
let result = run();
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{e}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<()> {
let args = parse_cli();
let metadata = MetadataCommand::new().exec().map_err(|e| anyhow!(e))?;
let cwd = std::env::current_dir().map_err(|e| anyhow!(e))?;
let executed_from_workspace_root = cwd == metadata.workspace_root;
let workspace = args.workspace || executed_from_workspace_root;
let current_package = find_current_package(&metadata, &cwd);
let packages_to_check: Vec<&Package> = if workspace {
metadata
.packages
.iter()
.filter(|e| metadata.workspace_members.contains(&e.id))
.collect()
} else {
if current_package.is_none() {
return Err(anyhow::anyhow!("Failed to identify current package.").into());
}
std::iter::once(current_package.unwrap()).collect()
};
for package in packages_to_check {
let info = load_customs(package, &metadata);
let info = match info {
Some(e) => e,
None => continue,
};
let directory = package
.manifest_path
.parent()
.expect("Manifest must be in some directory");
for regulation in info.regulation.iter().flat_map(|e| e.expand()) {
regulation.check(directory.as_std_path()).unwrap();
}
}
Ok(())
}
fn find_current_package<'a>(metadata: &'a Metadata, cwd: &Path) -> Option<&'a Package> {
let cwd = cwd.to_str().expect("Failed path to string conversion");
metadata
.packages
.iter()
.filter(|e| metadata.workspace_members.contains(&e.id))
.map(|p| {
let package_dir = p
.manifest_path
.parent()
.expect("every manifest must be in a directory");
let package_dir = package_dir.as_str();
if package_dir.is_prefix_of(cwd) {
(p, package_dir.len())
} else {
(p, 0)
}
})
.max_by(|(_, a), (_, b)| a.cmp(b))
.filter(|(_, e)| *e > 0)
.map(|(p, _)| p)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CustomsFile {
pub default: Option<Regulation>,
#[serde(default)]
pub regulation: Vec<Regulation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Regulation {
#[serde(default)]
#[serde(rename = "platform-targets")]
pub platform_targets: Vec<String>,
#[serde(default)]
#[serde(rename = "build-targets")]
pub build_targets: Vec<String>,
#[serde(default)]
pub jobs: Vec<String>,
}
pub fn load_customs(package: &Package, metadata: &Metadata) -> Option<CustomsFile> {
const CUSTOMS_FILE_NAME: &str = "Customs.toml";
let workspace_root = metadata.workspace_root.clone();
let customs = package
.manifest_path
.ancestors()
.take_while(|e| e.as_std_path() != workspace_root.as_std_path())
.map(|e| e.parent().unwrap().join(CUSTOMS_FILE_NAME))
.collect::<Vec<_>>();
let customs = customs
.into_iter()
.map(std::fs::read_to_string)
.flat_map(|e| e.ok())
.map(|e| {
let info: CustomsFile = toml::from_str(&e).unwrap();
info
})
.rev()
.collect::<Vec<_>>();
let mut crate_customs = customs.last().unwrap().clone();
let default = customs
.into_iter()
.flat_map(|e| e.default.clone()) .last();
if let Some(default) = default {
for regulation in crate_customs.regulation.iter_mut() {
if regulation.platform_targets.is_empty() {
regulation.platform_targets = default.platform_targets.clone();
}
if regulation.build_targets.is_empty() {
regulation.build_targets = default.build_targets.clone();
}
if regulation.jobs.is_empty() {
regulation.jobs = default.jobs.clone();
}
}
}
Some(crate_customs)
}
impl Regulation {
pub fn expand(&self) -> Vec<RegulationCheck> {
let build_targets = self.build_targets.clone();
const ALL_BUILD_TARGETS_DESIGNATOR: &str = "all";
if build_targets
.iter()
.any(|e| e == ALL_BUILD_TARGETS_DESIGNATOR)
&& build_targets.len() != 1
{
panic!("build-targets all can only be alone");
}
self.platform_targets
.iter()
.cartesian_product(build_targets.iter())
.cartesian_product(self.jobs.iter())
.map(|((p, b), j)| RegulationCheck {
platform_target: p.clone(),
build_target: b.clone(),
job: j.clone(),
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct RegulationCheck {
pub platform_target: String,
pub build_target: String,
pub job: String,
}
impl RegulationCheck {
pub fn check(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
let build_target: String = match self.build_target.as_str() {
"lib" => "--lib".into(),
"bins" => "--bins".into(),
"tests" => "--tests".into(),
"all" => "--all-targets".into(),
_ => {
if let Some(bin) = self.build_target.strip_prefix("bin:") {
format!("--bin={bin}")
} else {
panic!("invalid build target {}", self.build_target)
}
}
};
let mut platform_target = self.platform_target.clone();
const HOST_PLATFORM_DESIGNATOR: &str = "host";
if platform_target == HOST_PLATFORM_DESIGNATOR {
platform_target = get_host_platform_target();
}
let status = std::process::Command::new("cargo")
.arg(self.job.as_str())
.arg(format!("--target={platform_target}"))
.arg(build_target)
.current_dir(path)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if !status.success() {
anyhow::bail!("failed"); }
Ok(())
}
}
fn get_host_platform_target() -> String {
use rustc_version::version_meta;
let meta = version_meta().expect("Failed to get rustc version");
meta.host
}