1use std::{2 path::{Path, PathBuf},3 process::exit,4};56use clap::{Parser, Subcommand};7use jrsonnet_pkg::{8 install,9 jsonnet_bundler::{GitSource, JsonnetFile},10};11use tracing::{error, info, warn};1213#[derive(Parser)]14#[clap(about = "A jsonnet package manager")]15struct Opts {16 17 #[clap(long, default_value = "vendor")]18 jsonnetpkg_home: PathBuf,19 #[clap(subcommand)]20 command: Command,21}2223#[derive(Subcommand)]24enum Command {25 26 Init,27 28 Install {29 30 uris: Vec<String>,31 32 #[clap(long)]33 dry_run: bool,34 },35 36 Update {37 38 uris: Vec<String>,39 40 #[clap(long)]41 dry_run: bool,42 },43 44 Remove {45 46 names: Vec<String>,47 48 #[clap(long)]49 dry_run: bool,50 },51}5253const MANIFEST: &str = "jsonnetfile.json";54const LOCKFILE: &str = "jsonnetfile.lock.json";5556fn load_manifest() -> JsonnetFile {57 let path = Path::new(MANIFEST);58 if path.exists() {59 JsonnetFile::load(path).unwrap_or_else(|e| {60 error!("failed to load {MANIFEST}: {e}");61 exit(1);62 })63 } else {64 JsonnetFile {65 version: 1,66 dependencies: Vec::new(),67 legacy_imports: true,68 }69 }70}7172fn save_json(path: &Path, value: &impl serde::Serialize) {73 let json = serde_json::to_string_pretty(value).expect("serialization failed");74 std::fs::write(path, format!("{json}\n")).unwrap_or_else(|e| {75 error!("failed to write {}: {e}", path.display());76 exit(1);77 });78}7980fn load_lockfile() -> Option<JsonnetFile> {81 let path = Path::new(LOCKFILE);82 if path.exists() {83 Some(JsonnetFile::load(path).unwrap_or_else(|e| {84 error!("failed to load {LOCKFILE}: {e}");85 exit(1);86 }))87 } else {88 None89 }90}9192fn do_install(93 manifest: &JsonnetFile,94 lock: Option<&JsonnetFile>,95 vendor_dir: &Path,96 dry_run: bool,97) {98 let new_lock = install::install(manifest, lock, vendor_dir, dry_run).unwrap_or_else(|e| {99 error!("install failed: {e}");100 exit(1);101 });102 if !dry_run {103 save_json(Path::new(LOCKFILE), &new_lock);104 }105}106107#[allow(clippy::too_many_lines)]108fn main() {109 tracing_subscriber::fmt().init();110111 rustls::crypto::ring::default_provider()112 .install_default()113 .expect("install rustls crypto provider");114115 let opts = Opts::parse();116117 match opts.command {118 Command::Init => {119 let path = Path::new(MANIFEST);120 if path.exists() {121 warn!("{MANIFEST} already exists");122 exit(1);123 }124 let jf = JsonnetFile {125 version: 1,126 dependencies: Vec::new(),127 legacy_imports: true,128 };129 save_json(path, &jf);130 }131 Command::Install { uris, dry_run } => {132 let mut manifest = load_manifest();133134 for uri in &uris {135 let dep = GitSource::parse(uri).unwrap_or_else(|| {136 eprintln!("failed to parse URI: {uri}");137 exit(1);138 });139 let is_new = !manifest.dependencies.iter().any(|d| {140 std::mem::discriminant(&d.source) == std::mem::discriminant(&dep.source)141 && d.canonical_name() == dep.canonical_name()142 });143 if is_new {144 manifest.dependencies.push(dep);145 }146 }147148 if !uris.is_empty() {149 save_json(Path::new(MANIFEST), &manifest);150 }151152 let lock = load_lockfile();153 do_install(&manifest, lock.as_ref(), &opts.jsonnetpkg_home, dry_run);154 }155 Command::Update { uris, dry_run } => {156 let mut manifest = load_manifest();157158 if !uris.is_empty() {159 for uri in &uris {160 let dep = GitSource::parse(uri).unwrap_or_else(|| {161 eprintln!("failed to parse URI: {uri}");162 exit(1);163 });164 if let Some(existing) = manifest165 .dependencies166 .iter_mut()167 .find(|d| d.canonical_name() == dep.canonical_name())168 {169 *existing = dep;170 } else {171 manifest.dependencies.push(dep);172 }173 }174 save_json(Path::new(MANIFEST), &manifest);175 }176177 do_install(&manifest, None, &opts.jsonnetpkg_home, dry_run);178 }179 Command::Remove { names, dry_run } => {180 let mut manifest = load_manifest();181182 let matched: Vec<_> = manifest183 .dependencies184 .iter()185 .filter(|dep| {186 names.iter().any(|name| {187 dep.canonical_name() == *name || dep.legacy_link_name() == *name188 })189 })190 .cloned()191 .collect::<Vec<_>>();192193 if matched.is_empty() {194 eprintln!("no matching dependencies found");195 exit(1);196 }197198 for dep in &matched {199 let canonical = dep.canonical_name();200 let dir = opts.jsonnetpkg_home.join(&canonical);201 let legacy = dep.legacy_link_name();202 let link = opts.jsonnetpkg_home.join(&legacy);203 if dry_run {204 info!("would remove: {canonical} ({})", dir.display());205 } else {206 info!("removing: {canonical}");207 if dir.exists() {208 let _ = std::fs::remove_dir_all(&dir);209 }210 if link.symlink_metadata().is_ok() {211 let _ = std::fs::remove_file(&link);212 }213 }214 }215216 if !dry_run {217 manifest.dependencies.retain(|dep| {218 !names.iter().any(|name| {219 dep.canonical_name() == *name || dep.legacy_link_name() == *name220 })221 });222 save_json(Path::new(MANIFEST), &manifest);223 save_json(Path::new(LOCKFILE), &manifest);224 }225 }226 }227}