use anyhow::Result;
use clap::{arg, Parser, Subcommand, ValueEnum};
use nixpacks::{
create_docker_image, generate_build_plan, get_plan_providers,
nixpacks::{
builder::docker::DockerBuilderOptions,
nix::pkg::Pkg,
plan::{
generator::GeneratePlanOptions,
phase::{Phase, StartPhase},
BuildPlan,
},
},
};
use std::{
collections::hash_map::DefaultHasher,
env,
hash::{Hash, Hasher},
ops::Deref,
string::ToString,
};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum PlanFormat {
Json,
Toml,
}
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Args {
#[command(subcommand)]
command: Commands,
#[arg(long, global = true)]
json_plan: Option<String>,
#[arg(long, short, global = true)]
install_cmd: Option<String>,
#[arg(long, short, global = true)]
build_cmd: Option<String>,
#[arg(long, short, global = true)]
start_cmd: Option<String>,
#[arg(long, short, global = true)]
pkgs: Vec<String>,
#[arg(long, short, global = true)]
apt: Vec<String>,
#[arg(long, global = true)]
libs: Vec<String>,
#[arg(long, short, global = true)]
env: Vec<String>,
#[arg(long, short, global = true)]
config: Option<String>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand)]
enum Commands {
Plan {
path: String,
#[arg(short, long, value_enum, default_value = "json")]
format: PlanFormat,
},
Detect {
path: String,
},
Build {
path: String,
#[arg(short, long)]
name: Option<String>,
#[arg(short, long)]
out: Option<String>,
#[arg(short, long, hide = true)]
dockerfile: bool,
#[arg(short, long)]
tag: Vec<String>,
#[arg(short, long)]
label: Vec<String>,
#[arg(long)]
platform: Vec<String>,
#[arg(long)]
cache_key: Option<String>,
#[arg(long)]
current_dir: bool,
#[arg(long)]
no_cache: bool,
#[arg(long)]
incremental_cache_image: Option<String>,
#[arg(long)]
cache_from: Option<String>,
#[arg(long)]
docker_host: Option<String>,
#[arg(long, global = true)]
add_host: Vec<String>,
#[arg(long)]
docker_tls_verify: Option<String>,
#[arg(long)]
docker_output: Vec<String>,
#[arg(long)]
docker_cert_path: Option<String>,
#[arg(long)]
inline_cache: bool,
#[arg(long)]
no_error_without_start: bool,
#[arg(long)]
cpu_quota: Option<String>,
#[arg(long)]
memory: Option<String>,
#[arg(long, short)]
verbose: bool,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let pkgs = args
.pkgs
.iter()
.map(|p| p.deref())
.map(Pkg::new)
.collect::<Vec<_>>();
let mut cli_plan = BuildPlan::default();
if !args.pkgs.is_empty() || !args.libs.is_empty() || !args.apt.is_empty() {
let mut setup = Phase::setup(Some([pkgs, [Pkg::new("...")].to_vec()].to_vec().concat()));
setup.apt_pkgs = Some([args.apt, ["...".to_string()].to_vec()].to_vec().concat());
setup.nix_libs = Some([args.libs, ["...".to_string()].to_vec()].to_vec().concat());
cli_plan.add_phase(setup);
}
if let Some(install_cmds) = args.install_cmd {
let mut install = Phase::install(None);
install.cmds = Some(vec![install_cmds]);
cli_plan.add_phase(install);
}
if let Some(build_cmds) = args.build_cmd {
let mut build = Phase::build(None);
build.cmds = Some(vec![build_cmds]);
cli_plan.add_phase(build);
}
if let Some(start_cmd) = args.start_cmd {
let start = StartPhase::new(start_cmd);
cli_plan.set_start_phase(start);
}
let json_plan = args.json_plan.map(BuildPlan::from_json).transpose()?;
let cli_plan = if let Some(json_plan) = json_plan {
BuildPlan::merge_plans(&[json_plan, cli_plan])
} else {
cli_plan
};
let env: Vec<&str> = args.env.iter().map(|e| e.deref()).collect();
let options = GeneratePlanOptions {
plan: Some(cli_plan),
config_file: args.config,
};
match args.command {
Commands::Plan { path, format } => {
let plan = generate_build_plan(&path, env, &options)?;
let plan_s = match format {
PlanFormat::Json => plan.to_json()?,
PlanFormat::Toml => plan.to_toml()?,
};
println!("{plan_s}");
}
Commands::Detect { path } => {
let providers = get_plan_providers(&path, env, &options)?;
println!("{}", providers.join(", "));
}
Commands::Build {
path,
name,
out,
dockerfile,
tag,
label,
platform,
cache_key,
current_dir,
no_cache,
incremental_cache_image,
cache_from,
docker_host,
docker_tls_verify,
docker_output,
add_host,
docker_cert_path,
inline_cache,
no_error_without_start,
cpu_quota,
memory,
verbose,
} => {
let verbose = verbose || args.env.contains(&"NIXPACKS_VERBOSE=1".to_string());
let cache_key = if !no_cache && cache_key.is_none() {
get_default_cache_key(&path)?
} else {
cache_key
};
let build_options = &DockerBuilderOptions {
name,
tags: tag,
labels: label,
out_dir: out,
quiet: false,
cache_key,
no_cache,
platform,
print_dockerfile: dockerfile,
current_dir,
inline_cache,
cache_from,
docker_host,
docker_tls_verify,
docker_output,
docker_cert_path,
no_error_without_start,
incremental_cache_image,
cpu_quota,
add_host,
memory,
verbose,
};
create_docker_image(&path, env, &options, build_options).await?;
}
}
Ok(())
}
fn get_default_cache_key(path: &str) -> Result<Option<String>> {
let current_dir = env::current_dir()?;
let source = current_dir.join(path).canonicalize();
if let Ok(source) = source {
let source_str = source.to_string_lossy().to_string();
let mut hasher = DefaultHasher::new();
source_str.hash(&mut hasher);
let encoded_source = base64::encode(hasher.finish().to_be_bytes())
.replace(|c: char| !c.is_alphanumeric(), "");
Ok(Some(encoded_source))
} else {
Ok(None)
}
}