1use crate::*;
18
19use leo_ast::DiGraph;
20use leo_errors::Result;
21use leo_span::Symbol;
22
23use indexmap::{IndexMap, map::Entry};
24use snarkvm::prelude::{Program as SvmProgram, TestnetV0, anyhow};
25use std::path::{Path, PathBuf};
26
27#[derive(Clone, Debug)]
30pub enum ProgramData {
31 Bytecode(String),
32 SourcePath {
35 directory: PathBuf,
36 source: PathBuf,
37 },
38}
39
40#[derive(Clone, Debug)]
42pub struct Package {
43 pub base_directory: PathBuf,
45
46 pub workspace_root: Option<PathBuf>,
54
55 pub compilation_units: Vec<CompilationUnit>,
62
63 pub manifest: Manifest,
65
66 pub dep_graph: DiGraph<Symbol>,
68}
69
70impl Package {
71 pub fn build_directory(&self) -> PathBuf {
79 self.workspace_root.as_deref().unwrap_or(&self.base_directory).join(BUILD_DIRECTORY)
80 }
81
82 pub fn primary_unit(&self) -> Option<&CompilationUnit> {
85 let primary = bare_unit_name(&self.manifest.program);
86 self.compilation_units.iter().find(|u| !u.kind.is_test() && bare_unit_name(&u.name.to_string()) == primary)
87 }
88
89 pub fn unit_build_directory(&self, name: &str) -> PathBuf {
93 self.build_directory().join(bare_unit_name(name))
94 }
95
96 pub fn unit_bytecode_path(&self, name: &str) -> PathBuf {
99 let bare = bare_unit_name(name);
100 self.unit_build_directory(name).join(format!("{bare}.aleo"))
101 }
102
103 pub fn unit_abi_path(&self, name: &str) -> PathBuf {
105 self.unit_build_directory(name).join(ABI_FILENAME)
106 }
107
108 pub fn unit_interfaces_directory(&self, name: &str) -> PathBuf {
111 self.unit_build_directory(name).join(INTERFACES_DIRNAME)
112 }
113
114 pub fn source_directory(&self) -> PathBuf {
115 self.base_directory.join(SOURCE_DIRECTORY)
116 }
117
118 pub fn tests_directory(&self) -> PathBuf {
119 self.base_directory.join(TESTS_DIRECTORY)
120 }
121
122 pub fn initialize<P: AsRef<Path>>(package_name: &str, path: P, is_library: bool) -> Result<PathBuf> {
124 Self::initialize_impl(package_name, path.as_ref(), is_library)
125 }
126
127 fn initialize_impl(package_name: &str, path: &Path, is_library: bool) -> Result<PathBuf> {
128 let package_name = if is_library {
129 if !crate::is_valid_library_name(package_name) {
130 return Err(crate::errors::cli_invalid_package_name("library", package_name).into());
131 }
132
133 package_name.to_string()
134 } else {
135 let program_name =
136 if package_name.ends_with(".aleo") { package_name.to_string() } else { format!("{package_name}.aleo") };
137
138 if !crate::is_valid_program_name(&program_name) {
139 return Err(crate::errors::cli_invalid_package_name("program", &program_name).into());
140 }
141
142 program_name
143 };
144
145 let path = path.canonicalize().map_err(|e| crate::errors::failed_path(path.display(), e))?;
146 let full_path = path.join(package_name.strip_suffix(".aleo").unwrap_or(&package_name));
147
148 if full_path.exists() {
150 return Err(
151 crate::errors::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
152 );
153 }
154
155 std::fs::create_dir(&full_path)
157 .map_err(|e| crate::errors::failed_to_initialize_package(&package_name, &full_path, e))?;
158
159 std::env::set_current_dir(&full_path)
161 .map_err(|e| crate::errors::failed_to_initialize_package(&package_name, &full_path, e))?;
162
163 const GITIGNORE_TEMPLATE: &str = ".env\n*.avm\n*.prover\n*.verifier\nbuild/\n";
165 const GITIGNORE_FILENAME: &str = ".gitignore";
166
167 let gitignore_path = full_path.join(GITIGNORE_FILENAME);
168 std::fs::write(gitignore_path, GITIGNORE_TEMPLATE).map_err(crate::errors::io_error_gitignore_file)?;
169
170 let manifest = Manifest {
172 program: package_name.clone(),
173 version: "0.1.0".to_string(),
174 description: String::new(),
175 license: "MIT".to_string(),
176 leo: env!("CARGO_PKG_VERSION").to_string(),
177 dependencies: None,
178 dev_dependencies: None,
179 no_std: false,
180 };
181
182 let manifest_path = full_path.join(MANIFEST_FILENAME);
183 manifest.write_to_file(manifest_path)?;
184
185 let source_path = full_path.join(SOURCE_DIRECTORY);
187
188 std::fs::create_dir(&source_path)
189 .map_err(|e| crate::errors::failed_to_create_source_directory(source_path.display(), e))?;
190
191 let name_no_aleo = package_name.strip_suffix(".aleo").unwrap_or(&package_name);
192
193 if is_library {
194 let lib_path = source_path.join("lib.leo");
196
197 std::fs::write(&lib_path, lib_template(name_no_aleo)).map_err(|e| {
198 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", lib_path.display()), e)
199 })?;
200
201 let tests_path = full_path.join(TESTS_DIRECTORY);
203
204 std::fs::create_dir(&tests_path)
205 .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
206
207 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
208
209 std::fs::write(&test_file_path, lib_test_template(name_no_aleo)).map_err(|e| {
210 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
211 })?;
212 } else {
213 let main_path = source_path.join(MAIN_FILENAME);
215
216 std::fs::write(&main_path, main_template(name_no_aleo)).map_err(|e| {
217 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e)
218 })?;
219
220 let tests_path = full_path.join(TESTS_DIRECTORY);
222
223 std::fs::create_dir(&tests_path)
224 .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
225
226 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
227
228 std::fs::write(&test_file_path, test_template(name_no_aleo)).map_err(|e| {
229 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
230 })?;
231 }
232
233 Ok(full_path)
234 }
235
236 pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
240 path: P,
241 home_path: Q,
242 network: Option<NetworkName>,
243 endpoint: Option<&str>,
244 network_retries: u32,
245 ) -> Result<Self> {
246 Self::from_directory_impl(
247 path.as_ref(),
248 home_path.as_ref(),
249 false,
250 false,
251 false,
252 false,
253 false,
254 network,
255 endpoint,
256 network_retries,
257 )
258 }
259
260 #[allow(clippy::too_many_arguments)]
266 pub fn from_aleo_file<P: AsRef<Path>, Q: AsRef<Path>>(
267 path: P,
268 home_path: Q,
269 imports_directory: Option<&Path>,
270 no_cache: bool,
271 no_local: bool,
272 network: Option<NetworkName>,
273 endpoint: Option<&str>,
274 network_retries: u32,
275 ) -> Result<Self> {
276 Self::from_aleo_file_impl(
277 path.as_ref(),
278 home_path.as_ref(),
279 imports_directory,
280 no_cache,
281 no_local,
282 network,
283 endpoint,
284 network_retries,
285 )
286 }
287
288 #[allow(clippy::too_many_arguments)]
291 pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
292 path: P,
293 home_path: Q,
294 no_cache: bool,
295 no_local: bool,
296 offline: bool,
297 network: Option<NetworkName>,
298 endpoint: Option<&str>,
299 network_retries: u32,
300 ) -> Result<Self> {
301 Self::from_directory_impl(
302 path.as_ref(),
303 home_path.as_ref(),
304 true,
305 false,
306 no_cache,
307 no_local,
308 offline,
309 network,
310 endpoint,
311 network_retries,
312 )
313 }
314
315 #[allow(clippy::too_many_arguments)]
318 pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
319 path: P,
320 home_path: Q,
321 no_cache: bool,
322 no_local: bool,
323 offline: bool,
324 network: Option<NetworkName>,
325 endpoint: Option<&str>,
326 network_retries: u32,
327 ) -> Result<Self> {
328 Self::from_directory_impl(
329 path.as_ref(),
330 home_path.as_ref(),
331 true,
332 true,
333 no_cache,
334 no_local,
335 offline,
336 network,
337 endpoint,
338 network_retries,
339 )
340 }
341
342 pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
343 let path = self.tests_directory();
344 let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
347 data.into_iter()
348 }
349
350 fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
351 path.read_dir()
352 .ok()
353 .into_iter()
354 .flatten()
355 .flat_map(|maybe_filename| maybe_filename.ok())
356 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
357 .flat_map(move |entry| {
358 let path = entry.path();
359 if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
360 })
361 }
362
363 #[allow(clippy::too_many_arguments)]
364 fn from_aleo_file_impl(
365 path: &Path,
366 home_path: &Path,
367 imports_directory: Option<&Path>,
368 no_cache: bool,
369 no_local: bool,
370 network: Option<NetworkName>,
371 endpoint: Option<&str>,
372 network_retries: u32,
373 ) -> Result<Self> {
374 if path.extension().and_then(|extension| extension.to_str()) != Some("aleo") {
375 return Err(anyhow!("Expected an Aleo bytecode file with the `.aleo` extension: {}", path.display()).into());
376 }
377
378 let path = path.canonicalize().map_err(|error| crate::errors::failed_path(path.display(), error))?;
379 if !path.is_file() {
380 return Err(anyhow!("Expected an Aleo bytecode file: {}", path.display()).into());
381 }
382 let home_path =
383 home_path.canonicalize().map_err(|error| crate::errors::failed_path(home_path.display(), error))?;
384 let bytecode = std::fs::read_to_string(&path).map_err(|error| {
385 crate::errors::util_file_io_error(format_args!("Trying to read Aleo file at {}", path.display()), error)
386 })?;
387 let source_name = path.file_stem().and_then(|name| name.to_str()).unwrap_or("program");
388 let main_program: SvmProgram<TestnetV0> =
389 bytecode.parse().map_err(|_| crate::errors::snarkvm_parsing_error(source_name))?;
390 let program_name = main_program.id().to_string();
391 let program_symbol = symbol(&program_name)?;
392 let base_directory = path
393 .parent()
394 .ok_or_else(|| anyhow!("Aleo bytecode file has no parent directory: {}", path.display()))?
395 .to_path_buf();
396
397 let main_dependency = Dependency {
398 name: program_name.clone(),
399 location: Location::Local,
400 path: Some(path.clone()),
401 edition: None,
402 ..Default::default()
403 };
404 let imports_directory = if no_local {
405 None
406 } else {
407 imports_directory
408 .map(|imports_directory| -> Result<PathBuf> {
409 let imports_directory = imports_directory
410 .canonicalize()
411 .map_err(|error| crate::errors::failed_path(imports_directory.display(), error))?;
412 if !imports_directory.is_dir() {
413 return Err(
414 anyhow!("Expected an Aleo imports directory: {}", imports_directory.display()).into()
415 );
416 }
417 Ok(imports_directory)
418 })
419 .transpose()?
420 };
421 let declared_deps = IndexMap::from([(program_symbol, main_dependency.clone())]);
422
423 let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
424 let mut digraph = DiGraph::new(Default::default());
425 let old_lock = Lock::default();
426 let mut new_lock = Lock::default();
427 Self::graph_build(
428 &home_path,
429 network,
430 endpoint,
431 &main_dependency,
432 main_dependency.clone(),
433 &mut map,
434 &mut digraph,
435 no_cache,
436 false,
437 imports_directory.as_deref(),
438 network_retries,
439 &declared_deps,
440 &old_lock,
441 &mut new_lock,
442 false,
443 )?;
444
445 let compilation_units = digraph
446 .post_order()
447 .map_err(|_| crate::errors::circular_dependency_error())?
448 .into_iter()
449 .map(|name| {
450 map.swap_remove(&name)
451 .map(|(_, unit)| unit)
452 .ok_or_else(|| anyhow!("Dependency graph contains an unknown program `{name}`.").into())
453 })
454 .collect::<Result<Vec<_>>>()?;
455 let manifest = Manifest {
456 program: program_name,
457 version: "0.0.0".to_string(),
458 description: String::new(),
459 license: String::new(),
460 leo: env!("CARGO_PKG_VERSION").to_string(),
461 dependencies: None,
462 dev_dependencies: None,
463 no_std: false,
464 };
465
466 Ok(Package { base_directory, workspace_root: None, compilation_units, manifest, dep_graph: digraph })
467 }
468
469 #[allow(clippy::too_many_arguments)]
470 fn from_directory_impl(
471 path: &Path,
472 home_path: &Path,
473 build_graph: bool,
474 with_tests: bool,
475 no_cache: bool,
476 no_local: bool,
477 offline: bool,
478 network: Option<NetworkName>,
479 endpoint: Option<&str>,
480 network_retries: u32,
481 ) -> Result<Self> {
482 let map_err = |path: &Path, err| {
483 crate::errors::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
484 };
485
486 let path = path.canonicalize().map_err(|err| map_err(path, err))?;
487
488 let workspace_root = Workspace::discover_root(&path)?;
492
493 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
494
495 let (compilation_units, digraph) = if build_graph {
496 let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
497
498 let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
499
500 let mut digraph = DiGraph::<Symbol>::new(Default::default());
501
502 let declared_deps = collect_declared_deps(&path, &manifest, with_tests)?;
505
506 let lock_dir = workspace_root.as_deref().unwrap_or(&path).to_path_buf();
508 let old_lock = Lock::read(&lock_dir);
510 let mut new_lock = Lock::default();
511
512 let first_dependency = Dependency {
513 name: manifest.program.clone(),
514 location: Location::Local,
515 path: Some(path.clone()),
516 edition: None,
517 ..Default::default()
518 };
519
520 let test_dependencies: Vec<Dependency> = if with_tests {
521 let tests_directory = path.join(TESTS_DIRECTORY);
522 let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
523 .map(|path| Dependency {
524 name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
526 edition: None,
527 location: Location::Test,
528 path: Some(path.to_path_buf()),
529 ..Default::default()
530 })
531 .collect();
532 if let Some(deps) = manifest.dev_dependencies.as_ref() {
533 for dep in deps {
536 let dep = canonicalize_dependency_path_relative_to(&path, dep.clone())?;
537 let dep = if dep.location == Location::Workspace {
538 resolve_workspace_dependency(&path, dep)?
539 } else {
540 dep
541 };
542 test_dependencies.push(dep);
543 }
544 }
545 test_dependencies
546 } else {
547 Vec::new()
548 };
549
550 for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
551 Self::graph_build(
552 &home_path,
553 network,
554 endpoint,
555 &first_dependency,
556 dependency,
557 &mut map,
558 &mut digraph,
559 no_cache,
560 no_local,
561 None,
562 network_retries,
563 &declared_deps,
564 &old_lock,
565 &mut new_lock,
566 offline,
567 )?;
568 }
569
570 if workspace_root.is_some() {
573 new_lock.carry_over(&old_lock, |_| true);
574 } else {
575 let dev_git_names: Vec<&str> = if with_tests {
576 Vec::new()
577 } else {
578 manifest
579 .dev_dependencies
580 .iter()
581 .flatten()
582 .filter(|dep| dep.location == Location::Git)
583 .map(|dep| dep.name.as_str())
584 .collect()
585 };
586 new_lock.carry_over(&old_lock, |entry| dev_git_names.contains(&entry.name.as_str()));
587 }
588 new_lock.write(&lock_dir)?;
590
591 let ordered_dependency_symbols =
592 digraph.post_order().map_err(|_| crate::errors::circular_dependency_error())?;
593
594 (
595 ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect(),
596 digraph,
597 )
598 } else {
599 (Vec::new(), DiGraph::default())
600 };
601
602 Ok(Package { base_directory: path, workspace_root, compilation_units, manifest, dep_graph: digraph })
603 }
604
605 #[allow(clippy::too_many_arguments)]
606 fn graph_build(
607 home_path: &Path,
608 network: Option<NetworkName>,
609 endpoint: Option<&str>,
610 main_program: &Dependency,
611 new: Dependency,
612 map: &mut IndexMap<Symbol, (Dependency, CompilationUnit)>,
613 graph: &mut DiGraph<Symbol>,
614 no_cache: bool,
615 no_local: bool,
616 aleo_imports_directory: Option<&Path>,
617 network_retries: u32,
618 declared_deps: &IndexMap<Symbol, Dependency>,
619 old_lock: &Lock,
620 new_lock: &mut Lock,
621 offline: bool,
622 ) -> Result<()> {
623 let mut new = new;
624 if new.location == Location::Network
625 && let Some(imports_directory) = aleo_imports_directory
626 {
627 let path = aleo_import_path(imports_directory, &new.name);
628 if path.exists() {
629 if !path.is_file() {
630 return Err(anyhow!("Expected Aleo import `{}` to be a file: {}", new.name, path.display()).into());
631 }
632 let bytecode = std::fs::read_to_string(&path).map_err(|error| {
633 crate::errors::util_file_io_error(
634 format_args!("Trying to read Aleo file at {}", path.display()),
635 error,
636 )
637 })?;
638 let imported: SvmProgram<TestnetV0> =
639 bytecode.parse().map_err(|_| crate::errors::snarkvm_parsing_error(bare_unit_name(&new.name)))?;
640 if imported.id().to_string() != new.name {
641 return Err(anyhow!(
642 "Aleo import `{}` resolved to `{}`, but that file declares `{}`.",
643 new.name,
644 path.display(),
645 imported.id()
646 )
647 .into());
648 }
649 new.location = Location::Local;
650 new.path = Some(path);
651 new.edition = None;
652 }
653 }
654
655 let name_symbol = symbol(&new.name)?;
656
657 let unit = match map.entry(name_symbol) {
658 Entry::Occupied(occupied) => {
659 let existing_dep = &occupied.get().0;
662 assert_eq!(new.name, existing_dep.name);
663 if new.location != existing_dep.location
664 || new.path != existing_dep.path
665 || new.edition != existing_dep.edition
666 || new.git != existing_dep.git
667 {
668 return Err(crate::errors::conflicting_dependency(existing_dep, new).into());
669 }
670 return Ok(());
671 }
672 Entry::Vacant(vacant) => {
673 let unit = match (new.path.as_ref(), new.location) {
674 (Some(path), Location::Local) if !no_local => {
675 if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
677 CompilationUnit::from_aleo_path(name_symbol, path, declared_deps)?
678 } else {
679 CompilationUnit::from_package_path(name_symbol, path)?
680 }
681 }
682 (Some(path), Location::Test) => {
683 CompilationUnit::from_test_path(path, main_program.clone())?
686 }
687 (_, Location::Network) | (Some(_), Location::Local) => {
688 let Some(endpoint) = endpoint else {
690 return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
691 };
692 let Some(network) = network else {
693 return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
694 };
695 CompilationUnit::fetch(
696 name_symbol,
697 new.edition,
698 home_path,
699 network,
700 endpoint,
701 no_cache,
702 network_retries,
703 )?
704 }
705 (_, Location::Git) => CompilationUnit::from_git(
706 name_symbol,
707 &new,
708 home_path,
709 old_lock,
710 new_lock,
711 offline,
712 declared_deps,
713 )?,
714 (_, Location::Workspace) => {
715 return Err(anyhow!(
716 "Workspace dependency `{}` was not resolved before graph building. This is a compiler bug.",
717 new.name
718 )
719 .into());
720 }
721 _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
722 };
723
724 vacant.insert((new, unit.clone()));
725
726 unit
727 }
728 };
729
730 graph.add_node(name_symbol);
731
732 let checkouts_root = crate::git::checkouts_root(home_path);
735 if let ProgramData::SourcePath { directory, .. } = &unit.data
736 && directory.starts_with(&checkouts_root)
737 {
738 let checkout = directory
740 .strip_prefix(&checkouts_root)
741 .ok()
742 .and_then(|rel| {
743 let mut components = rel.components();
744 Some((components.next()?, components.next()?))
745 })
746 .map(|(key, commit)| checkouts_root.join(key).join(commit));
747 for dependency in unit.dependencies.iter() {
748 if let Some(path) = &dependency.path
749 && !checkout.as_ref().is_some_and(|checkout| path.starts_with(checkout))
750 {
751 return Err(crate::errors::invalid_manifest_dependency(
752 &dependency.name,
753 "a git dependency may only reference paths inside its own repository checkout",
754 )
755 .into());
756 }
757 }
758 }
759
760 for dependency in unit.dependencies.iter() {
761 let dependency_symbol = symbol(&dependency.name)?;
762 graph.add_edge(name_symbol, dependency_symbol);
763 Self::graph_build(
764 home_path,
765 network,
766 endpoint,
767 main_program,
768 dependency.clone(),
769 map,
770 graph,
771 no_cache,
772 no_local,
773 aleo_imports_directory,
774 network_retries,
775 declared_deps,
776 old_lock,
777 new_lock,
778 offline,
779 )?;
780 }
781
782 Ok(())
783 }
784}
785
786pub fn default_aleo_imports_directory(path: &Path) -> Option<PathBuf> {
788 let parent = path.parent()?;
789 if parent.file_name() == path.file_stem() {
790 return parent.parent().map(Path::to_path_buf);
791 }
792
793 let imports = parent.join("imports");
794 imports.is_dir().then_some(imports)
795}
796
797pub fn aleo_import_path(imports_directory: &Path, program_name: &str) -> PathBuf {
799 let bare_name = bare_unit_name(program_name);
800 let per_unit_path = imports_directory.join(bare_name).join(program_name);
801 if per_unit_path.exists() { per_unit_path } else { imports_directory.join(program_name) }
802}
803
804fn main_template(name: &str) -> String {
805 format!(
806 r#"// The '{name}' program.
807program {name}.aleo {{
808 // This is the constructor for the program.
809 // The constructor allows you to manage program upgrades.
810 // It is called when the program is deployed or upgraded.
811 // It is currently configured to **prevent** upgrades.
812 // Other configurations include:
813 // - @admin(address="aleo1...")
814 // - @checksum(mapping="credits.aleo/fixme", key="0field")
815 // - @custom
816 // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
817 @noupgrade
818 constructor() {{}}
819
820 fn main(public a: u32, b: u32) -> u32 {{
821 let c: u32 = a + b;
822 return c;
823 }}
824}}
825"#
826 )
827}
828
829fn test_template(name: &str) -> String {
830 format!(
831 r#"// The 'test_{name}' test program.
832import {name}.aleo;
833program test_{name}.aleo {{
834 @test
835 @should_fail
836 fn test_main_fails() {{
837 let result: u32 = {name}.aleo::main(2u32, 3u32);
838 assert_eq(result, 3u32);
839 }}
840
841 @noupgrade
842 constructor() {{}}
843}}
844"#
845 )
846}
847
848fn lib_template(name: &str) -> String {
849 format!(
850 r#"// The '{name}' library.
851
852// Returns the identity of x.
853export fn example(x: u32) -> u32 {{
854 return x;
855}}
856"#
857 )
858}
859
860fn lib_test_template(name: &str) -> String {
861 format!(
862 r#"// The 'test_{name}' test program.
863program test_{name}.aleo {{
864 @test
865 fn test_example() {{
866 assert_eq({name}::example(42u32), 42u32);
867 }}
868
869 @noupgrade
870 constructor() {{}}
871}}
872"#
873 )
874}
875
876fn collect_declared_deps(
883 root_path: &Path,
884 manifest: &Manifest,
885 with_tests: bool,
886) -> Result<IndexMap<Symbol, Dependency>> {
887 let mut declared = IndexMap::new();
888 collect_declared_deps_recursive(root_path, manifest, with_tests, &mut declared)?;
889 Ok(declared)
890}
891
892fn collect_declared_deps_recursive(
893 base_path: &Path,
894 manifest: &Manifest,
895 include_dev: bool,
896 declared: &mut IndexMap<Symbol, Dependency>,
897) -> Result<()> {
898 let deps = manifest.dependencies.iter().flatten();
899 let dev: Vec<&Dependency> =
900 if include_dev { manifest.dev_dependencies.iter().flatten().collect() } else { Vec::new() };
901 for dep in deps.chain(dev) {
902 let dep = canonicalize_dependency_path_relative_to(base_path, dep.clone())?;
903 let dep = if dep.location == Location::Workspace { resolve_workspace_dependency(base_path, dep)? } else { dep };
905 let sym = symbol(&dep.name)?;
906 let Entry::Vacant(e) = declared.entry(sym) else {
910 continue;
911 };
912 e.insert(dep.clone());
913 if dep.location == Location::Local
914 && let Some(path) = &dep.path
915 {
916 let manifest_path = path.join(MANIFEST_FILENAME);
917 if path.is_dir() && manifest_path.exists() {
918 let child = Manifest::read_from_file(manifest_path)?;
919 collect_declared_deps_recursive(path, &child, false, declared)?;
921 }
922 }
923 }
924 Ok(())
925}
926
927#[cfg(test)]
928mod tests {
929 use super::*;
930 use leo_span::create_session_if_not_set_then;
931
932 const LEAF_PROGRAM: &str = "\
933program leaf.aleo;
934
935function identity:
936 input r0 as u32.private;
937 output r0 as u32.private;
938";
939
940 const DEPENDENCY_PROGRAM: &str = "\
941import leaf.aleo;
942
943program dependency.aleo;
944
945function times_two:
946 input r0 as u32.private;
947 call leaf.aleo/identity r0 into r1;
948 add r1 r1 into r2;
949 output r2 as u32.private;
950";
951
952 const MAIN_PROGRAM: &str = "\
953import dependency.aleo;
954
955program standalone.aleo;
956
957function main:
958 input r0 as u32.private;
959 call dependency.aleo/times_two r0 into r1;
960 output r1 as u32.private;
961";
962
963 fn dummy_package(base: &str) -> Package {
964 dummy_package_with(base, None)
965 }
966
967 fn dummy_package_with(base: &str, workspace_root: Option<PathBuf>) -> Package {
968 Package {
969 base_directory: PathBuf::from(base),
970 workspace_root,
971 compilation_units: Vec::new(),
972 manifest: Manifest {
973 program: "demo.aleo".to_string(),
974 version: "0.1.0".to_string(),
975 description: String::new(),
976 license: "MIT".to_string(),
977 leo: "0.0.0".to_string(),
978 dependencies: None,
979 dev_dependencies: None,
980 no_std: false,
981 },
982 dep_graph: DiGraph::default(),
983 }
984 }
985
986 #[test]
987 fn bare_unit_name_strips_aleo_suffix() {
988 assert_eq!(crate::bare_unit_name("token.aleo"), "token");
989 assert_eq!(crate::bare_unit_name("token"), "token");
990 assert_eq!(crate::bare_unit_name("credits.aleo"), "credits");
991 }
992
993 #[test]
994 fn unit_paths_are_keyed_by_bare_name() {
995 let pkg = dummy_package("/tmp/demo");
996 assert_eq!(pkg.unit_build_directory("token.aleo"), PathBuf::from("/tmp/demo/build/token"));
999 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/demo/build/token"));
1000 assert_eq!(pkg.unit_bytecode_path("token.aleo"), PathBuf::from("/tmp/demo/build/token/token.aleo"));
1001 assert_eq!(pkg.unit_abi_path("token"), PathBuf::from("/tmp/demo/build/token/abi.json"));
1002 assert_eq!(pkg.unit_interfaces_directory("token"), PathBuf::from("/tmp/demo/build/token/interfaces"));
1003 }
1004
1005 #[test]
1006 fn libraries_are_keyed_like_programs() {
1007 let pkg = dummy_package("/tmp/demo");
1010 assert_eq!(pkg.unit_build_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib"));
1011 assert_eq!(pkg.unit_interfaces_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib/interfaces"));
1012 }
1013
1014 #[test]
1015 fn build_directory_is_the_single_root() {
1016 let pkg = dummy_package("/tmp/demo");
1017 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/demo/build"));
1018 assert!(pkg.unit_bytecode_path("x").starts_with(pkg.build_directory()));
1020 assert!(pkg.unit_interfaces_directory("credits.aleo").starts_with(pkg.build_directory()));
1021 }
1022
1023 #[test]
1024 fn workspace_root_routes_build_directory_to_shared() {
1025 let pkg = dummy_package_with("/tmp/ws/members/token", Some(PathBuf::from("/tmp/ws")));
1030 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/ws/build"));
1031 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/ws/build/token"));
1032 assert_eq!(pkg.unit_bytecode_path("token"), PathBuf::from("/tmp/ws/build/token/token.aleo"));
1033 let dep = dummy_package_with("/tmp/ws/members/swap", Some(PathBuf::from("/tmp/ws")));
1037 assert_eq!(pkg.unit_bytecode_path("token"), dep.unit_bytecode_path("token"));
1038 }
1039
1040 #[test]
1041 fn standalone_package_keeps_per_base_build_directory() {
1042 let pkg = dummy_package_with("/tmp/standalone", None);
1045 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/standalone/build"));
1046 assert_eq!(pkg.unit_build_directory("demo"), PathBuf::from("/tmp/standalone/build/demo"));
1047 }
1048
1049 #[test]
1050 fn aleo_file_uses_sibling_imports_directory() {
1051 create_session_if_not_set_then(|_| {
1052 let root = crate::test_util::unique_dir("aleo-file-flat-imports");
1053 let program_path = root.join("standalone.aleo");
1054 let home = root.join("home");
1055 crate::test_util::write_file(&program_path, MAIN_PROGRAM);
1056 crate::test_util::write_file(&root.join("imports/dependency.aleo"), DEPENDENCY_PROGRAM);
1057 crate::test_util::write_file(&root.join("imports/leaf.aleo"), LEAF_PROGRAM);
1058 std::fs::create_dir_all(&home).expect("test registry directory should be created");
1059
1060 let package =
1061 Package::from_aleo_file(&program_path, &home, Some(&root.join("imports")), false, false, None, None, 0)
1062 .expect("standalone Aleo program should load with its local import");
1063
1064 let names = package.compilation_units.iter().map(|unit| unit.name.to_string()).collect::<Vec<_>>();
1065 assert_eq!(names, ["leaf.aleo", "dependency.aleo", "standalone.aleo"]);
1066 assert!(package.compilation_units.iter().all(|unit| unit.is_local));
1067 assert_eq!(package.manifest.program, "standalone.aleo");
1068
1069 std::fs::remove_dir_all(root).expect("test directory should be removed");
1070 });
1071 }
1072
1073 #[test]
1074 fn aleo_file_uses_per_unit_build_layout() {
1075 create_session_if_not_set_then(|_| {
1076 let root = crate::test_util::unique_dir("aleo-file-per-unit-imports");
1077 let program_path = root.join("standalone/standalone.aleo");
1078 let home = root.join("home");
1079 crate::test_util::write_file(&program_path, MAIN_PROGRAM);
1080 crate::test_util::write_file(&root.join("dependency/dependency.aleo"), DEPENDENCY_PROGRAM);
1081 crate::test_util::write_file(&root.join("leaf/leaf.aleo"), LEAF_PROGRAM);
1082 std::fs::create_dir_all(&home).expect("test registry directory should be created");
1083
1084 let package = Package::from_aleo_file(&program_path, &home, Some(&root), false, false, None, None, 0)
1085 .expect("standalone Aleo build artifact should load with its local import");
1086
1087 let names = package.compilation_units.iter().map(|unit| unit.name.to_string()).collect::<Vec<_>>();
1088 assert_eq!(names, ["leaf.aleo", "dependency.aleo", "standalone.aleo"]);
1089 assert!(package.compilation_units.iter().all(|unit| unit.is_local));
1090
1091 std::fs::remove_dir_all(root).expect("test directory should be removed");
1092 });
1093 }
1094
1095 #[test]
1096 fn missing_aleo_import_is_classified_as_network() {
1097 create_session_if_not_set_then(|_| {
1098 let root = crate::test_util::unique_dir("aleo-file-network-import");
1099 let program_path = root.join("standalone.aleo");
1100 crate::test_util::write_file(&program_path, MAIN_PROGRAM);
1101
1102 let unit =
1103 CompilationUnit::from_aleo_path(Symbol::intern("standalone.aleo"), &program_path, &IndexMap::new())
1104 .expect("test Aleo program should load");
1105
1106 let dependency = unit.dependencies.first().expect("test program should have one direct import");
1107 assert_eq!(dependency.name, "dependency.aleo");
1108 assert_eq!(dependency.location, Location::Network);
1109
1110 std::fs::remove_dir_all(root).expect("test directory should be removed");
1111 });
1112 }
1113
1114 #[test]
1115 fn aleo_file_resolves_local_import_below_network_import() {
1116 create_session_if_not_set_then(|_| {
1117 let root = crate::test_util::unique_dir("aleo-file-mixed-transitive-imports");
1118 let program_path = root.join("standalone.aleo");
1119 let imports = root.join("imports");
1120 let home = root.join("home");
1121 crate::test_util::write_file(&program_path, MAIN_PROGRAM);
1122 crate::test_util::write_file(&imports.join("leaf.aleo"), LEAF_PROGRAM);
1123 crate::test_util::write_file(
1124 &home.join("registry/testnet/dependency/0/dependency.aleo"),
1125 DEPENDENCY_PROGRAM,
1126 );
1127
1128 let package = Package::from_aleo_file(
1129 &program_path,
1130 &home,
1131 Some(&imports),
1132 false,
1133 false,
1134 Some(NetworkName::TestnetV0),
1135 Some("http://localhost:1"),
1136 0,
1137 )
1138 .expect("a local transitive import below a network import should be used");
1139
1140 let units =
1141 package.compilation_units.iter().map(|unit| (unit.name.to_string(), unit.is_local)).collect::<Vec<_>>();
1142 assert_eq!(units, [
1143 ("leaf.aleo".to_string(), true),
1144 ("dependency.aleo".to_string(), false),
1145 ("standalone.aleo".to_string(), true)
1146 ]);
1147
1148 std::fs::remove_dir_all(root).expect("test directory should be removed");
1149 });
1150 }
1151
1152 #[test]
1153 fn aleo_file_rejects_non_directory_imports_path() {
1154 create_session_if_not_set_then(|_| {
1155 let root = crate::test_util::unique_dir("aleo-file-invalid-imports-directory");
1156 let program_path = root.join("standalone.aleo");
1157 let home = root.join("home");
1158 crate::test_util::write_file(&program_path, MAIN_PROGRAM);
1159 std::fs::create_dir_all(&home).expect("test registry directory should be created");
1160
1161 let error = Package::from_aleo_file(&program_path, &home, Some(&program_path), false, false, None, None, 0)
1162 .expect_err("an imports path that is not a directory should fail");
1163 assert!(error.to_string().contains("Expected an Aleo imports directory"));
1164
1165 std::fs::remove_dir_all(root).expect("test directory should be removed");
1166 });
1167 }
1168}