1use crate::*;
18
19use leo_ast::DiGraph;
20use leo_errors::Result;
21use leo_span::Symbol;
22
23use indexmap::{IndexMap, map::Entry};
24use snarkvm::prelude::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 unit_snapshots_directory(&self, name: &str) -> PathBuf {
118 self.unit_build_directory(name).join(SNAPSHOTS_DIRNAME)
119 }
120
121 pub fn source_directory(&self) -> PathBuf {
122 self.base_directory.join(SOURCE_DIRECTORY)
123 }
124
125 pub fn tests_directory(&self) -> PathBuf {
126 self.base_directory.join(TESTS_DIRECTORY)
127 }
128
129 pub fn initialize<P: AsRef<Path>>(package_name: &str, path: P, is_library: bool) -> Result<PathBuf> {
131 Self::initialize_impl(package_name, path.as_ref(), is_library)
132 }
133
134 fn initialize_impl(package_name: &str, path: &Path, is_library: bool) -> Result<PathBuf> {
135 let package_name = if is_library {
136 if !crate::is_valid_library_name(package_name) {
137 return Err(crate::errors::cli_invalid_package_name("library", package_name).into());
138 }
139
140 package_name.to_string()
141 } else {
142 let program_name =
143 if package_name.ends_with(".aleo") { package_name.to_string() } else { format!("{package_name}.aleo") };
144
145 if !crate::is_valid_program_name(&program_name) {
146 return Err(crate::errors::cli_invalid_package_name("program", &program_name).into());
147 }
148
149 program_name
150 };
151
152 let path = path.canonicalize().map_err(|e| crate::errors::failed_path(path.display(), e))?;
153 let full_path = path.join(package_name.strip_suffix(".aleo").unwrap_or(&package_name));
154
155 if full_path.exists() {
157 return Err(
158 crate::errors::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
159 );
160 }
161
162 std::fs::create_dir(&full_path)
164 .map_err(|e| crate::errors::failed_to_initialize_package(&package_name, &full_path, e))?;
165
166 std::env::set_current_dir(&full_path)
168 .map_err(|e| crate::errors::failed_to_initialize_package(&package_name, &full_path, e))?;
169
170 const GITIGNORE_TEMPLATE: &str = ".env\n*.avm\n*.prover\n*.verifier\nbuild/\n";
172 const GITIGNORE_FILENAME: &str = ".gitignore";
173
174 let gitignore_path = full_path.join(GITIGNORE_FILENAME);
175 std::fs::write(gitignore_path, GITIGNORE_TEMPLATE).map_err(crate::errors::io_error_gitignore_file)?;
176
177 let manifest = Manifest {
179 program: package_name.clone(),
180 version: "0.1.0".to_string(),
181 description: String::new(),
182 license: "MIT".to_string(),
183 leo: env!("CARGO_PKG_VERSION").to_string(),
184 dependencies: None,
185 dev_dependencies: None,
186 no_std: false,
187 };
188
189 let manifest_path = full_path.join(MANIFEST_FILENAME);
190 manifest.write_to_file(manifest_path)?;
191
192 let source_path = full_path.join(SOURCE_DIRECTORY);
194
195 std::fs::create_dir(&source_path)
196 .map_err(|e| crate::errors::failed_to_create_source_directory(source_path.display(), e))?;
197
198 let name_no_aleo = package_name.strip_suffix(".aleo").unwrap_or(&package_name);
199
200 if is_library {
201 let lib_path = source_path.join("lib.leo");
203
204 std::fs::write(&lib_path, lib_template(name_no_aleo)).map_err(|e| {
205 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", lib_path.display()), e)
206 })?;
207
208 let tests_path = full_path.join(TESTS_DIRECTORY);
210
211 std::fs::create_dir(&tests_path)
212 .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
213
214 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
215
216 std::fs::write(&test_file_path, lib_test_template(name_no_aleo)).map_err(|e| {
217 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
218 })?;
219 } else {
220 let main_path = source_path.join(MAIN_FILENAME);
222
223 std::fs::write(&main_path, main_template(name_no_aleo)).map_err(|e| {
224 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e)
225 })?;
226
227 let tests_path = full_path.join(TESTS_DIRECTORY);
229
230 std::fs::create_dir(&tests_path)
231 .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
232
233 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
234
235 std::fs::write(&test_file_path, test_template(name_no_aleo)).map_err(|e| {
236 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
237 })?;
238 }
239
240 Ok(full_path)
241 }
242
243 pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
247 path: P,
248 home_path: Q,
249 network: Option<NetworkName>,
250 endpoint: Option<&str>,
251 network_retries: u32,
252 ) -> Result<Self> {
253 Self::from_directory_impl(
254 path.as_ref(),
255 home_path.as_ref(),
256 false,
257 false,
258 false,
259 false,
260 false,
261 network,
262 endpoint,
263 network_retries,
264 )
265 }
266
267 #[allow(clippy::too_many_arguments)]
270 pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
271 path: P,
272 home_path: Q,
273 no_cache: bool,
274 no_local: bool,
275 offline: bool,
276 network: Option<NetworkName>,
277 endpoint: Option<&str>,
278 network_retries: u32,
279 ) -> Result<Self> {
280 Self::from_directory_impl(
281 path.as_ref(),
282 home_path.as_ref(),
283 true,
284 false,
285 no_cache,
286 no_local,
287 offline,
288 network,
289 endpoint,
290 network_retries,
291 )
292 }
293
294 #[allow(clippy::too_many_arguments)]
297 pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
298 path: P,
299 home_path: Q,
300 no_cache: bool,
301 no_local: bool,
302 offline: bool,
303 network: Option<NetworkName>,
304 endpoint: Option<&str>,
305 network_retries: u32,
306 ) -> Result<Self> {
307 Self::from_directory_impl(
308 path.as_ref(),
309 home_path.as_ref(),
310 true,
311 true,
312 no_cache,
313 no_local,
314 offline,
315 network,
316 endpoint,
317 network_retries,
318 )
319 }
320
321 pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
322 let path = self.tests_directory();
323 let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
326 data.into_iter()
327 }
328
329 fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
330 path.read_dir()
331 .ok()
332 .into_iter()
333 .flatten()
334 .flat_map(|maybe_filename| maybe_filename.ok())
335 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
336 .flat_map(move |entry| {
337 let path = entry.path();
338 if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
339 })
340 }
341
342 #[allow(clippy::too_many_arguments)]
343 fn from_directory_impl(
344 path: &Path,
345 home_path: &Path,
346 build_graph: bool,
347 with_tests: bool,
348 no_cache: bool,
349 no_local: bool,
350 offline: bool,
351 network: Option<NetworkName>,
352 endpoint: Option<&str>,
353 network_retries: u32,
354 ) -> Result<Self> {
355 let map_err = |path: &Path, err| {
356 crate::errors::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
357 };
358
359 let path = path.canonicalize().map_err(|err| map_err(path, err))?;
360
361 let workspace_root = Workspace::discover_root(&path)?;
365
366 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
367
368 let (compilation_units, digraph) = if build_graph {
369 let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
370
371 let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
372
373 let mut digraph = DiGraph::<Symbol>::new(Default::default());
374
375 let declared_deps = collect_declared_deps(&path, &manifest, with_tests)?;
378
379 let lock_dir = workspace_root.as_deref().unwrap_or(&path).to_path_buf();
381 let old_lock = Lock::read(&lock_dir);
383 let mut new_lock = Lock::default();
384
385 let first_dependency = Dependency {
386 name: manifest.program.clone(),
387 location: Location::Local,
388 path: Some(path.clone()),
389 edition: None,
390 ..Default::default()
391 };
392
393 let test_dependencies: Vec<Dependency> = if with_tests {
394 let tests_directory = path.join(TESTS_DIRECTORY);
395 let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
396 .map(|path| Dependency {
397 name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
399 edition: None,
400 location: Location::Test,
401 path: Some(path.to_path_buf()),
402 ..Default::default()
403 })
404 .collect();
405 if let Some(deps) = manifest.dev_dependencies.as_ref() {
406 test_dependencies.extend(deps.iter().cloned());
407 }
408 test_dependencies
409 } else {
410 Vec::new()
411 };
412
413 for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
414 Self::graph_build(
415 &home_path,
416 network,
417 endpoint,
418 &first_dependency,
419 dependency,
420 &mut map,
421 &mut digraph,
422 no_cache,
423 no_local,
424 network_retries,
425 &declared_deps,
426 &old_lock,
427 &mut new_lock,
428 offline,
429 )?;
430 }
431
432 if workspace_root.is_some() {
435 new_lock.carry_over(&old_lock, |_| true);
436 } else {
437 let dev_git_names: Vec<&str> = if with_tests {
438 Vec::new()
439 } else {
440 manifest
441 .dev_dependencies
442 .iter()
443 .flatten()
444 .filter(|dep| dep.location == Location::Git)
445 .map(|dep| dep.name.as_str())
446 .collect()
447 };
448 new_lock.carry_over(&old_lock, |entry| dev_git_names.contains(&entry.name.as_str()));
449 }
450 new_lock.write(&lock_dir)?;
452
453 let ordered_dependency_symbols =
454 digraph.post_order().map_err(|_| crate::errors::circular_dependency_error())?;
455
456 (
457 ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect(),
458 digraph,
459 )
460 } else {
461 (Vec::new(), DiGraph::default())
462 };
463
464 Ok(Package { base_directory: path, workspace_root, compilation_units, manifest, dep_graph: digraph })
465 }
466
467 #[allow(clippy::too_many_arguments)]
468 fn graph_build(
469 home_path: &Path,
470 network: Option<NetworkName>,
471 endpoint: Option<&str>,
472 main_program: &Dependency,
473 new: Dependency,
474 map: &mut IndexMap<Symbol, (Dependency, CompilationUnit)>,
475 graph: &mut DiGraph<Symbol>,
476 no_cache: bool,
477 no_local: bool,
478 network_retries: u32,
479 declared_deps: &IndexMap<Symbol, Dependency>,
480 old_lock: &Lock,
481 new_lock: &mut Lock,
482 offline: bool,
483 ) -> Result<()> {
484 let name_symbol = symbol(&new.name)?;
485
486 let unit = match map.entry(name_symbol) {
487 Entry::Occupied(occupied) => {
488 let existing_dep = &occupied.get().0;
491 assert_eq!(new.name, existing_dep.name);
492 if new.location != existing_dep.location
493 || new.path != existing_dep.path
494 || new.edition != existing_dep.edition
495 || new.git != existing_dep.git
496 {
497 return Err(crate::errors::conflicting_dependency(existing_dep, new).into());
498 }
499 return Ok(());
500 }
501 Entry::Vacant(vacant) => {
502 let unit = match (new.path.as_ref(), new.location) {
503 (Some(path), Location::Local) if !no_local => {
504 if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
506 CompilationUnit::from_aleo_path(name_symbol, path, declared_deps)?
507 } else {
508 CompilationUnit::from_package_path(name_symbol, path)?
509 }
510 }
511 (Some(path), Location::Test) => {
512 CompilationUnit::from_test_path(path, main_program.clone())?
515 }
516 (_, Location::Network) | (Some(_), Location::Local) => {
517 let Some(endpoint) = endpoint else {
519 return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
520 };
521 let Some(network) = network else {
522 return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
523 };
524 CompilationUnit::fetch(
525 name_symbol,
526 new.edition,
527 home_path,
528 network,
529 endpoint,
530 no_cache,
531 network_retries,
532 )?
533 }
534 (_, Location::Git) => CompilationUnit::from_git(
535 name_symbol,
536 &new,
537 home_path,
538 old_lock,
539 new_lock,
540 offline,
541 declared_deps,
542 )?,
543 (_, Location::Workspace) => {
544 return Err(anyhow!(
545 "Workspace dependency `{}` was not resolved before graph building. This is a compiler bug.",
546 new.name
547 )
548 .into());
549 }
550 _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
551 };
552
553 vacant.insert((new, unit.clone()));
554
555 unit
556 }
557 };
558
559 graph.add_node(name_symbol);
560
561 let checkouts_root = crate::git::checkouts_root(home_path);
564 if let ProgramData::SourcePath { directory, .. } = &unit.data
565 && directory.starts_with(&checkouts_root)
566 {
567 let checkout = directory
569 .strip_prefix(&checkouts_root)
570 .ok()
571 .and_then(|rel| {
572 let mut components = rel.components();
573 Some((components.next()?, components.next()?))
574 })
575 .map(|(key, commit)| checkouts_root.join(key).join(commit));
576 for dependency in unit.dependencies.iter() {
577 if let Some(path) = &dependency.path
578 && !checkout.as_ref().is_some_and(|checkout| path.starts_with(checkout))
579 {
580 return Err(crate::errors::invalid_manifest_dependency(
581 &dependency.name,
582 "a git dependency may only reference paths inside its own repository checkout",
583 )
584 .into());
585 }
586 }
587 }
588
589 for dependency in unit.dependencies.iter() {
590 let dependency_symbol = symbol(&dependency.name)?;
591 graph.add_edge(name_symbol, dependency_symbol);
592 Self::graph_build(
593 home_path,
594 network,
595 endpoint,
596 main_program,
597 dependency.clone(),
598 map,
599 graph,
600 no_cache,
601 no_local,
602 network_retries,
603 declared_deps,
604 old_lock,
605 new_lock,
606 offline,
607 )?;
608 }
609
610 Ok(())
611 }
612}
613
614fn main_template(name: &str) -> String {
615 format!(
616 r#"// The '{name}' program.
617program {name}.aleo {{
618 // This is the constructor for the program.
619 // The constructor allows you to manage program upgrades.
620 // It is called when the program is deployed or upgraded.
621 // It is currently configured to **prevent** upgrades.
622 // Other configurations include:
623 // - @admin(address="aleo1...")
624 // - @checksum(mapping="credits.aleo/fixme", key="0field")
625 // - @custom
626 // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
627 @noupgrade
628 constructor() {{}}
629
630 fn main(public a: u32, b: u32) -> u32 {{
631 let c: u32 = a + b;
632 return c;
633 }}
634}}
635"#
636 )
637}
638
639fn test_template(name: &str) -> String {
640 format!(
641 r#"// The 'test_{name}' test program.
642import {name}.aleo;
643program test_{name}.aleo {{
644 @test
645 @should_fail
646 fn test_main_fails() {{
647 let result: u32 = {name}.aleo::main(2u32, 3u32);
648 assert_eq(result, 3u32);
649 }}
650
651 @noupgrade
652 constructor() {{}}
653}}
654"#
655 )
656}
657
658fn lib_template(name: &str) -> String {
659 format!(
660 r#"// The '{name}' library.
661
662// Returns the identity of x.
663fn example(x: u32) -> u32 {{
664 return x;
665}}
666"#
667 )
668}
669
670fn lib_test_template(name: &str) -> String {
671 format!(
672 r#"// The 'test_{name}' test program.
673program test_{name}.aleo {{
674 @test
675 fn test_example() {{
676 assert_eq({name}::example(42u32), 42u32);
677 }}
678
679 @noupgrade
680 constructor() {{}}
681}}
682"#
683 )
684}
685
686fn collect_declared_deps(
693 root_path: &Path,
694 manifest: &Manifest,
695 with_tests: bool,
696) -> Result<IndexMap<Symbol, Dependency>> {
697 let mut declared = IndexMap::new();
698 collect_declared_deps_recursive(root_path, manifest, with_tests, &mut declared)?;
699 Ok(declared)
700}
701
702fn collect_declared_deps_recursive(
703 base_path: &Path,
704 manifest: &Manifest,
705 include_dev: bool,
706 declared: &mut IndexMap<Symbol, Dependency>,
707) -> Result<()> {
708 let deps = manifest.dependencies.iter().flatten();
709 let dev: Vec<&Dependency> =
710 if include_dev { manifest.dev_dependencies.iter().flatten().collect() } else { Vec::new() };
711 for dep in deps.chain(dev) {
712 let dep = canonicalize_dependency_path_relative_to(base_path, dep.clone())?;
713 let dep = if dep.location == Location::Workspace { resolve_workspace_dependency(base_path, dep)? } else { dep };
715 let sym = symbol(&dep.name)?;
716 let Entry::Vacant(e) = declared.entry(sym) else {
720 continue;
721 };
722 e.insert(dep.clone());
723 if dep.location == Location::Local
724 && let Some(path) = &dep.path
725 {
726 let manifest_path = path.join(MANIFEST_FILENAME);
727 if path.is_dir() && manifest_path.exists() {
728 let child = Manifest::read_from_file(manifest_path)?;
729 collect_declared_deps_recursive(path, &child, false, declared)?;
731 }
732 }
733 }
734 Ok(())
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740
741 fn dummy_package(base: &str) -> Package {
742 dummy_package_with(base, None)
743 }
744
745 fn dummy_package_with(base: &str, workspace_root: Option<PathBuf>) -> Package {
746 Package {
747 base_directory: PathBuf::from(base),
748 workspace_root,
749 compilation_units: Vec::new(),
750 manifest: Manifest {
751 program: "demo.aleo".to_string(),
752 version: "0.1.0".to_string(),
753 description: String::new(),
754 license: "MIT".to_string(),
755 leo: "0.0.0".to_string(),
756 dependencies: None,
757 dev_dependencies: None,
758 no_std: false,
759 },
760 dep_graph: DiGraph::default(),
761 }
762 }
763
764 #[test]
765 fn bare_unit_name_strips_aleo_suffix() {
766 assert_eq!(crate::bare_unit_name("token.aleo"), "token");
767 assert_eq!(crate::bare_unit_name("token"), "token");
768 assert_eq!(crate::bare_unit_name("credits.aleo"), "credits");
769 }
770
771 #[test]
772 fn unit_paths_are_keyed_by_bare_name() {
773 let pkg = dummy_package("/tmp/demo");
774 assert_eq!(pkg.unit_build_directory("token.aleo"), PathBuf::from("/tmp/demo/build/token"));
777 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/demo/build/token"));
778 assert_eq!(pkg.unit_bytecode_path("token.aleo"), PathBuf::from("/tmp/demo/build/token/token.aleo"));
779 assert_eq!(pkg.unit_abi_path("token"), PathBuf::from("/tmp/demo/build/token/abi.json"));
780 assert_eq!(pkg.unit_interfaces_directory("token"), PathBuf::from("/tmp/demo/build/token/interfaces"));
781 assert_eq!(pkg.unit_snapshots_directory("token"), PathBuf::from("/tmp/demo/build/token/snapshots"));
782 }
783
784 #[test]
785 fn libraries_are_keyed_like_programs() {
786 let pkg = dummy_package("/tmp/demo");
789 assert_eq!(pkg.unit_build_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib"));
790 assert_eq!(pkg.unit_interfaces_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib/interfaces"));
791 }
792
793 #[test]
794 fn build_directory_is_the_single_root() {
795 let pkg = dummy_package("/tmp/demo");
796 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/demo/build"));
797 assert!(pkg.unit_bytecode_path("x").starts_with(pkg.build_directory()));
799 assert!(pkg.unit_interfaces_directory("credits.aleo").starts_with(pkg.build_directory()));
800 }
801
802 #[test]
803 fn workspace_root_routes_build_directory_to_shared() {
804 let pkg = dummy_package_with("/tmp/ws/members/token", Some(PathBuf::from("/tmp/ws")));
809 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/ws/build"));
810 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/ws/build/token"));
811 assert_eq!(pkg.unit_bytecode_path("token"), PathBuf::from("/tmp/ws/build/token/token.aleo"));
812 let dep = dummy_package_with("/tmp/ws/members/swap", Some(PathBuf::from("/tmp/ws")));
816 assert_eq!(pkg.unit_bytecode_path("token"), dep.unit_bytecode_path("token"));
817 }
818
819 #[test]
820 fn standalone_package_keeps_per_base_build_directory() {
821 let pkg = dummy_package_with("/tmp/standalone", None);
824 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/standalone/build"));
825 assert_eq!(pkg.unit_build_directory("demo"), PathBuf::from("/tmp/standalone/build/demo"));
826 }
827}