1#![allow(dead_code)]
19
20use cli_ui::styles::{paint, CYAN, DIM, OK};
21use cli_ui::{bail, header, ok, phase, step, summary, CliCommand, CliOptions};
22use std::path::PathBuf;
23
24fn workspace_examples() -> Vec<String> {
27 let Ok(rd) = std::fs::read_dir("examples") else {
28 return Vec::new();
29 };
30 let mut v: Vec<String> = rd
31 .flatten()
32 .filter_map(|e| {
33 let p = e.path();
34 if p.is_file() && p.extension()?.to_str() == Some("rs") {
35 Some(p.file_stem()?.to_string_lossy().to_string())
36 } else {
37 None
38 }
39 })
40 .collect();
41 v.sort();
42 v
43}
44
45fn workspace_bins() -> Vec<String> {
46 let Ok(rd) = std::fs::read_dir("src/bin") else {
47 return Vec::new();
48 };
49 let mut v: Vec<String> = rd
50 .flatten()
51 .filter_map(|e| {
52 let p = e.path();
53 if p.is_file() && p.extension()?.to_str() == Some("rs") {
54 Some(p.file_stem()?.to_string_lossy().to_string())
55 } else {
56 None
57 }
58 })
59 .collect();
60 v.sort();
61 v
62}
63
64fn workspace_members() -> Vec<String> {
65 let Ok(src) = std::fs::read_to_string("Cargo.toml") else {
67 return Vec::new();
68 };
69 let mut members = Vec::new();
70 let mut in_members = false;
71 for line in src.lines() {
72 let t = line.trim();
73 if t == "members = [" || t.starts_with("members = [") {
74 in_members = true;
75 }
76 if in_members {
77 if t == "]" {
78 break;
79 }
80 if let Some(s) = t
81 .trim_matches(|c| c == '"' || c == ',' || c == '[' || c == ']')
82 .split('"')
83 .next()
84 {
85 let clean = s.trim().trim_matches('"').trim_matches(',').trim();
86 if !clean.is_empty() && !clean.starts_with('[') {
87 members.push(clean.to_string());
88 }
89 }
90 }
91 }
92 members
93}
94
95fn available_features() -> Vec<String> {
96 let Ok(src) = std::fs::read_to_string("Cargo.toml") else {
98 return Vec::new();
99 };
100 let mut features = Vec::new();
101 let mut in_features = false;
102 for line in src.lines() {
103 let t = line.trim();
104 if t == "[features]" {
105 in_features = true;
106 continue;
107 }
108 if in_features {
109 if t.starts_with('[') {
110 break;
111 }
112 if let Some(name) = t.split('=').next() {
113 let n = name.trim();
114 if !n.is_empty() && !n.starts_with('#') {
115 features.push(n.to_string());
116 }
117 }
118 }
119 }
120 features
121}
122
123fn known_profiles() -> Vec<String> {
124 vec![
125 "dev".into(),
126 "release".into(),
127 "test".into(),
128 "bench".into(),
129 ]
130}
131
132fn known_targets() -> Vec<String> {
133 vec![
134 "x86_64-unknown-linux-gnu".into(),
135 "x86_64-unknown-linux-musl".into(),
136 "x86_64-apple-darwin".into(),
137 "aarch64-apple-darwin".into(),
138 "x86_64-pc-windows-msvc".into(),
139 "wasm32-unknown-unknown".into(),
140 "wasm32-wasi".into(),
141 "aarch64-unknown-linux-gnu".into(),
142 ]
143}
144
145fn known_editions() -> Vec<String> {
146 vec!["2015".into(), "2018".into(), "2021".into(), "2024".into()]
147}
148
149fn known_vcs() -> Vec<String> {
150 vec![
151 "git".into(),
152 "hg".into(),
153 "pijul".into(),
154 "fossil".into(),
155 "none".into(),
156 ]
157}
158
159#[derive(CliOptions)]
162#[cli(about = "global flags for mini-cargo")]
163struct Global {
164 #[arg(short = 'v', long = "verbose", negatable)]
166 verbose: bool,
167
168 #[arg(short = 'q', long = "quiet", negatable)]
170 quiet: bool,
171
172 #[arg(
174 long = "color",
175 default = "auto",
176 validate(one_of("auto", "always", "never"))
177 )]
178 color: String,
179
180 #[arg(long = "manifest-path", validate(is_file, ext("toml")))]
182 manifest_path: Option<PathBuf>,
183
184 #[arg(long = "target-dir", validate(is_dir))]
186 target_dir: Option<PathBuf>,
187}
188
189#[derive(CliOptions)]
193#[cli(
194 name = "build",
195 about = "Compile the current package",
196 tagline = "all targets by default",
197 example = "cargo-mini build --release",
198 example = "cargo-mini build --target wasm32-unknown-unknown"
199)]
200struct BuildOpt {
201 #[arg(section = "Build", short = 'r', long = "release")]
203 release: bool,
204
205 #[arg(section = "Build", long = "example", complete = workspace_examples)]
207 example: Option<String>,
208
209 #[arg(section = "Build", long = "bin", complete = workspace_bins)]
211 bin: Option<String>,
212
213 #[arg(section = "Features", short = 'F', long = "features",
215 complete = available_features)]
216 features: Option<String>,
217
218 #[arg(section = "Features", long = "all-features")]
220 all_features: bool,
221
222 #[arg(section = "Features", long = "no-default-features")]
224 no_default_features: bool,
225
226 #[arg(section = "Target", long = "target", complete = known_targets)]
228 target: Option<String>,
229
230 #[arg(section = "Target", long = "profile", complete = known_profiles)]
232 profile: Option<String>,
233
234 #[arg(
236 section = "Performance",
237 short = 'j',
238 long = "jobs",
239 default = "4",
240 validate(range(1, 256))
241 )]
242 jobs: usize,
243
244 #[arg(section = "Output", long = "quiet", negatable)]
246 quiet: bool,
247
248 #[arg(section = "Output", long = "verbose", negatable)]
250 verbose: bool,
251}
252
253#[derive(CliOptions)]
255#[cli(
256 name = "run",
257 about = "Run a binary or example of the local package",
258 example = "cargo-mini run --example mini_cargo",
259 example = "cargo-mini run --release -- --help"
260)]
261struct RunOpt {
262 #[arg(section = "Target", long = "example", complete = workspace_examples)]
264 example: Option<String>,
265
266 #[arg(section = "Target", long = "bin", complete = workspace_bins)]
268 bin: Option<String>,
269
270 #[arg(section = "Build", short = 'r', long = "release")]
272 release: bool,
273
274 #[arg(section = "Features", short = 'F', long = "features",
276 complete = available_features)]
277 features: Option<String>,
278
279 #[arg(section = "Build", long = "profile", complete = known_profiles)]
281 profile: Option<String>,
282
283 #[arg(section = "Build", long = "target", complete = known_targets)]
285 target: Option<String>,
286
287 #[arg(skip)]
289 _extra_args: Vec<String>,
290}
291
292#[derive(CliOptions)]
294#[cli(
295 name = "add",
296 about = "Add dependencies to a Cargo.toml manifest file",
297 example = "cargo-mini add serde --features derive",
298 example = "cargo-mini add tokio --features full --no-default-features"
299)]
300struct AddOpt {
301 #[arg(positional)]
303 crate_name: String,
304
305 #[arg(section = "Dependency", short = 'F', long = "features")]
307 features: Option<String>,
308
309 #[arg(section = "Dependency", long = "dev")]
311 dev: bool,
312
313 #[arg(section = "Dependency", long = "build")]
315 build: bool,
316
317 #[arg(section = "Workspace", short = 'p', long = "package",
319 complete = workspace_members)]
320 package: Option<String>,
321
322 #[arg(section = "Misc", long = "dry-run")]
324 dry_run: bool,
325
326 #[arg(section = "Dependency", long = "no-default-features")]
328 no_default_features: bool,
329}
330
331#[derive(CliOptions)]
333#[cli(
334 name = "remove",
335 about = "Remove dependencies from a Cargo.toml manifest file",
336 example = "cargo-mini remove serde"
337)]
338struct RemoveOpt {
339 #[arg(positional)]
341 crate_name: String,
342
343 #[arg(section = "Dependency", long = "dev")]
345 dev: bool,
346
347 #[arg(section = "Dependency", long = "build")]
349 build: bool,
350
351 #[arg(section = "Workspace", short = 'p', long = "package",
353 complete = workspace_members)]
354 package: Option<String>,
355}
356
357#[derive(CliOptions)]
359#[cli(
360 name = "new",
361 about = "Create a new Cargo package",
362 example = "cargo-mini new my-project --lib",
363 example = "cargo-mini new my-cli --edition 2021 --vcs git"
364)]
365struct NewOpt {
366 #[arg(positional)]
368 path: PathBuf,
369
370 #[arg(section = "Template", long = "lib")]
372 lib: bool,
373
374 #[arg(section = "Template", long = "bin")]
376 bin: bool,
377
378 #[arg(
380 section = "Manifest",
381 long = "edition",
382 default = "2021",
383 validate(one_of("2015", "2018", "2021", "2024"))
384 )]
385 edition: String,
386
387 #[arg(
389 section = "Manifest",
390 long = "vcs",
391 validate(one_of("git", "hg", "pijul", "fossil", "none"))
392 )]
393 vcs: Option<String>,
394
395 #[arg(section = "Manifest", long = "name")]
397 name: Option<String>,
398}
399
400#[derive(CliOptions)]
402#[cli(
403 name = "init",
404 about = "Create a new Cargo package in an existing directory",
405 example = "cargo-mini init",
406 example = "cargo-mini init my-dir --lib"
407)]
408struct InitOpt {
409 #[arg(positional)]
411 path: Option<PathBuf>,
412
413 #[arg(section = "Template", long = "lib")]
415 lib: bool,
416
417 #[arg(section = "Template", long = "bin")]
419 bin: bool,
420
421 #[arg(
423 section = "Manifest",
424 long = "edition",
425 default = "2021",
426 validate(one_of("2015", "2018", "2021", "2024"))
427 )]
428 edition: String,
429
430 #[arg(
432 section = "Manifest",
433 long = "vcs",
434 validate(one_of("git", "hg", "pijul", "fossil", "none"))
435 )]
436 vcs: Option<String>,
437}
438
439#[derive(CliOptions)]
441#[cli(
442 name = "test",
443 about = "Execute all unit and integration tests and build examples",
444 example = "cargo-mini test --lib",
445 example = "cargo-mini test integration_test -- --nocapture"
446)]
447struct TestOpt {
448 #[arg(positional)]
450 filter: Option<String>,
451
452 #[arg(section = "Target", long = "lib")]
454 lib: bool,
455
456 #[arg(section = "Target", long = "example", complete = workspace_examples)]
458 example: Option<String>,
459
460 #[arg(section = "Build", long = "release")]
462 release: bool,
463
464 #[arg(section = "Features", short = 'F', long = "features",
466 complete = available_features)]
467 features: Option<String>,
468
469 #[arg(
471 section = "Output",
472 long = "test-threads",
473 default = "4",
474 validate(range(1, 256))
475 )]
476 test_threads: usize,
477
478 #[arg(section = "Output", long = "nocapture")]
480 nocapture: bool,
481}
482
483#[derive(CliOptions)]
485#[cli(
486 name = "check",
487 about = "Check a local package and all of its dependencies for errors",
488 example = "cargo-mini check --all-targets"
489)]
490struct CheckOpt {
491 #[arg(section = "Target", long = "all-targets")]
493 all_targets: bool,
494
495 #[arg(section = "Features", short = 'F', long = "features",
497 complete = available_features)]
498 features: Option<String>,
499
500 #[arg(section = "Target", long = "target", complete = known_targets)]
502 target: Option<String>,
503}
504
505#[derive(CliOptions)]
507#[cli(
508 name = "clean",
509 about = "Remove artifacts that cargo has generated in the past",
510 example = "cargo-mini clean --release"
511)]
512struct CleanOpt {
513 #[arg(section = "Target", long = "release")]
515 release: bool,
516
517 #[arg(section = "Target", long = "target", complete = known_targets)]
519 target: Option<String>,
520
521 #[arg(section = "Workspace", short = 'p', long = "package",
523 complete = workspace_members)]
524 package: Option<String>,
525}
526
527#[derive(CliOptions)]
529#[cli(
530 name = "metadata",
531 about = "Output the resolved dependencies of the current package as JSON"
532)]
533struct MetadataOpt {
534 #[arg(
536 section = "Output",
537 long = "format-version",
538 default = "1",
539 validate(one_of("1"))
540 )]
541 format_version: u32,
542
543 #[arg(section = "Output", long = "no-deps")]
545 no_deps: bool,
546}
547
548#[derive(CliOptions)]
550#[cli(
551 name = "update",
552 about = "Update dependencies as recorded in the lock file",
553 example = "cargo-mini update serde"
554)]
555struct UpdateOpt {
556 #[arg(positional)]
558 crate_name: Option<String>,
559
560 #[arg(section = "Misc", long = "dry-run")]
562 dry_run: bool,
563
564 #[arg(section = "Misc", long = "unstable-features")]
566 unstable: bool,
567}
568
569#[derive(CliOptions)]
571#[cli(
572 name = "info",
573 about = "Display information about a package in the registry",
574 example = "cargo-mini info serde"
575)]
576struct InfoOpt {
577 #[arg(positional)]
579 crate_name: String,
580
581 #[arg(section = "Filter", long = "version")]
583 version: Option<String>,
584}
585
586#[derive(CliOptions)]
593#[cli(
594 name = "import",
595 about = "Import a list of dependencies from a CSV manifest",
596 example = "cargo-mini import --file deps.csv"
597)]
598struct ImportOpt {
599 #[arg(
601 section = "Input",
602 long = "file",
603 validate(exists, is_file, ext("csv"))
604 )]
605 file: PathBuf,
606
607 #[arg(section = "Misc", long = "dry-run")]
609 dry_run: bool,
610}
611
612#[derive(CliCommand)]
615#[cli(
616 name = "cargo-mini",
617 about = "The Rust package manager (emulator)",
618 tagline = "no real side effects — safe to tab-complete",
619 theme = "yellow",
620 global = Global,
621 example = "cargo-mini build --release",
622 example = "cargo-mini run --example mini_cargo",
623 example = "cargo-mini add tokio --features full",
624)]
625enum Cmd {
626 Build(BuildOpt),
628 #[cli(alias = "r")]
630 Run(RunOpt),
631 Add(AddOpt),
633 #[cli(alias = "rm")]
635 Remove(RemoveOpt),
636 New(NewOpt),
638 Init(InitOpt),
640 #[cli(alias = "t")]
642 Test(TestOpt),
643 Check(CheckOpt),
645 Clean(CleanOpt),
647 Metadata(MetadataOpt),
649 Update(UpdateOpt),
651 Info(InfoOpt),
653 Import(ImportOpt),
655}
656
657fn main() {
660 let cmd = match Cmd::parse() {
661 Ok(c) => c,
662 Err(e) => bail!("{}", e),
663 };
664 let global = Cmd::global();
665
666 header!(
667 "cargo-mini",
668 env!("CARGO_PKG_VERSION"),
669 "The Rust package manager (emulator)",
670 "no real side effects — safe to tab-complete",
671 badge = cli_ui::styles::theme_styles("yellow").0
672 );
673
674 if global.verbose {
675 phase!("verbose", "verbose output enabled");
676 }
677
678 match cmd {
679 Cmd::Build(opt) => cmd_build(&opt, global),
680 Cmd::Run(opt) => cmd_run(&opt, global),
681 Cmd::Add(opt) => cmd_add(&opt, global),
682 Cmd::Remove(opt) => cmd_remove(&opt, global),
683 Cmd::New(opt) => cmd_new(&opt, global),
684 Cmd::Init(opt) => cmd_init(&opt, global),
685 Cmd::Test(opt) => cmd_test(&opt, global),
686 Cmd::Check(opt) => cmd_check(&opt, global),
687 Cmd::Clean(opt) => cmd_clean(&opt, global),
688 Cmd::Metadata(opt) => cmd_metadata(&opt, global),
689 Cmd::Update(opt) => cmd_update(&opt, global),
690 Cmd::Info(opt) => cmd_info(&opt, global),
691 Cmd::Import(opt) => cmd_import(&opt, global),
692 }
693}
694
695fn cmd_build(opt: &BuildOpt, global: &Global) {
698 let mode = if opt.release { "release" } else { "dev" };
699 phase!("compile", "Compiling packages in {} mode", mode);
700 if let Some(ref e) = opt.example {
701 phase!("target", "example = {e}");
702 }
703 if let Some(ref b) = opt.bin {
704 phase!("target", "bin = {b}");
705 }
706 if let Some(ref t) = opt.target {
707 phase!("target", "target = {t}");
708 }
709 if let Some(ref f) = opt.features {
710 phase!("features", "{f}");
711 }
712 fake_compile(opt.jobs);
713 ok!("target/debug/mini_cargo");
714 summary! {
715 done: "Build finished",
716 "mode" => paint(OK, mode),
717 "jobs" => paint(DIM, &opt.jobs.to_string()),
718 "verbose" => paint(DIM, &global.verbose.to_string()),
719 }
720}
721
722fn cmd_run(opt: &RunOpt, global: &Global) {
723 let mode = if opt.release { "release" } else { "dev" };
724 phase!("compile", "Compiling in {} mode", mode);
725 if let Some(ref e) = opt.example {
726 phase!("run", "example = {e}");
727 }
728 if let Some(ref b) = opt.bin {
729 phase!("run", "bin = {b}");
730 }
731 if let Some(ref f) = opt.features {
732 phase!("features", "{f}");
733 }
734 fake_compile(4);
735 phase!("running", "Running `target/{}/...`", mode);
736 summary! {
737 done: "Run complete",
738 "mode" => paint(OK, mode),
739 "quiet" => paint(DIM, &global.quiet.to_string()),
740 }
741}
742
743fn cmd_add(opt: &AddOpt, _global: &Global) {
744 let kind = if opt.dev {
745 "dev-dependency"
746 } else if opt.build {
747 "build-dependency"
748 } else {
749 "dependency"
750 };
751 phase!("resolve", "Resolving {} `{}`", kind, opt.crate_name);
752 if let Some(ref f) = opt.features {
753 phase!("features", "enabling: {f}");
754 }
755 if opt.dry_run {
756 phase!("dry-run", "would write to Cargo.toml (dry run)");
757 } else {
758 phase!("update", "Updating Cargo.toml");
759 phase!("lock", "Updating Cargo.lock");
760 }
761 summary! {
762 done: "Dependency added",
763 "crate" => paint(CYAN, &opt.crate_name),
764 "kind" => paint(DIM, kind),
765 }
766}
767
768fn cmd_remove(opt: &RemoveOpt, _global: &Global) {
769 phase!("remove", "Removing `{}` from Cargo.toml", opt.crate_name);
770 summary! {
771 done: "Dependency removed",
772 "crate" => paint(CYAN, &opt.crate_name),
773 }
774}
775
776fn cmd_new(opt: &NewOpt, _global: &Global) {
777 let kind = if opt.lib { "library" } else { "binary" };
778 let name = opt
779 .name
780 .as_deref()
781 .or_else(|| opt.path.file_name()?.to_str())
782 .unwrap_or("my-package");
783 phase!("create", "Creating {} package `{}`", kind, name);
784 phase!("edition", "edition = {}", opt.edition);
785 if let Some(ref v) = opt.vcs {
786 phase!("vcs", "initializing {} repo", v);
787 }
788 ok!(opt.path.display());
789 summary! {
790 done: "Package created",
791 "name" => paint(CYAN, name),
792 "kind" => paint(DIM, kind),
793 "edition" => paint(DIM, &opt.edition),
794 }
795}
796
797fn cmd_init(opt: &InitOpt, _global: &Global) {
798 let dir = opt
799 .path
800 .as_deref()
801 .unwrap_or_else(|| std::path::Path::new("."));
802 let kind = if opt.lib { "library" } else { "binary" };
803 phase!(
804 "init",
805 "Initializing {} package in `{}`",
806 kind,
807 dir.display()
808 );
809 if let Some(ref v) = opt.vcs {
810 phase!("vcs", "initializing {} repo", v);
811 }
812 summary! {
813 done: "Package initialized",
814 "dir" => paint(CYAN, &dir.display().to_string()),
815 "edition" => paint(DIM, &opt.edition),
816 }
817}
818
819fn cmd_test(opt: &TestOpt, _global: &Global) {
820 let mode = if opt.release { "release" } else { "dev" };
821 phase!("compile", "Compiling test targets in {} mode", mode);
822 if let Some(ref f) = opt.filter {
823 phase!("filter", "running tests matching `{f}`");
824 }
825 if let Some(ref f) = opt.features {
826 phase!("features", "{f}");
827 }
828 fake_compile(4);
829 step!("running {} test thread(s)", opt.test_threads);
830 eprintln!();
831 eprintln!(
832 " {} test result: {}. {} passed; 0 failed",
833 paint(DIM, "│"),
834 paint(OK, "ok"),
835 paint(OK, "42"),
836 );
837 eprintln!();
838 summary! {
839 done: "Tests passed",
840 "mode" => paint(OK, mode),
841 "threads" => paint(DIM, &opt.test_threads.to_string()),
842 "passed" => paint(OK, "42"),
843 "failed" => paint(DIM, "0"),
844 }
845}
846
847fn cmd_check(opt: &CheckOpt, _global: &Global) {
848 phase!("check", "Checking package");
849 if opt.all_targets {
850 phase!("targets", "checking all targets");
851 }
852 if let Some(ref f) = opt.features {
853 phase!("features", "{f}");
854 }
855 if let Some(ref t) = opt.target {
856 phase!("target", "{t}");
857 }
858 fake_compile(4);
859 summary! { done: "Check passed" }
860}
861
862fn cmd_clean(opt: &CleanOpt, _global: &Global) {
863 let mode = if opt.release { "release" } else { "debug" };
864 phase!("clean", "Removing target/{}", mode);
865 if let Some(ref p) = opt.package {
866 phase!("package", "{p}");
867 }
868 summary! {
869 done: "Clean complete",
870 "mode" => paint(DIM, mode),
871 }
872}
873
874fn cmd_metadata(opt: &MetadataOpt, _global: &Global) {
875 phase!("metadata", "Resolving dependency graph");
876 if opt.no_deps {
877 phase!("filter", "excluding transitive dependencies");
878 }
879 eprintln!();
880 eprintln!(
881 " {} {{\"packages\":[...],\"workspace_root\":\"{}\"}}",
882 paint(DIM, "│"),
883 paint(DIM, "."),
884 );
885 eprintln!();
886 summary! {
887 done: "Metadata output",
888 "version" => paint(DIM, &opt.format_version.to_string()),
889 }
890}
891
892fn cmd_update(opt: &UpdateOpt, _global: &Global) {
893 if let Some(ref c) = opt.crate_name {
894 phase!("update", "Updating `{}`", c);
895 } else {
896 phase!("update", "Updating all dependencies");
897 }
898 if opt.dry_run {
899 phase!("dry-run", "would update Cargo.lock (dry run)");
900 }
901 summary! { done: "Lock file updated" }
902}
903
904fn cmd_info(opt: &InfoOpt, _global: &Global) {
905 phase!("fetch", "Fetching info for `{}`", opt.crate_name);
906 if let Some(ref v) = opt.version {
907 phase!("version", "showing v{v}");
908 }
909 eprintln!();
910 eprintln!(
911 " {} {} v1.0.0",
912 paint(DIM, "│"),
913 paint(CYAN, &opt.crate_name)
914 );
915 eprintln!(
916 " {} {}",
917 paint(DIM, "│"),
918 paint(DIM, "A well-known Rust crate (emulated)")
919 );
920 eprintln!(
921 " {} https://crates.io/crates/{}",
922 paint(DIM, "│"),
923 opt.crate_name
924 );
925 eprintln!();
926 summary! {
927 done: "Info retrieved",
928 "crate" => paint(CYAN, &opt.crate_name),
929 }
930}
931
932fn cmd_import(opt: &ImportOpt, _global: &Global) {
933 phase!("read", "Reading CSV manifest `{}`", opt.file.display());
934 if opt.dry_run {
935 phase!("dry-run", "would update Cargo.toml (dry run)");
936 } else {
937 phase!("update", "Updating Cargo.toml");
938 }
939 summary! {
940 done: "Import complete",
941 "file" => paint(CYAN, &opt.file.display().to_string()),
942 }
943}
944
945fn fake_compile(jobs: usize) {
948 let pkgs = ["proc-macro2", "quote", "syn", "serde", "cli-ui"];
949 step!("compiling {} packages ({} jobs)", pkgs.len(), jobs);
950 eprintln!();
951 for pkg in &pkgs {
952 eprintln!(
953 " {} {} {}",
954 paint(DIM, "│"),
955 paint(DIM, "Compiling"),
956 paint(CYAN, pkg),
957 );
958 }
959 eprintln!();
960}