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 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)]
263 pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
264 path: P,
265 home_path: Q,
266 no_cache: bool,
267 no_local: bool,
268 offline: bool,
269 network: Option<NetworkName>,
270 endpoint: Option<&str>,
271 network_retries: u32,
272 ) -> Result<Self> {
273 Self::from_directory_impl(
274 path.as_ref(),
275 home_path.as_ref(),
276 true,
277 false,
278 no_cache,
279 no_local,
280 offline,
281 network,
282 endpoint,
283 network_retries,
284 )
285 }
286
287 #[allow(clippy::too_many_arguments)]
290 pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
291 path: P,
292 home_path: Q,
293 no_cache: bool,
294 no_local: bool,
295 offline: bool,
296 network: Option<NetworkName>,
297 endpoint: Option<&str>,
298 network_retries: u32,
299 ) -> Result<Self> {
300 Self::from_directory_impl(
301 path.as_ref(),
302 home_path.as_ref(),
303 true,
304 true,
305 no_cache,
306 no_local,
307 offline,
308 network,
309 endpoint,
310 network_retries,
311 )
312 }
313
314 pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
315 let path = self.tests_directory();
316 let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
319 data.into_iter()
320 }
321
322 fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
323 path.read_dir()
324 .ok()
325 .into_iter()
326 .flatten()
327 .flat_map(|maybe_filename| maybe_filename.ok())
328 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
329 .flat_map(move |entry| {
330 let path = entry.path();
331 if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
332 })
333 }
334
335 #[allow(clippy::too_many_arguments)]
336 fn from_directory_impl(
337 path: &Path,
338 home_path: &Path,
339 build_graph: bool,
340 with_tests: bool,
341 no_cache: bool,
342 no_local: bool,
343 offline: bool,
344 network: Option<NetworkName>,
345 endpoint: Option<&str>,
346 network_retries: u32,
347 ) -> Result<Self> {
348 let map_err = |path: &Path, err| {
349 crate::errors::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
350 };
351
352 let path = path.canonicalize().map_err(|err| map_err(path, err))?;
353
354 let workspace_root = Workspace::discover_root(&path)?;
358
359 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
360
361 let (compilation_units, digraph) = if build_graph {
362 let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
363
364 let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
365
366 let mut digraph = DiGraph::<Symbol>::new(Default::default());
367
368 let declared_deps = collect_declared_deps(&path, &manifest, with_tests)?;
371
372 let lock_dir = workspace_root.as_deref().unwrap_or(&path).to_path_buf();
374 let old_lock = Lock::read(&lock_dir);
376 let mut new_lock = Lock::default();
377
378 let first_dependency = Dependency {
379 name: manifest.program.clone(),
380 location: Location::Local,
381 path: Some(path.clone()),
382 edition: None,
383 ..Default::default()
384 };
385
386 let test_dependencies: Vec<Dependency> = if with_tests {
387 let tests_directory = path.join(TESTS_DIRECTORY);
388 let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
389 .map(|path| Dependency {
390 name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
392 edition: None,
393 location: Location::Test,
394 path: Some(path.to_path_buf()),
395 ..Default::default()
396 })
397 .collect();
398 if let Some(deps) = manifest.dev_dependencies.as_ref() {
399 for dep in deps {
402 let dep = canonicalize_dependency_path_relative_to(&path, dep.clone())?;
403 let dep = if dep.location == Location::Workspace {
404 resolve_workspace_dependency(&path, dep)?
405 } else {
406 dep
407 };
408 test_dependencies.push(dep);
409 }
410 }
411 test_dependencies
412 } else {
413 Vec::new()
414 };
415
416 for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
417 Self::graph_build(
418 &home_path,
419 network,
420 endpoint,
421 &first_dependency,
422 dependency,
423 &mut map,
424 &mut digraph,
425 no_cache,
426 no_local,
427 network_retries,
428 &declared_deps,
429 &old_lock,
430 &mut new_lock,
431 offline,
432 )?;
433 }
434
435 if workspace_root.is_some() {
438 new_lock.carry_over(&old_lock, |_| true);
439 } else {
440 let dev_git_names: Vec<&str> = if with_tests {
441 Vec::new()
442 } else {
443 manifest
444 .dev_dependencies
445 .iter()
446 .flatten()
447 .filter(|dep| dep.location == Location::Git)
448 .map(|dep| dep.name.as_str())
449 .collect()
450 };
451 new_lock.carry_over(&old_lock, |entry| dev_git_names.contains(&entry.name.as_str()));
452 }
453 new_lock.write(&lock_dir)?;
455
456 let ordered_dependency_symbols =
457 digraph.post_order().map_err(|_| crate::errors::circular_dependency_error())?;
458
459 (
460 ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect(),
461 digraph,
462 )
463 } else {
464 (Vec::new(), DiGraph::default())
465 };
466
467 Ok(Package { base_directory: path, workspace_root, compilation_units, manifest, dep_graph: digraph })
468 }
469
470 #[allow(clippy::too_many_arguments)]
471 fn graph_build(
472 home_path: &Path,
473 network: Option<NetworkName>,
474 endpoint: Option<&str>,
475 main_program: &Dependency,
476 new: Dependency,
477 map: &mut IndexMap<Symbol, (Dependency, CompilationUnit)>,
478 graph: &mut DiGraph<Symbol>,
479 no_cache: bool,
480 no_local: bool,
481 network_retries: u32,
482 declared_deps: &IndexMap<Symbol, Dependency>,
483 old_lock: &Lock,
484 new_lock: &mut Lock,
485 offline: bool,
486 ) -> Result<()> {
487 let name_symbol = symbol(&new.name)?;
488
489 let unit = match map.entry(name_symbol) {
490 Entry::Occupied(occupied) => {
491 let existing_dep = &occupied.get().0;
494 assert_eq!(new.name, existing_dep.name);
495 if new.location != existing_dep.location
496 || new.path != existing_dep.path
497 || new.edition != existing_dep.edition
498 || new.git != existing_dep.git
499 {
500 return Err(crate::errors::conflicting_dependency(existing_dep, new).into());
501 }
502 return Ok(());
503 }
504 Entry::Vacant(vacant) => {
505 let unit = match (new.path.as_ref(), new.location) {
506 (Some(path), Location::Local) if !no_local => {
507 if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
509 CompilationUnit::from_aleo_path(name_symbol, path, declared_deps)?
510 } else {
511 CompilationUnit::from_package_path(name_symbol, path)?
512 }
513 }
514 (Some(path), Location::Test) => {
515 CompilationUnit::from_test_path(path, main_program.clone())?
518 }
519 (_, Location::Network) | (Some(_), Location::Local) => {
520 let Some(endpoint) = endpoint else {
522 return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
523 };
524 let Some(network) = network else {
525 return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
526 };
527 CompilationUnit::fetch(
528 name_symbol,
529 new.edition,
530 home_path,
531 network,
532 endpoint,
533 no_cache,
534 network_retries,
535 )?
536 }
537 (_, Location::Git) => CompilationUnit::from_git(
538 name_symbol,
539 &new,
540 home_path,
541 old_lock,
542 new_lock,
543 offline,
544 declared_deps,
545 )?,
546 (_, Location::Workspace) => {
547 return Err(anyhow!(
548 "Workspace dependency `{}` was not resolved before graph building. This is a compiler bug.",
549 new.name
550 )
551 .into());
552 }
553 _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
554 };
555
556 vacant.insert((new, unit.clone()));
557
558 unit
559 }
560 };
561
562 graph.add_node(name_symbol);
563
564 let checkouts_root = crate::git::checkouts_root(home_path);
567 if let ProgramData::SourcePath { directory, .. } = &unit.data
568 && directory.starts_with(&checkouts_root)
569 {
570 let checkout = directory
572 .strip_prefix(&checkouts_root)
573 .ok()
574 .and_then(|rel| {
575 let mut components = rel.components();
576 Some((components.next()?, components.next()?))
577 })
578 .map(|(key, commit)| checkouts_root.join(key).join(commit));
579 for dependency in unit.dependencies.iter() {
580 if let Some(path) = &dependency.path
581 && !checkout.as_ref().is_some_and(|checkout| path.starts_with(checkout))
582 {
583 return Err(crate::errors::invalid_manifest_dependency(
584 &dependency.name,
585 "a git dependency may only reference paths inside its own repository checkout",
586 )
587 .into());
588 }
589 }
590 }
591
592 for dependency in unit.dependencies.iter() {
593 let dependency_symbol = symbol(&dependency.name)?;
594 graph.add_edge(name_symbol, dependency_symbol);
595 Self::graph_build(
596 home_path,
597 network,
598 endpoint,
599 main_program,
600 dependency.clone(),
601 map,
602 graph,
603 no_cache,
604 no_local,
605 network_retries,
606 declared_deps,
607 old_lock,
608 new_lock,
609 offline,
610 )?;
611 }
612
613 Ok(())
614 }
615}
616
617fn main_template(name: &str) -> String {
618 format!(
619 r#"// The '{name}' program.
620program {name}.aleo {{
621 // This is the constructor for the program.
622 // The constructor allows you to manage program upgrades.
623 // It is called when the program is deployed or upgraded.
624 // It is currently configured to **prevent** upgrades.
625 // Other configurations include:
626 // - @admin(address="aleo1...")
627 // - @checksum(mapping="credits.aleo/fixme", key="0field")
628 // - @custom
629 // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
630 @noupgrade
631 constructor() {{}}
632
633 fn main(public a: u32, b: u32) -> u32 {{
634 let c: u32 = a + b;
635 return c;
636 }}
637}}
638"#
639 )
640}
641
642fn test_template(name: &str) -> String {
643 format!(
644 r#"// The 'test_{name}' test program.
645import {name}.aleo;
646program test_{name}.aleo {{
647 @test
648 @should_fail
649 fn test_main_fails() {{
650 let result: u32 = {name}.aleo::main(2u32, 3u32);
651 assert_eq(result, 3u32);
652 }}
653
654 @noupgrade
655 constructor() {{}}
656}}
657"#
658 )
659}
660
661fn lib_template(name: &str) -> String {
662 format!(
663 r#"// The '{name}' library.
664
665// Returns the identity of x.
666export fn example(x: u32) -> u32 {{
667 return x;
668}}
669"#
670 )
671}
672
673fn lib_test_template(name: &str) -> String {
674 format!(
675 r#"// The 'test_{name}' test program.
676program test_{name}.aleo {{
677 @test
678 fn test_example() {{
679 assert_eq({name}::example(42u32), 42u32);
680 }}
681
682 @noupgrade
683 constructor() {{}}
684}}
685"#
686 )
687}
688
689fn collect_declared_deps(
696 root_path: &Path,
697 manifest: &Manifest,
698 with_tests: bool,
699) -> Result<IndexMap<Symbol, Dependency>> {
700 let mut declared = IndexMap::new();
701 collect_declared_deps_recursive(root_path, manifest, with_tests, &mut declared)?;
702 Ok(declared)
703}
704
705fn collect_declared_deps_recursive(
706 base_path: &Path,
707 manifest: &Manifest,
708 include_dev: bool,
709 declared: &mut IndexMap<Symbol, Dependency>,
710) -> Result<()> {
711 let deps = manifest.dependencies.iter().flatten();
712 let dev: Vec<&Dependency> =
713 if include_dev { manifest.dev_dependencies.iter().flatten().collect() } else { Vec::new() };
714 for dep in deps.chain(dev) {
715 let dep = canonicalize_dependency_path_relative_to(base_path, dep.clone())?;
716 let dep = if dep.location == Location::Workspace { resolve_workspace_dependency(base_path, dep)? } else { dep };
718 let sym = symbol(&dep.name)?;
719 let Entry::Vacant(e) = declared.entry(sym) else {
723 continue;
724 };
725 e.insert(dep.clone());
726 if dep.location == Location::Local
727 && let Some(path) = &dep.path
728 {
729 let manifest_path = path.join(MANIFEST_FILENAME);
730 if path.is_dir() && manifest_path.exists() {
731 let child = Manifest::read_from_file(manifest_path)?;
732 collect_declared_deps_recursive(path, &child, false, declared)?;
734 }
735 }
736 }
737 Ok(())
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 fn dummy_package(base: &str) -> Package {
745 dummy_package_with(base, None)
746 }
747
748 fn dummy_package_with(base: &str, workspace_root: Option<PathBuf>) -> Package {
749 Package {
750 base_directory: PathBuf::from(base),
751 workspace_root,
752 compilation_units: Vec::new(),
753 manifest: Manifest {
754 program: "demo.aleo".to_string(),
755 version: "0.1.0".to_string(),
756 description: String::new(),
757 license: "MIT".to_string(),
758 leo: "0.0.0".to_string(),
759 dependencies: None,
760 dev_dependencies: None,
761 no_std: false,
762 },
763 dep_graph: DiGraph::default(),
764 }
765 }
766
767 #[test]
768 fn bare_unit_name_strips_aleo_suffix() {
769 assert_eq!(crate::bare_unit_name("token.aleo"), "token");
770 assert_eq!(crate::bare_unit_name("token"), "token");
771 assert_eq!(crate::bare_unit_name("credits.aleo"), "credits");
772 }
773
774 #[test]
775 fn unit_paths_are_keyed_by_bare_name() {
776 let pkg = dummy_package("/tmp/demo");
777 assert_eq!(pkg.unit_build_directory("token.aleo"), PathBuf::from("/tmp/demo/build/token"));
780 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/demo/build/token"));
781 assert_eq!(pkg.unit_bytecode_path("token.aleo"), PathBuf::from("/tmp/demo/build/token/token.aleo"));
782 assert_eq!(pkg.unit_abi_path("token"), PathBuf::from("/tmp/demo/build/token/abi.json"));
783 assert_eq!(pkg.unit_interfaces_directory("token"), PathBuf::from("/tmp/demo/build/token/interfaces"));
784 }
785
786 #[test]
787 fn libraries_are_keyed_like_programs() {
788 let pkg = dummy_package("/tmp/demo");
791 assert_eq!(pkg.unit_build_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib"));
792 assert_eq!(pkg.unit_interfaces_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib/interfaces"));
793 }
794
795 #[test]
796 fn build_directory_is_the_single_root() {
797 let pkg = dummy_package("/tmp/demo");
798 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/demo/build"));
799 assert!(pkg.unit_bytecode_path("x").starts_with(pkg.build_directory()));
801 assert!(pkg.unit_interfaces_directory("credits.aleo").starts_with(pkg.build_directory()));
802 }
803
804 #[test]
805 fn workspace_root_routes_build_directory_to_shared() {
806 let pkg = dummy_package_with("/tmp/ws/members/token", Some(PathBuf::from("/tmp/ws")));
811 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/ws/build"));
812 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/ws/build/token"));
813 assert_eq!(pkg.unit_bytecode_path("token"), PathBuf::from("/tmp/ws/build/token/token.aleo"));
814 let dep = dummy_package_with("/tmp/ws/members/swap", Some(PathBuf::from("/tmp/ws")));
818 assert_eq!(pkg.unit_bytecode_path("token"), dep.unit_bytecode_path("token"));
819 }
820
821 #[test]
822 fn standalone_package_keeps_per_base_build_directory() {
823 let pkg = dummy_package_with("/tmp/standalone", None);
826 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/standalone/build"));
827 assert_eq!(pkg.unit_build_directory("demo"), PathBuf::from("/tmp/standalone/build/demo"));
828 }
829}