#![warn(rust_2018_idioms, unused_lifetimes)]
#![allow(clippy::multiple_crate_versions)]
use cargo_lambda_build::Zig;
use cargo_lambda_invoke::Invoke;
use cargo_lambda_metadata::{
cargo::{build::Build, deploy::Deploy, load_metadata, watch::Watch},
config::{Config, ConfigOptions, FunctionNames, load_config},
error::MetadataError,
};
use cargo_lambda_new::{Init, New};
use cargo_lambda_system::System;
use cargo_lambda_watch::xray_layer;
use clap::{CommandFactory, Parser, Subcommand};
use clap_cargo::style::CLAP_STYLING;
use miette::{ErrorHook, IntoDiagnostic, Result};
use std::{boxed::Box, env, io::IsTerminal, path::PathBuf, str::FromStr};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Parser)]
#[command(name = "cargo", bin_name = "cargo", disable_version_flag = true)]
#[command(styles = CLAP_STYLING)]
enum App {
Lambda(Lambda),
#[command(subcommand, hide = true)]
Zig(Zig),
}
#[derive(Clone, Debug, Parser)]
struct Lambda {
#[command(subcommand)]
subcommand: Option<Box<LambdaSubcommand>>,
#[arg(short = 'v', long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[arg(
long,
default_value = "auto",
value_name = "WHEN",
global = true,
env = "CARGO_LAMBDA_COLOR"
)]
color: String,
#[arg(long, global = true, env = "CARGO_LAMBDA_GLOBAL")]
global: Option<PathBuf>,
#[arg(short = 'x', long, global = true, env = "CARGO_LAMBDA_CONTEXT")]
context: Option<String>,
#[arg(long, global = true, env = "CARGO_LAMBDA_ADMERGE")]
admerge: bool,
#[arg(short = 'V', long)]
version: bool,
}
#[derive(Clone, Debug, strum_macros::Display, strum_macros::EnumString)]
#[strum(ascii_case_insensitive)]
enum Color {
Auto,
Always,
Never,
}
impl Color {
fn is_ansi(&self) -> bool {
match self {
Color::Auto => std::io::stdout().is_terminal(),
Color::Always => true,
Color::Never => false,
}
}
fn write_env_var(&self) {
unsafe {
std::env::set_var("CARGO_LAMBDA_COLOR", self.to_lowercase());
}
}
fn to_lowercase(&self) -> String {
self.to_string().to_lowercase()
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Subcommand)]
enum LambdaSubcommand {
Build(Build),
Deploy(Deploy),
Init(Init),
Invoke(Invoke),
New(New),
System(System),
Watch(Watch),
}
impl LambdaSubcommand {
async fn run(
self,
color: &str,
global: Option<PathBuf>,
context: Option<String>,
admerge: bool,
) -> Result<()> {
match self {
Self::Build(b) => Self::run_build(b, global, context, admerge).await,
Self::Deploy(d) => Self::run_deploy(d, global, context, admerge).await,
Self::Init(mut i) => i.run().await,
Self::Invoke(i) => i.run().await,
Self::New(mut n) => n.run().await,
Self::System(s) => Self::run_system(s, global, context, admerge).await,
Self::Watch(w) => Self::run_watch(w, color, global, context, admerge).await,
}
}
async fn run_build(
build: Build,
global: Option<PathBuf>,
context: Option<String>,
admerge: bool,
) -> Result<()> {
let options = ConfigOptions {
names: FunctionNames::new(build.pkg_name(), build.bin_name()),
context,
global,
admerge,
};
let metadata = load_metadata(
build.manifest_path(),
build.cargo_opts.common.target_dir.as_deref(),
)?;
let args_config = Config {
build,
..Default::default()
};
let mut config = load_config(&args_config, &metadata, &options)?;
cargo_lambda_build::run(&mut config.build, &metadata).await
}
async fn run_watch(
watch: Watch,
color: &str,
global: Option<PathBuf>,
context: Option<String>,
admerge: bool,
) -> Result<()> {
let options = ConfigOptions {
names: FunctionNames::new(watch.pkg_name(), watch.bin_name()),
context,
global,
admerge,
};
let metadata = load_metadata(
watch.manifest_path(),
watch.cargo_opts.common.target_dir.as_deref(),
)?;
let args_config = Config {
watch,
..Default::default()
};
let config = load_config(&args_config, &metadata, &options)?;
cargo_lambda_watch::run(&config.watch, &config.env, &metadata, color).await
}
async fn run_deploy(
deploy: Deploy,
global: Option<PathBuf>,
context: Option<String>,
admerge: bool,
) -> Result<()> {
let options = ConfigOptions {
names: FunctionNames::new(deploy.name.clone(), deploy.binary_name.clone()),
context,
global,
admerge,
};
let metadata = load_metadata(deploy.manifest_path(), None)?;
let args_config = Config {
deploy,
..Default::default()
};
let config = load_config(&args_config, &metadata, &options)?;
let mut deploy = config.deploy;
deploy.base_env = config.env.clone();
cargo_lambda_deploy::run(&deploy, &metadata).await
}
async fn run_system(
system: System,
global: Option<PathBuf>,
context: Option<String>,
admerge: bool,
) -> Result<()> {
let options = ConfigOptions {
names: FunctionNames::new(system.pkg_name(), None),
global,
context,
admerge,
};
cargo_lambda_system::run(&system, &options).await
}
}
fn print_version() -> Result<()> {
println!(
"cargo-lambda {} {}",
env!("CARGO_PKG_VERSION"),
env!("CARGO_LAMBDA_BUILD_INFO")
);
Ok(())
}
fn print_help() -> Result<()> {
let mut app = App::command();
let lambda = app
.find_subcommand_mut("lambda")
.cloned()
.map(|a| a.name("cargo lambda").bin_name("cargo lambda"));
match lambda {
Some(lambda) => lambda.styles(CLAP_STYLING).print_help().into_diagnostic(),
None => {
println!("Run `cargo lambda --help` to see usage");
Ok(())
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let mut args = env::args();
let program_path = PathBuf::from(args.next().expect("missing program path"));
let program_name = program_path.file_stem().expect("missing program name");
if program_name.eq_ignore_ascii_case("ar") {
let zig = Zig::Ar {
args: args.collect(),
};
run_zig(zig)
} else {
let app = App::parse();
match app {
App::Zig(zig) => run_zig(zig),
App::Lambda(lambda) => {
let color = Color::from_str(&lambda.color)
.expect("invalid color option, must be auto, always, or never");
color.write_env_var();
miette::set_hook(error_hook(Some(&color)))?;
run_subcommand(lambda, color).await
}
}
}
}
async fn run_subcommand(lambda: Lambda, color: Color) -> Result<()> {
if lambda.version {
return print_version();
}
let subcommand = match lambda.subcommand {
None => return print_help(),
Some(subcommand) => subcommand,
};
let log_directive = if lambda.verbose == 0 {
std::env::var("RUST_LOG").unwrap_or_else(|_| "cargo_lambda=info".into())
} else if lambda.verbose == 1 {
"cargo_lambda=debug".into()
} else {
"cargo_lambda=trace".into()
};
let fmt = tracing_subscriber::fmt::layer()
.with_target(false)
.without_time()
.with_ansi(color.is_ansi());
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(log_directive))
.with(fmt);
if let LambdaSubcommand::Watch(w) = &*subcommand {
subscriber.with(xray_layer(w)).init();
} else {
subscriber.init();
}
subcommand
.run(
&color.to_lowercase(),
lambda.global,
lambda.context,
lambda.admerge,
)
.await
}
fn error_hook(color: Option<&Color>) -> ErrorHook {
let ansi = match color {
Some(color) => color.is_ansi(),
None => {
let color = std::env::var("CARGO_LAMBDA_COLOR");
Color::try_from(color.as_deref().unwrap_or("auto"))
.expect("invalid color option, must be auto, always, or never")
.is_ansi()
}
};
Box::new(move |_| {
Box::new(miette::MietteHandlerOpts::new()
.terminal_links(true)
.footer("Was this behavior unexpected?\nStart a thread in https://github.com/cargo-lambda/cargo-lambda/discussions".into())
.color(ansi)
.build())
})
}
fn run_zig(zig: Zig) -> Result<()> {
miette::set_hook(error_hook(None))?;
zig.execute()
.map_err(MetadataError::ZigBuildError)
.into_diagnostic()
}