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 };
187
188 let manifest_path = full_path.join(MANIFEST_FILENAME);
189 manifest.write_to_file(manifest_path)?;
190
191 let source_path = full_path.join(SOURCE_DIRECTORY);
193
194 std::fs::create_dir(&source_path)
195 .map_err(|e| crate::errors::failed_to_create_source_directory(source_path.display(), e))?;
196
197 let name_no_aleo = package_name.strip_suffix(".aleo").unwrap_or(&package_name);
198
199 if is_library {
200 let lib_path = source_path.join("lib.leo");
202
203 std::fs::write(&lib_path, lib_template(name_no_aleo)).map_err(|e| {
204 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", lib_path.display()), e)
205 })?;
206
207 let tests_path = full_path.join(TESTS_DIRECTORY);
209
210 std::fs::create_dir(&tests_path)
211 .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
212
213 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
214
215 std::fs::write(&test_file_path, lib_test_template(name_no_aleo)).map_err(|e| {
216 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
217 })?;
218 } else {
219 let main_path = source_path.join(MAIN_FILENAME);
221
222 std::fs::write(&main_path, main_template(name_no_aleo)).map_err(|e| {
223 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e)
224 })?;
225
226 let tests_path = full_path.join(TESTS_DIRECTORY);
228
229 std::fs::create_dir(&tests_path)
230 .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
231
232 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
233
234 std::fs::write(&test_file_path, test_template(name_no_aleo)).map_err(|e| {
235 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
236 })?;
237 }
238
239 Ok(full_path)
240 }
241
242 pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
246 path: P,
247 home_path: Q,
248 network: Option<NetworkName>,
249 endpoint: Option<&str>,
250 network_retries: u32,
251 ) -> Result<Self> {
252 Self::from_directory_impl(
253 path.as_ref(),
254 home_path.as_ref(),
255 false,
256 false,
257 false,
258 false,
259 network,
260 endpoint,
261 network_retries,
262 )
263 }
264
265 pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
268 path: P,
269 home_path: Q,
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_directory_impl(
277 path.as_ref(),
278 home_path.as_ref(),
279 true,
280 false,
281 no_cache,
282 no_local,
283 network,
284 endpoint,
285 network_retries,
286 )
287 }
288
289 pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
292 path: P,
293 home_path: Q,
294 no_cache: bool,
295 no_local: 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 network,
308 endpoint,
309 network_retries,
310 )
311 }
312
313 pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
314 let path = self.tests_directory();
315 let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
318 data.into_iter()
319 }
320
321 fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
322 path.read_dir()
323 .ok()
324 .into_iter()
325 .flatten()
326 .flat_map(|maybe_filename| maybe_filename.ok())
327 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
328 .flat_map(move |entry| {
329 let path = entry.path();
330 if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
331 })
332 }
333
334 #[allow(clippy::too_many_arguments)]
335 fn from_directory_impl(
336 path: &Path,
337 home_path: &Path,
338 build_graph: bool,
339 with_tests: bool,
340 no_cache: bool,
341 no_local: bool,
342 network: Option<NetworkName>,
343 endpoint: Option<&str>,
344 network_retries: u32,
345 ) -> Result<Self> {
346 let map_err = |path: &Path, err| {
347 crate::errors::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
348 };
349
350 let path = path.canonicalize().map_err(|err| map_err(path, err))?;
351
352 let workspace_root = Workspace::discover_root(&path)?;
356
357 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
358
359 let (compilation_units, digraph) = if build_graph {
360 let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
361
362 let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
363
364 let mut digraph = DiGraph::<Symbol>::new(Default::default());
365
366 let declared_deps = collect_declared_deps(&path, &manifest, with_tests)?;
369
370 let first_dependency = Dependency {
371 name: manifest.program.clone(),
372 location: Location::Local,
373 path: Some(path.clone()),
374 edition: None,
375 };
376
377 let test_dependencies: Vec<Dependency> = if with_tests {
378 let tests_directory = path.join(TESTS_DIRECTORY);
379 let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
380 .map(|path| Dependency {
381 name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
383 edition: None,
384 location: Location::Test,
385 path: Some(path.to_path_buf()),
386 })
387 .collect();
388 if let Some(deps) = manifest.dev_dependencies.as_ref() {
389 test_dependencies.extend(deps.iter().cloned());
390 }
391 test_dependencies
392 } else {
393 Vec::new()
394 };
395
396 for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
397 Self::graph_build(
398 &home_path,
399 network,
400 endpoint,
401 &first_dependency,
402 dependency,
403 &mut map,
404 &mut digraph,
405 no_cache,
406 no_local,
407 network_retries,
408 &declared_deps,
409 )?;
410 }
411
412 let ordered_dependency_symbols =
413 digraph.post_order().map_err(|_| crate::errors::circular_dependency_error())?;
414
415 (
416 ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect(),
417 digraph,
418 )
419 } else {
420 (Vec::new(), DiGraph::default())
421 };
422
423 Ok(Package { base_directory: path, workspace_root, compilation_units, manifest, dep_graph: digraph })
424 }
425
426 #[allow(clippy::too_many_arguments)]
427 fn graph_build(
428 home_path: &Path,
429 network: Option<NetworkName>,
430 endpoint: Option<&str>,
431 main_program: &Dependency,
432 new: Dependency,
433 map: &mut IndexMap<Symbol, (Dependency, CompilationUnit)>,
434 graph: &mut DiGraph<Symbol>,
435 no_cache: bool,
436 no_local: bool,
437 network_retries: u32,
438 declared_deps: &IndexMap<Symbol, Dependency>,
439 ) -> Result<()> {
440 let name_symbol = symbol(&new.name)?;
441
442 let unit = match map.entry(name_symbol) {
443 Entry::Occupied(occupied) => {
444 let existing_dep = &occupied.get().0;
447 assert_eq!(new.name, existing_dep.name);
448 if new.location != existing_dep.location
449 || new.path != existing_dep.path
450 || new.edition != existing_dep.edition
451 {
452 return Err(crate::errors::conflicting_dependency(existing_dep, new).into());
453 }
454 return Ok(());
455 }
456 Entry::Vacant(vacant) => {
457 let unit = match (new.path.as_ref(), new.location) {
458 (Some(path), Location::Local) if !no_local => {
459 if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
461 CompilationUnit::from_aleo_path(name_symbol, path, declared_deps)?
462 } else {
463 CompilationUnit::from_package_path(name_symbol, path)?
464 }
465 }
466 (Some(path), Location::Test) => {
467 CompilationUnit::from_test_path(path, main_program.clone())?
470 }
471 (_, Location::Network) | (Some(_), Location::Local) => {
472 let Some(endpoint) = endpoint else {
474 return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
475 };
476 let Some(network) = network else {
477 return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
478 };
479 CompilationUnit::fetch(
480 name_symbol,
481 new.edition,
482 home_path,
483 network,
484 endpoint,
485 no_cache,
486 network_retries,
487 )?
488 }
489 (_, Location::Workspace) => {
490 return Err(anyhow!(
491 "Workspace dependency `{}` was not resolved before graph building. This is a compiler bug.",
492 new.name
493 )
494 .into());
495 }
496 _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
497 };
498
499 vacant.insert((new, unit.clone()));
500
501 unit
502 }
503 };
504
505 graph.add_node(name_symbol);
506
507 for dependency in unit.dependencies.iter() {
508 let dependency_symbol = symbol(&dependency.name)?;
509 graph.add_edge(name_symbol, dependency_symbol);
510 Self::graph_build(
511 home_path,
512 network,
513 endpoint,
514 main_program,
515 dependency.clone(),
516 map,
517 graph,
518 no_cache,
519 no_local,
520 network_retries,
521 declared_deps,
522 )?;
523 }
524
525 Ok(())
526 }
527}
528
529fn main_template(name: &str) -> String {
530 format!(
531 r#"// The '{name}' program.
532program {name}.aleo {{
533 // This is the constructor for the program.
534 // The constructor allows you to manage program upgrades.
535 // It is called when the program is deployed or upgraded.
536 // It is currently configured to **prevent** upgrades.
537 // Other configurations include:
538 // - @admin(address="aleo1...")
539 // - @checksum(mapping="credits.aleo/fixme", key="0field")
540 // - @custom
541 // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
542 @noupgrade
543 constructor() {{}}
544
545 fn main(public a: u32, b: u32) -> u32 {{
546 let c: u32 = a + b;
547 return c;
548 }}
549}}
550"#
551 )
552}
553
554fn test_template(name: &str) -> String {
555 format!(
556 r#"// The 'test_{name}' test program.
557import {name}.aleo;
558program test_{name}.aleo {{
559 @test
560 @should_fail
561 fn test_main_fails() {{
562 let result: u32 = {name}.aleo::main(2u32, 3u32);
563 assert_eq(result, 3u32);
564 }}
565
566 @noupgrade
567 constructor() {{}}
568}}
569"#
570 )
571}
572
573fn lib_template(name: &str) -> String {
574 format!(
575 r#"// The '{name}' library.
576
577// Returns the identity of x.
578fn example(x: u32) -> u32 {{
579 return x;
580}}
581"#
582 )
583}
584
585fn lib_test_template(name: &str) -> String {
586 format!(
587 r#"// The 'test_{name}' test program.
588program test_{name}.aleo {{
589 @test
590 fn test_example() {{
591 assert_eq({name}::example(42u32), 42u32);
592 }}
593
594 @noupgrade
595 constructor() {{}}
596}}
597"#
598 )
599}
600
601fn collect_declared_deps(
608 root_path: &Path,
609 manifest: &Manifest,
610 with_tests: bool,
611) -> Result<IndexMap<Symbol, Dependency>> {
612 let mut declared = IndexMap::new();
613 collect_declared_deps_recursive(root_path, manifest, with_tests, &mut declared)?;
614 Ok(declared)
615}
616
617fn collect_declared_deps_recursive(
618 base_path: &Path,
619 manifest: &Manifest,
620 include_dev: bool,
621 declared: &mut IndexMap<Symbol, Dependency>,
622) -> Result<()> {
623 let deps = manifest.dependencies.iter().flatten();
624 let dev: Vec<&Dependency> =
625 if include_dev { manifest.dev_dependencies.iter().flatten().collect() } else { Vec::new() };
626 for dep in deps.chain(dev) {
627 let dep = canonicalize_dependency_path_relative_to(base_path, dep.clone())?;
628 let dep = if dep.location == Location::Workspace { resolve_workspace_dependency(base_path, dep)? } else { dep };
630 let sym = symbol(&dep.name)?;
631 let Entry::Vacant(e) = declared.entry(sym) else {
635 continue;
636 };
637 e.insert(dep.clone());
638 if dep.location == Location::Local
639 && let Some(path) = &dep.path
640 {
641 let manifest_path = path.join(MANIFEST_FILENAME);
642 if path.is_dir() && manifest_path.exists() {
643 let child = Manifest::read_from_file(manifest_path)?;
644 collect_declared_deps_recursive(path, &child, false, declared)?;
646 }
647 }
648 }
649 Ok(())
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 fn dummy_package(base: &str) -> Package {
657 dummy_package_with(base, None)
658 }
659
660 fn dummy_package_with(base: &str, workspace_root: Option<PathBuf>) -> Package {
661 Package {
662 base_directory: PathBuf::from(base),
663 workspace_root,
664 compilation_units: Vec::new(),
665 manifest: Manifest {
666 program: "demo.aleo".to_string(),
667 version: "0.1.0".to_string(),
668 description: String::new(),
669 license: "MIT".to_string(),
670 leo: "0.0.0".to_string(),
671 dependencies: None,
672 dev_dependencies: None,
673 },
674 dep_graph: DiGraph::default(),
675 }
676 }
677
678 #[test]
679 fn bare_unit_name_strips_aleo_suffix() {
680 assert_eq!(crate::bare_unit_name("token.aleo"), "token");
681 assert_eq!(crate::bare_unit_name("token"), "token");
682 assert_eq!(crate::bare_unit_name("credits.aleo"), "credits");
683 }
684
685 #[test]
686 fn unit_paths_are_keyed_by_bare_name() {
687 let pkg = dummy_package("/tmp/demo");
688 assert_eq!(pkg.unit_build_directory("token.aleo"), PathBuf::from("/tmp/demo/build/token"));
691 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/demo/build/token"));
692 assert_eq!(pkg.unit_bytecode_path("token.aleo"), PathBuf::from("/tmp/demo/build/token/token.aleo"));
693 assert_eq!(pkg.unit_abi_path("token"), PathBuf::from("/tmp/demo/build/token/abi.json"));
694 assert_eq!(pkg.unit_interfaces_directory("token"), PathBuf::from("/tmp/demo/build/token/interfaces"));
695 assert_eq!(pkg.unit_snapshots_directory("token"), PathBuf::from("/tmp/demo/build/token/snapshots"));
696 }
697
698 #[test]
699 fn libraries_are_keyed_like_programs() {
700 let pkg = dummy_package("/tmp/demo");
703 assert_eq!(pkg.unit_build_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib"));
704 assert_eq!(pkg.unit_interfaces_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib/interfaces"));
705 }
706
707 #[test]
708 fn build_directory_is_the_single_root() {
709 let pkg = dummy_package("/tmp/demo");
710 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/demo/build"));
711 assert!(pkg.unit_bytecode_path("x").starts_with(pkg.build_directory()));
713 assert!(pkg.unit_interfaces_directory("credits.aleo").starts_with(pkg.build_directory()));
714 }
715
716 #[test]
717 fn workspace_root_routes_build_directory_to_shared() {
718 let pkg = dummy_package_with("/tmp/ws/members/token", Some(PathBuf::from("/tmp/ws")));
723 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/ws/build"));
724 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/ws/build/token"));
725 assert_eq!(pkg.unit_bytecode_path("token"), PathBuf::from("/tmp/ws/build/token/token.aleo"));
726 let dep = dummy_package_with("/tmp/ws/members/swap", Some(PathBuf::from("/tmp/ws")));
730 assert_eq!(pkg.unit_bytecode_path("token"), dep.unit_bytecode_path("token"));
731 }
732
733 #[test]
734 fn standalone_package_keeps_per_base_build_directory() {
735 let pkg = dummy_package_with("/tmp/standalone", None);
738 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/standalone/build"));
739 assert_eq!(pkg.unit_build_directory("demo"), PathBuf::from("/tmp/standalone/build/demo"));
740 }
741}