1use crate::{MAX_PROGRAM_SIZE, *};
18
19use leo_errors::Result;
20use leo_span::Symbol;
21
22use snarkvm::prelude::{Program as SvmProgram, TestnetV0};
23
24use indexmap::{IndexMap, IndexSet};
25use std::path::Path;
26
27fn find_cached_edition(cache_directory: &Path, name: &str) -> Option<u16> {
30 let program_cache = cache_directory.join(name);
31 if !program_cache.exists() {
32 return None;
33 }
34
35 std::fs::read_dir(&program_cache)
37 .ok()?
38 .filter_map(|entry| entry.ok())
39 .filter_map(|entry| {
40 let file_name = entry.file_name();
41 let name = file_name.to_str()?;
42 name.parse::<u16>().ok()
43 })
44 .max()
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum PackageKind {
50 Program,
52 Library,
54 Test,
56}
57
58impl PackageKind {
59 pub fn is_program(&self) -> bool {
60 matches!(self, Self::Program)
61 }
62
63 pub fn is_library(&self) -> bool {
64 matches!(self, Self::Library)
65 }
66
67 pub fn is_test(&self) -> bool {
68 matches!(self, Self::Test)
69 }
70}
71
72#[derive(Clone, Debug)]
74pub struct CompilationUnit {
75 pub name: Symbol,
79 pub data: ProgramData,
80 pub edition: Option<u16>,
81 pub dependencies: IndexSet<Dependency>,
82 pub is_local: bool,
83 pub kind: PackageKind,
84}
85
86impl CompilationUnit {
87 pub fn from_aleo_path<P: AsRef<Path>>(name: Symbol, path: P, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
90 Self::from_aleo_path_impl(name, path.as_ref(), map)
91 }
92
93 fn from_aleo_path_impl(name: Symbol, path: &Path, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
94 let bytecode = std::fs::read_to_string(path).map_err(|e| {
95 crate::errors::util_file_io_error(format_args!("Trying to read aleo file at {}", path.display()), e)
96 })?;
97
98 let dependencies = parse_dependencies_from_aleo(name, &bytecode, map)?;
99
100 Ok(CompilationUnit {
101 name,
102 data: ProgramData::Bytecode(bytecode),
103 edition: None,
104 dependencies,
105 is_local: true,
106 kind: PackageKind::Program,
107 })
108 }
109
110 pub fn from_package_path<P: AsRef<Path>>(name: Symbol, path: P) -> Result<Self> {
113 Self::from_package_path_impl(name, path.as_ref())
114 }
115
116 fn from_package_path_impl(name: Symbol, path: &Path) -> Result<Self> {
117 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
118 let manifest_symbol = crate::symbol(&manifest.program)?;
119 if name != manifest_symbol {
120 return Err(
121 crate::errors::conflicting_manifest(format_args!("{name}"), format_args!("{manifest_symbol}")).into()
122 );
123 }
124 let source_directory = path.join(SOURCE_DIRECTORY);
125 source_directory.read_dir().map_err(|e| {
126 crate::errors::util_file_io_error(
127 format_args!("Failed to read directory {}", source_directory.display()),
128 e,
129 )
130 })?;
131
132 let main_path = source_directory.join(MAIN_FILENAME);
133 let lib_path = source_directory.join(LIB_FILENAME);
134
135 let (source_path, kind) = match (main_path.exists(), lib_path.exists()) {
136 (true, true) => {
137 return Err(crate::errors::ambiguous_entry_file(
138 source_directory.display(),
139 MAIN_FILENAME,
140 LIB_FILENAME,
141 )
142 .into());
143 }
144 (true, false) => (main_path, PackageKind::Program),
145 (false, true) => (lib_path, PackageKind::Library),
146 (false, false) => {
147 return Err(
148 crate::errors::invalid_entry_file(source_directory.display(), MAIN_FILENAME, LIB_FILENAME).into()
149 );
150 }
151 };
152
153 Ok(CompilationUnit {
154 name,
155 data: ProgramData::SourcePath { directory: path.to_path_buf(), source: source_path },
156 edition: None,
157 dependencies: manifest
158 .dependencies
159 .unwrap_or_default()
160 .into_iter()
161 .map(|dependency| {
162 let dep = canonicalize_dependency_path_relative_to(path, dependency)?;
163 if dep.location == Location::Workspace { resolve_workspace_dependency(path, dep) } else { Ok(dep) }
164 })
165 .collect::<Result<IndexSet<_>, _>>()?,
166 is_local: true,
167 kind,
168 })
169 }
170
171 pub fn from_test_path<P: AsRef<Path>>(source_path: P, main_program: Dependency) -> Result<Self> {
178 Self::from_path_test_impl(source_path.as_ref(), main_program)
179 }
180
181 fn from_path_test_impl(source_path: &Path, main_program: Dependency) -> Result<Self> {
182 let name = filename_no_leo_extension(source_path)
183 .ok_or_else(|| crate::errors::failed_path(source_path.display(), ""))?;
184 let test_directory = source_path.parent().ok_or_else(|| {
185 crate::errors::failed_to_open_file(format_args!(
186 "Failed to find directory for test {}",
187 source_path.display()
188 ))
189 })?;
190 let package_directory = test_directory.parent().ok_or_else(|| {
191 crate::errors::failed_to_open_file(format_args!(
192 "Failed to find package for test {}",
193 source_path.display()
194 ))
195 })?;
196 let manifest = Manifest::read_from_file(package_directory.join(MANIFEST_FILENAME))?;
197 let mut dependencies = manifest
200 .dependencies
201 .into_iter()
202 .flatten()
203 .chain(manifest.dev_dependencies.into_iter().flatten())
204 .map(|dependency| {
205 let dep = canonicalize_dependency_path_relative_to(package_directory, dependency)?;
206 if dep.location == Location::Workspace {
207 resolve_workspace_dependency(package_directory, dep)
208 } else {
209 Ok(dep)
210 }
211 })
212 .collect::<Result<IndexSet<_>, _>>()?;
213 dependencies.insert(main_program);
214
215 Ok(CompilationUnit {
216 name: Symbol::intern(&(name.to_owned() + ".aleo")),
217 edition: None,
218 data: ProgramData::SourcePath {
219 directory: test_directory.to_path_buf(),
220 source: source_path.to_path_buf(),
221 },
222 dependencies,
223 is_local: true,
224 kind: PackageKind::Test,
225 })
226 }
227
228 pub fn fetch<P: AsRef<Path>>(
231 name: Symbol,
232 edition: Option<u16>,
233 home_path: P,
234 network: NetworkName,
235 endpoint: &str,
236 no_cache: bool,
237 network_retries: u32,
238 ) -> Result<Self> {
239 Self::fetch_impl(name, edition, home_path.as_ref(), network, endpoint, no_cache, network_retries)
240 }
241
242 fn fetch_impl(
243 name: Symbol,
244 edition: Option<u16>,
245 home_path: &Path,
246 network: NetworkName,
247 endpoint: &str,
248 no_cache: bool,
249 network_retries: u32,
250 ) -> Result<Self> {
251 let name = Symbol::intern(name.to_string().strip_suffix(".aleo").unwrap_or(&name.to_string()));
254
255 let cache_directory = home_path.join(format!("registry/{network}"));
257
258 let edition = match edition {
261 _ if name == Symbol::intern("credits") => 0,
263 Some(edition) => edition,
264 None if !no_cache => {
265 match find_cached_edition(&cache_directory, &name.to_string()) {
267 Some(cached_edition) => cached_edition,
268 None => crate::fetch_latest_edition(&name.to_string(), endpoint, network, network_retries)?,
269 }
270 }
271 None => crate::fetch_latest_edition(&name.to_string(), endpoint, network, network_retries)?,
273 };
274
275 let cache_directory = cache_directory.join(format!("{name}/{edition}"));
279 let full_cache_path = cache_directory.join(format!("{name}.aleo"));
280 if !cache_directory.exists() {
281 std::fs::create_dir_all(&cache_directory).map_err(|err| {
283 crate::errors::util_file_io_error(format!("Could not write path {}", cache_directory.display()), err)
284 })?;
285 }
286
287 let existing_bytecode = match full_cache_path.exists() {
289 false => None,
290 true => {
291 let existing_contents = std::fs::read_to_string(&full_cache_path).map_err(|e| {
292 crate::errors::util_file_io_error(
293 format_args!("Trying to read cached file at {}", full_cache_path.display()),
294 e,
295 )
296 })?;
297 Some(existing_contents)
298 }
299 };
300
301 let bytecode = match (existing_bytecode, no_cache) {
302 (Some(bytecode), false) => bytecode,
304 (existing, _) => {
306 let primary_url = if name == Symbol::intern("credits") {
308 format!("{endpoint}/{network}/program/credits.aleo")
309 } else {
310 format!("{endpoint}/{network}/program/{name}.aleo/{edition}")
311 };
312 let secondary_url = format!("{endpoint}/{network}/program/{name}.aleo");
313 let contents = fetch_from_network(&primary_url, network_retries)
314 .or_else(|_| fetch_from_network(&secondary_url, network_retries))
315 .map_err(|err| {
316 crate::errors::failed_to_retrieve_from_endpoint(
317 primary_url,
318 format_args!("Failed to fetch program `{name}` from network `{network}`: {err}"),
319 )
320 })?;
321
322 if let Some(existing_contents) = existing
324 && existing_contents != contents
325 {
326 println!(
327 "Warning: The cached file at `{}` is different from the one fetched from the network. The cached file will be overwritten.",
328 full_cache_path.display()
329 );
330 }
331
332 std::fs::write(&full_cache_path, &contents).map_err(|err| {
334 crate::errors::util_file_io_error(
335 format_args!("Could not open file `{}`", full_cache_path.display()),
336 err,
337 )
338 })?;
339
340 contents
341 }
342 };
343
344 let dependencies = parse_dependencies_from_aleo(name, &bytecode, &IndexMap::new())?;
345
346 Ok(CompilationUnit {
347 name: Symbol::intern(&(name.to_string() + ".aleo")),
350 data: ProgramData::Bytecode(bytecode),
351 edition: Some(edition),
352 dependencies,
353 is_local: false,
354 kind: PackageKind::Program,
355 })
356 }
357
358 pub fn from_git(
360 name: Symbol,
361 dependency: &Dependency,
362 home_path: &Path,
363 old_lock: &Lock,
364 new_lock: &mut Lock,
365 offline: bool,
366 declared_deps: &IndexMap<Symbol, Dependency>,
367 ) -> Result<Self> {
368 let git = dependency
369 .git
370 .as_ref()
371 .ok_or_else(|| crate::errors::invalid_manifest_dependency(&dependency.name, "missing `git` URL"))?;
372 let url = git.url.as_str();
373 let reference =
374 git.reference().map_err(|reason| crate::errors::invalid_manifest_dependency(&dependency.name, reason))?;
375 let reference_str = reference.lock_string();
376
377 let memoized = new_lock
380 .commit_for_source(url, &reference_str)
381 .map(|commit| (crate::git::checkout_dir(home_path, url, commit), commit.to_string()))
382 .filter(|(dir, _)| dir.is_dir());
383 let (checkout, commit) = match memoized {
384 Some(hit) => hit,
385 None => {
386 let locked = old_lock.commit_for(&dependency.name, url, &reference_str).map(str::to_string);
387 crate::git::resolve(home_path, &dependency.name, url, &reference, locked.as_deref(), offline)?
388 }
389 };
390 new_lock.record(dependency.name.clone(), url.to_string(), reference_str, commit);
391
392 let located = find_in_checkout(&checkout, &dependency.name)?;
393 let mut unit = if located.extension().and_then(|e| e.to_str()) == Some("aleo") && located.is_file() {
394 Self::from_aleo_path(name, located, declared_deps)?
395 } else {
396 Self::from_package_path(name, located)?
397 };
398
399 unit.dependencies = unit
402 .dependencies
403 .into_iter()
404 .map(|dep| {
405 if dep.location == Location::Local && dep.path.as_ref().is_some_and(|p| p.starts_with(&checkout)) {
406 Dependency {
407 name: dep.name,
408 location: Location::Git,
409 path: None,
410 edition: None,
411 git: Some(git.clone()),
412 }
413 } else {
414 dep
415 }
416 })
417 .collect();
418 Ok(unit)
419 }
420}
421
422pub fn find_in_checkout(checkout: &Path, dep_name: &str) -> Result<std::path::PathBuf> {
427 let bare = crate::bare_unit_name(dep_name);
429 let with_aleo = format!("{bare}.aleo");
430 let (requested, alternate) =
431 if dep_name.ends_with(".aleo") { (with_aleo.as_str(), bare) } else { (bare, with_aleo.as_str()) };
432
433 let mut exact = Vec::new();
434 let mut alt = Vec::new();
435 collect_matching_manifest_dirs(checkout, requested, alternate, 0, &mut exact, &mut alt);
436 let matches = if exact.is_empty() { alt } else { exact };
437 if matches.len() > 1 {
438 let listed = matches
439 .iter()
440 .map(|d| d.strip_prefix(checkout).unwrap_or(d).display().to_string())
441 .collect::<Vec<_>>()
442 .join("`, `");
443 return Err(crate::errors::git_ambiguous_package(dep_name, listed).into());
444 }
445 if let Some(dir) = matches.into_iter().next() {
446 return Ok(dir);
447 }
448 let aleo = checkout.join(&with_aleo);
449 if aleo.is_file() {
450 return Ok(aleo);
451 }
452 Err(crate::errors::git_package_not_found(dep_name, checkout.display()).into())
453}
454
455fn collect_matching_manifest_dirs(
458 dir: &Path,
459 requested: &str,
460 alternate: &str,
461 depth: usize,
462 exact: &mut Vec<std::path::PathBuf>,
463 alt: &mut Vec<std::path::PathBuf>,
464) {
465 const MAX_DEPTH: usize = 6;
467 if depth > MAX_DEPTH {
468 return;
469 }
470
471 #[derive(serde::Deserialize)]
473 struct ManifestProgramOnly {
474 program: String,
475 }
476
477 let manifest = dir.join(MANIFEST_FILENAME);
478 if manifest.is_file()
479 && !manifest.is_symlink()
480 && let Ok(contents) = std::fs::read_to_string(&manifest)
481 && let Ok(parsed) = serde_json::from_str::<ManifestProgramOnly>(&contents)
482 {
483 if parsed.program == requested {
484 exact.push(dir.to_path_buf());
485 } else if parsed.program == alternate {
486 alt.push(dir.to_path_buf());
487 }
488 }
489
490 let mut entries: Vec<_> = std::fs::read_dir(dir).ok().into_iter().flatten().flatten().map(|e| e.path()).collect();
492 entries.sort();
493 for path in entries {
494 if path.is_symlink() || !path.is_dir() {
496 continue;
497 }
498 let Some(name) = path.file_name().map(|n| n.to_string_lossy()) else {
500 continue;
501 };
502 if name.starts_with('.') || name == BUILD_DIRECTORY || name == "target" {
503 continue;
504 }
505 collect_matching_manifest_dirs(&path, requested, alternate, depth + 1, exact, alt);
506 }
507}
508
509pub(crate) fn canonicalize_dependency_path_relative_to(base: &Path, mut dependency: Dependency) -> Result<Dependency> {
512 if let Some(path) = &mut dependency.path {
513 let joined = if path.is_absolute() { path.clone() } else { base.join(&path) };
514 *path = joined.canonicalize().map_err(|e| crate::errors::failed_path(joined.display(), e))?;
515 }
516 Ok(dependency)
517}
518
519fn parse_dependencies_from_aleo(
521 name: Symbol,
522 bytecode: &str,
523 existing: &IndexMap<Symbol, Dependency>,
524) -> Result<IndexSet<Dependency>> {
525 let program_size = bytecode.len();
527
528 if program_size > MAX_PROGRAM_SIZE {
529 return Err(leo_errors::LeoError::Backtraced(crate::errors::program_size_limit_exceeded(
530 name,
531 program_size,
532 MAX_PROGRAM_SIZE,
533 )));
534 }
535
536 let svm_program: SvmProgram<TestnetV0> =
538 bytecode.parse().map_err(|_| crate::errors::snarkvm_parsing_error(name))?;
539 let dependencies = svm_program
540 .imports()
541 .keys()
542 .map(|program_id| {
543 if let Some(dependency) = existing.get(&Symbol::intern(&program_id.to_string())) {
546 dependency.clone()
547 } else {
548 let name = program_id.to_string();
549 Dependency { name, location: Location::Network, path: None, edition: None, ..Default::default() }
550 }
551 })
552 .collect();
553 Ok(dependencies)
554}