use anyhow::{anyhow, Context};
use chef::{
AllFeatures, CommandArg, CookArgs, DefaultFeatures, OptimisationProfile, Recipe, TargetArgs,
};
use clap::crate_version;
use clap::Parser;
use fs_err as fs;
use std::collections::HashSet;
use std::io::IsTerminal;
use std::path::PathBuf;
#[derive(Parser)]
#[command(
bin_name = "cargo",
version = crate_version!(),
author = "Luca Palmieri <rust@lpalmieri.com>"
)]
pub struct Cli {
#[command(subcommand)]
command: CargoInvocation,
}
#[derive(Parser)]
pub enum CargoInvocation {
Chef {
#[command(subcommand)]
command: Command,
},
}
#[derive(Parser)]
#[command(
version = crate_version!(),
author = "Luca Palmieri <rust@lpalmieri.com>"
)]
pub enum Command {
Prepare(Prepare),
Cook(Cook),
}
#[derive(Parser)]
pub struct Prepare {
#[arg(long, default_value = "recipe.json")]
recipe_path: PathBuf,
#[arg(long)]
bin: Option<String>,
}
#[derive(Parser)]
pub struct Cook {
#[arg(long, default_value = "recipe.json")]
recipe_path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
release: bool,
#[arg(long)]
check: bool,
#[arg(long)]
clippy: bool,
#[arg(long)]
target: Option<Vec<String>>,
#[arg(long, env = "CARGO_TARGET_DIR")]
target_dir: Option<PathBuf>,
#[arg(long)]
no_default_features: bool,
#[arg(long)]
all_features: bool,
#[arg(long, value_delimiter = ',')]
features: Option<Vec<String>>,
#[arg(short = 'Z')]
unstable_features: Option<Vec<String>>,
#[arg(long)]
benches: bool,
#[arg(long)]
tests: bool,
#[arg(long)]
examples: bool,
#[arg(long)]
all_targets: bool,
#[arg(long)]
manifest_path: Option<PathBuf>,
#[arg(long, short = 'p')]
package: Option<Vec<String>>,
#[arg(long)]
workspace: bool,
#[arg(long)]
offline: bool,
#[arg(long)]
locked: bool,
#[arg(long, short = 'v')]
verbose: bool,
#[arg(long)]
frozen: bool,
#[arg(long)]
timings: bool,
#[arg(long)]
no_std: bool,
#[arg(long)]
bin: Option<Vec<String>>,
#[arg(long)]
bins: bool,
#[arg(long)]
zigbuild: bool,
#[clap(long)]
no_build: bool,
}
fn _main() -> Result<(), anyhow::Error> {
let current_directory = std::env::current_dir().unwrap();
let cli = Cli::parse();
let command = match cli.command {
CargoInvocation::Chef { command } => command,
};
match command {
Command::Cook(Cook {
recipe_path,
profile,
release,
check,
clippy,
target,
no_default_features,
all_features,
features,
unstable_features,
target_dir,
benches,
tests,
examples,
all_targets,
manifest_path,
package,
workspace,
offline,
frozen,
locked,
verbose,
timings,
no_std,
bin,
zigbuild,
bins,
no_build,
}) => {
if std::io::stdout().is_terminal() {
eprintln!("WARNING stdout appears to be a terminal.");
eprintln!(
"cargo-chef is not meant to be run in an interactive environment \
and will overwrite some existing files (namely any `lib.rs`, `main.rs` and \
`Cargo.toml` it finds)."
);
eprintln!();
eprint!("To continue anyway, type `yes`: ");
let mut answer = String::with_capacity(3);
std::io::stdin()
.read_line(&mut answer)
.context("Failed to read from stdin")?;
if "yes" != answer.trim() {
std::process::exit(1);
}
}
let features: Option<HashSet<String>> = features.and_then(|features| {
if features.is_empty() {
None
} else {
Some(features.into_iter().collect())
}
});
let unstable_features: Option<HashSet<String>> =
unstable_features.and_then(|unstable_features| {
if unstable_features.is_empty() {
None
} else {
Some(unstable_features.into_iter().collect())
}
});
let profile = match (release, profile) {
(false, None) => OptimisationProfile::Debug,
(false, Some(profile)) if profile == "dev" => OptimisationProfile::Debug,
(true, None) => OptimisationProfile::Release,
(false, Some(profile)) if profile == "release" => OptimisationProfile::Release,
(false, Some(custom_profile)) => OptimisationProfile::Other(custom_profile),
(true, Some(_)) => Err(anyhow!("You specified both --release and --profile arguments. Please remove one of them, or both"))?
};
let command = match (check, clippy, zigbuild, no_build) {
(true, false, false, false) => CommandArg::Check,
(false, true, false, false) => CommandArg::Clippy,
(false, false, true, false) => CommandArg::Zigbuild,
(false, false, false, true) => CommandArg::NoBuild,
(false, false, false, false) => CommandArg::Build,
_ => Err(anyhow!("Only one (or none) of the `clippy`, `check`, `zigbuild`, and `no-build` arguments are allowed. Please remove some of them, or all"))?,
};
let default_features = if no_default_features {
DefaultFeatures::Disabled
} else {
DefaultFeatures::Enabled
};
let all_features = if all_features {
AllFeatures::Enabled
} else {
AllFeatures::Disabled
};
let serialized = fs::read_to_string(recipe_path)
.context("Failed to read recipe from the specified path.")?;
let recipe: Recipe =
serde_json::from_str(&serialized).context("Failed to deserialize recipe.")?;
let target_args = TargetArgs {
benches,
tests,
examples,
all_targets,
};
recipe
.cook(CookArgs {
profile,
command,
default_features,
all_features,
features,
unstable_features,
target,
target_dir,
target_args,
manifest_path,
package,
workspace,
offline,
timings,
no_std,
bin,
locked,
frozen,
verbose,
bins,
no_build,
})
.context("Failed to cook recipe.")?;
}
Command::Prepare(Prepare { recipe_path, bin }) => {
let recipe =
Recipe::prepare(current_directory, bin).context("Failed to compute recipe")?;
let serialized =
serde_json::to_string(&recipe).context("Failed to serialize recipe.")?;
fs::write(recipe_path, serialized).context("Failed to save recipe to 'recipe.json'")?;
}
}
Ok(())
}
fn main() -> Result<(), anyhow::Error> {
env_logger::init();
_main()
}