1use super::*;
18use leo_ast::NetworkName;
19use leo_package::{CompilationUnit, Dependency, GitReference, GitSource, Location, Lock, Manifest, Workspace};
20use leo_span::Symbol;
21use std::path::{Path, PathBuf};
22
23#[derive(Parser, Debug)]
25#[clap(name = "leo", author = "The Leo Team <leo@provable.com>", version)]
26pub struct LeoAdd {
27 #[clap(name = "NAME", help = "The dependency name. Ex: `credits.aleo` or `credits`.")]
28 pub(crate) name: String,
29
30 #[clap(flatten)]
31 pub(crate) source: DependencySource,
32
33 #[clap(flatten)]
34 pub(crate) git_ref: GitRef,
35
36 #[clap(
37 long,
38 help = "Endpoint used to verify a network dependency exists. Overrides the `ENDPOINT` environment variable."
39 )]
40 pub(crate) endpoint: Option<String>,
41
42 #[clap(
43 long,
44 env = "NETWORK_RETRIES",
45 help = "Number of times to retry a failed network request when verifying a network dependency.",
46 default_value = "2"
47 )]
48 pub(crate) network_retries: u32,
49
50 #[clap(long, help = "This is a development dependency.", default_value = "false")]
51 pub(crate) dev: bool,
52}
53
54#[derive(Parser, Debug)]
55#[group(required = true, multiple = false)]
56pub struct DependencySource {
57 #[clap(
58 short = 'l',
59 long,
60 help = "Local path for the dependency (program or library, auto-detected).",
61 group = "source"
62 )]
63 pub(crate) local: Option<PathBuf>,
64
65 #[clap(short = 'n', long, help = "Whether the dependency is on a live network.", group = "source")]
66 pub(crate) network: bool,
67
68 #[clap(
69 short = 'e',
70 long,
71 help = "The expected edition of the program. DO NOT USE THIS UNLESS YOU KNOW WHAT YOU ARE DOING.",
72 group = "source"
73 )]
74 pub(crate) edition: Option<u16>,
75
76 #[clap(short = 'w', long, help = "Depend on another member of the enclosing workspace.", group = "source")]
77 pub(crate) workspace: bool,
78
79 #[clap(
80 short = 'g',
81 long,
82 help = "Git repository URL for the dependency (program or library, auto-detected).",
83 group = "source"
84 )]
85 pub(crate) git: Option<String>,
86}
87
88#[derive(Parser, Debug)]
90#[group(required = false, multiple = false)]
91pub struct GitRef {
92 #[clap(long, help = "Track a git branch (requires `--git`).", group = "gitref", requires = "git")]
93 pub(crate) branch: Option<String>,
94
95 #[clap(long, help = "Pin to a git tag (requires `--git`).", group = "gitref", requires = "git")]
96 pub(crate) tag: Option<String>,
97
98 #[clap(long, help = "Pin to a git revision (requires `--git`).", group = "gitref", requires = "git")]
99 pub(crate) rev: Option<String>,
100}
101
102fn normalize_program_name(raw: &str) -> Result<String> {
104 let name = if raw.ends_with(".aleo") { raw.to_string() } else { format!("{raw}.aleo") };
105 if !leo_package::is_valid_program_name(&name) {
106 return Err(crate::errors::invalid_package_name("program", name).into());
107 }
108 Ok(name)
109}
110
111fn classify_and_validate_name(
114 raw_name: &str,
115 current_is_library: bool,
116 located: &Path,
117 origin: impl std::fmt::Display,
118) -> Result<String> {
119 let dep_is_library = if located.extension().and_then(|e| e.to_str()) == Some("aleo") && located.is_file() {
120 false
121 } else {
122 let dep_manifest_path = located.join(leo_package::MANIFEST_FILENAME);
123 let dep_manifest = Manifest::read_from_file(&dep_manifest_path).map_err(|_| {
124 crate::errors::custom(format!(
125 "Could not read `{}` — is `{origin}` a valid Leo package?",
126 dep_manifest_path.display()
127 ))
128 })?;
129 !dep_manifest.program.ends_with(".aleo")
130 };
131
132 if current_is_library && !dep_is_library {
133 return Err(crate::errors::custom("A library package can only depend on other libraries.").into());
134 }
135
136 if !dep_is_library {
137 return normalize_program_name(raw_name);
138 }
139
140 if raw_name.ends_with(".aleo") {
141 return Err(crate::errors::custom(format!(
142 "`{raw_name}` ends with `.aleo` but the package at `{origin}` is a library, not a program.",
143 ))
144 .into());
145 }
146 if !leo_package::is_valid_library_name(raw_name) {
147 return Err(crate::errors::invalid_package_name("library", raw_name).into());
148 }
149 let lib_leo = located.join("src").join(leo_package::LIB_FILENAME);
151 if !lib_leo.exists() {
152 return Err(crate::errors::custom(format!(
153 "The package at `{origin}` has a library manifest but is missing `src/{}`.",
154 leo_package::LIB_FILENAME,
155 ))
156 .into());
157 }
158 Ok(raw_name.to_string())
159}
160
161impl Command for LeoAdd {
162 type Input = ();
163 type Output = ();
164
165 fn log_span(&self) -> Span {
166 tracing::span!(tracing::Level::INFO, "Leo")
167 }
168
169 fn prelude(&self, _: Context) -> Result<Self::Input> {
170 Ok(())
171 }
172
173 fn apply(self, context: Context, _: Self::Input) -> Result<Self::Output> {
174 let path = context.dir()?;
175
176 let manifest_path = path.join(leo_package::MANIFEST_FILENAME);
177 let mut manifest = Manifest::read_from_file(&manifest_path)?;
178
179 let current_is_library = !manifest.program.ends_with(".aleo");
180
181 let (name, location, dep_path) = if let Some(local_path) = &self.source.local {
183 let name = classify_and_validate_name(&self.name, current_is_library, local_path, local_path.display())?;
184 (name, Location::Local, Some(local_path.clone()))
185 } else if self.source.workspace {
186 let ws = leo_package::Workspace::discover(&path)?.ok_or_else(|| {
188 crate::errors::custom(
189 "Cannot add a workspace dependency: no `workspace.json` found in any parent directory.",
190 )
191 })?;
192 let name = normalize_program_name(&self.name)?;
193 if ws.find_member(&name).is_none() {
194 return Err(crate::errors::custom(format!(
195 "No workspace member named `{name}` found. Check the `members` list in `workspace.json`.",
196 ))
197 .into());
198 }
199 (name, Location::Workspace, None)
200 } else if let Some(url) = &self.source.git {
201 if !leo_package::is_valid_package_name(leo_package::bare_unit_name(&self.name)) {
203 return Err(crate::errors::custom(format!("`{}` is not a valid dependency name.", self.name)).into());
204 }
205
206 let reference = match GitReference::from_opts(&self.git_ref.branch, &self.git_ref.tag, &self.git_ref.rev) {
208 Ok(reference) => reference,
209 Err(reason) => return Err(crate::errors::custom(reason).into()),
210 };
211
212 let home = context.home()?;
213 let (checkout, commit) = leo_package::git::resolve(&home, &self.name, url, &reference, None, false)?;
214
215 let located = leo_package::find_in_checkout(&checkout, &self.name).map_err(|_| {
216 crate::errors::custom(format!(
217 "Could not find a Leo package named `{}` in `{url}`. Check the name and the git reference.",
218 self.name
219 ))
220 })?;
221 let name = classify_and_validate_name(&self.name, current_is_library, &located, url)?;
222
223 let lock_dir = Workspace::discover_root(&path)?.unwrap_or_else(|| path.clone());
225 let mut lock = Lock::read(&lock_dir);
226 lock.record(name.clone(), url.clone(), reference.lock_string(), commit);
227 lock.write(&lock_dir)?;
228
229 (name, Location::Git, None)
230 } else {
231 if current_is_library {
233 return Err(crate::errors::custom(
234 "A library package can only depend on other libraries. Use `--local <path>` to add a library dependency.",
235 )
236 .into());
237 }
238 let name = normalize_program_name(&self.name)?;
239
240 let network = get_network(&None).unwrap_or_else(|_| {
244 tracing::warn!("⚠️ No network specified, defaulting to 'testnet'.");
245 NetworkName::TestnetV0
246 });
247 let endpoint = get_endpoint(&self.endpoint).unwrap_or_else(|_| {
248 tracing::warn!("⚠️ No endpoint specified, defaulting to '{DEFAULT_ENDPOINT}'.");
249 DEFAULT_ENDPOINT.to_string()
250 });
251 let home = context.home()?;
252 CompilationUnit::fetch(
253 Symbol::intern(&name),
254 self.source.edition,
255 &home,
256 network,
257 &endpoint,
258 false,
259 self.network_retries,
260 )
261 .map_err(|err| {
262 crate::errors::custom(format!(
263 "Could not find program `{name}` on network `{network}` at `{endpoint}`: {err}",
264 ))
265 })?;
266
267 (name, Location::Network, None)
268 };
269
270 let new_dependency = Dependency {
271 name: name.clone(),
272 location,
273 path: dep_path.clone(),
274 edition: self.source.edition,
275 git: self.source.git.as_ref().map(|url| GitSource {
276 url: url.clone(),
277 branch: self.git_ref.branch.clone(),
278 tag: self.git_ref.tag.clone(),
279 rev: self.git_ref.rev.clone(),
280 }),
281 };
282
283 let deps = if self.dev { &mut manifest.dev_dependencies } else { &mut manifest.dependencies };
285
286 if let Some(existing) = deps.get_or_insert_default().iter_mut().find(|dep| dep.name == new_dependency.name) {
287 match existing.location {
288 Location::Local => tracing::warn!(
289 "⚠️ Dependency `{name}` already exists as a local dependency at `{}`. Overwriting.",
290 existing.path.as_ref().map(|p| p.display().to_string()).unwrap_or_default()
291 ),
292 Location::Workspace => {
293 tracing::warn!("⚠️ Dependency `{name}` already exists as a workspace dependency. Overwriting.")
294 }
295 Location::Git => {
296 tracing::warn!("⚠️ Dependency `{name}` already exists as a git dependency. Overwriting.")
297 }
298 _ => tracing::warn!("⚠️ Dependency `{name}` already exists as a network dependency. Overwriting."),
299 }
300 *existing = new_dependency;
301 } else {
302 deps.as_mut().unwrap().push(new_dependency);
303
304 match location {
305 Location::Local => tracing::info!(
306 "✅ Added local dependency `{name}` at path `{}`.",
307 dep_path.as_ref().map(|p| p.display().to_string()).unwrap_or_default()
308 ),
309 Location::Workspace => tracing::info!("✅ Added workspace dependency `{name}`."),
310 Location::Git => tracing::info!("✅ Added git dependency `{name}`."),
311 _ => tracing::info!("✅ Added network dependency `{name}`."),
312 }
313 }
314
315 manifest.write_to_file(manifest_path)?;
316
317 Ok(())
318 }
319}