1#![doc = include_str!("../README.md")]
2
3use std::{process::Command, sync::OnceLock};
4
5#[cfg(feature = "task_venv")]
6pub mod venv;
7
8mod install;
9
10
11pub use cicero_core::Result;
12
13#[derive(Debug)]
15pub struct Crate {
16 name: &'static str,
17 install_args: &'static [&'static str],
18}
19impl Crate {
20 pub const fn new(name: &'static str) -> Self {
22 Self {
23 name,
24 install_args: &[],
25 }
26 }
27
28 pub const fn with_install_args(mut self, install_args: &'static [&'static str]) -> Self {
30 self.install_args = install_args;
31 self
32 }
33
34 pub const fn into_cli(self) -> Cli {
36 Cli::new(CliInstallKind::Crate(self))
37 }
38}
39
40pub struct ExpectProgram {
42 name: &'static str,
43 install_instruction: Option<&'static str>,
44}
45impl ExpectProgram {
46 pub const fn new(name: &'static str) -> Self {
48 Self {
49 name,
50 install_instruction: None,
51 }
52 }
53
54 pub const fn with_install_instruction(mut self, install_instruction: &'static str) -> Self {
56 self.install_instruction = Some(install_instruction);
57 self
58 }
59
60 pub const fn into_cli(self) -> Cli {
62 Cli::new(CliInstallKind::ExpectProgram(self))
63 }
64}
65
66
67pub struct Cli {
69 install_kind: CliInstallKind,
70 program_name: OnceLock<ProgramName>,
71 command_fn: Option<CommandFn>,
72 crate_dependencies: &'static [Crate],
73 expected_dependencies: &'static [ExpectProgram],
74}
75enum CliInstallKind {
76 Crate(Crate),
77 ExpectProgram(ExpectProgram),
78}
79
80impl Cli {
81
82 pub const fn with_base_command(mut self, command_fn: CommandFn) -> Self {
104 self.command_fn = Some(command_fn);
105 self
106 }
107
108 pub const fn with_crate_dependencies(mut self, dependency_crates: &'static [Crate]) -> Self {
120 self.crate_dependencies = dependency_crates;
121 self
122 }
123
124 pub const fn with_expected_dependencies(mut self, expected_dependencies: &'static [ExpectProgram]) -> Self {
136 self.expected_dependencies = expected_dependencies;
137 self
138 }
139
140 pub fn command(&self) -> Command {
144
145 let program_name = self.program_name.get_or_init(|| {
146
147 for expected_dependency in self.expected_dependencies {
148 install::expect_command_installed(expected_dependency.name, expected_dependency.install_instruction);
149 }
150
151 for crate_dependency in self.crate_dependencies {
152 install::dependency_crate(crate_dependency)
153 .unwrap_or_else(|cause| panic!("Failed to install crate dependency {}: {cause}", crate_dependency.name));
154 }
155
156 match self.install_kind {
157 CliInstallKind::Crate(ref crate_to_install) => {
158 install::cli_crate(crate_to_install)
159 .unwrap_or_else(|cause| panic!("Failed to install CLI crate {}: {cause}", crate_to_install.name))
160 }
161 CliInstallKind::ExpectProgram(ref expect) => {
162 install::expect_command_installed(expect.name, expect.install_instruction);
163 ProgramName { value: expect.name.to_owned() }
164 }
165 }
166 });
167
168 let mut default_command = Command::new(&program_name.value);
169
170 if let CliInstallKind::Crate(_) = self.install_kind {
171 default_command
172 .env("PATH", crate::install::calculate_PATH());
173 }
174
175 match self.command_fn {
176 Some(command) => command(default_command),
177 None => default_command,
178 }
179 }
180
181 const fn new(install_kind: CliInstallKind) -> Self {
182 Self {
183 install_kind,
184 program_name: OnceLock::new(),
185 command_fn: None,
186 crate_dependencies: &[],
187 expected_dependencies: &[],
188 }
189 }
190}
191
192struct ProgramName { pub value: String }
193
194type CommandFn = &'static (dyn Fn(Command) -> Command + Send + Sync);
195
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn should_expect_installed() {
203 ExpectProgram::new("cargo")
204 .into_cli()
205 .command();
206 }
207
208 #[test]
209 #[should_panic]
210 fn should_abort_when_expected_command_not_installed() {
211 ExpectProgram::new("non-existent command")
212 .into_cli()
213 .command();
214 }
215}