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 compilation_units: Vec<CompilationUnit>,
53
54 pub manifest: Manifest,
56
57 pub dep_graph: DiGraph<Symbol>,
59}
60
61impl Package {
62 pub fn build_directory(&self) -> PathBuf {
67 self.base_directory.join(BUILD_DIRECTORY)
68 }
69
70 pub fn primary_unit(&self) -> Option<&CompilationUnit> {
73 let primary = bare_unit_name(&self.manifest.program);
74 self.compilation_units.iter().find(|u| !u.kind.is_test() && bare_unit_name(&u.name.to_string()) == primary)
75 }
76
77 pub fn unit_build_directory(&self, name: &str) -> PathBuf {
81 self.build_directory().join(bare_unit_name(name))
82 }
83
84 pub fn unit_bytecode_path(&self, name: &str) -> PathBuf {
87 let bare = bare_unit_name(name);
88 self.unit_build_directory(name).join(format!("{bare}.aleo"))
89 }
90
91 pub fn unit_abi_path(&self, name: &str) -> PathBuf {
93 self.unit_build_directory(name).join(ABI_FILENAME)
94 }
95
96 pub fn unit_interfaces_directory(&self, name: &str) -> PathBuf {
99 self.unit_build_directory(name).join(INTERFACES_DIRNAME)
100 }
101
102 pub fn unit_snapshots_directory(&self, name: &str) -> PathBuf {
106 self.unit_build_directory(name).join(SNAPSHOTS_DIRNAME)
107 }
108
109 pub fn source_directory(&self) -> PathBuf {
110 self.base_directory.join(SOURCE_DIRECTORY)
111 }
112
113 pub fn tests_directory(&self) -> PathBuf {
114 self.base_directory.join(TESTS_DIRECTORY)
115 }
116
117 pub fn initialize<P: AsRef<Path>>(package_name: &str, path: P, is_library: bool) -> Result<PathBuf> {
119 Self::initialize_impl(package_name, path.as_ref(), is_library)
120 }
121
122 fn initialize_impl(package_name: &str, path: &Path, is_library: bool) -> Result<PathBuf> {
123 let package_name = if is_library {
124 if !crate::is_valid_library_name(package_name) {
125 return Err(crate::errors::cli_invalid_package_name("library", package_name).into());
126 }
127
128 package_name.to_string()
129 } else {
130 let program_name =
131 if package_name.ends_with(".aleo") { package_name.to_string() } else { format!("{package_name}.aleo") };
132
133 if !crate::is_valid_program_name(&program_name) {
134 return Err(crate::errors::cli_invalid_package_name("program", &program_name).into());
135 }
136
137 program_name
138 };
139
140 let path = path.canonicalize().map_err(|e| crate::errors::failed_path(path.display(), e))?;
141 let full_path = path.join(package_name.strip_suffix(".aleo").unwrap_or(&package_name));
142
143 if full_path.exists() {
145 return Err(
146 crate::errors::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
147 );
148 }
149
150 std::fs::create_dir(&full_path)
152 .map_err(|e| crate::errors::failed_to_initialize_package(&package_name, &full_path, e))?;
153
154 std::env::set_current_dir(&full_path)
156 .map_err(|e| crate::errors::failed_to_initialize_package(&package_name, &full_path, e))?;
157
158 const GITIGNORE_TEMPLATE: &str = ".env\n*.avm\n*.prover\n*.verifier\nbuild/\n";
160 const GITIGNORE_FILENAME: &str = ".gitignore";
161
162 let gitignore_path = full_path.join(GITIGNORE_FILENAME);
163 std::fs::write(gitignore_path, GITIGNORE_TEMPLATE).map_err(crate::errors::io_error_gitignore_file)?;
164
165 let manifest = Manifest {
167 program: package_name.clone(),
168 version: "0.1.0".to_string(),
169 description: String::new(),
170 license: "MIT".to_string(),
171 leo: env!("CARGO_PKG_VERSION").to_string(),
172 dependencies: None,
173 dev_dependencies: None,
174 };
175
176 let manifest_path = full_path.join(MANIFEST_FILENAME);
177 manifest.write_to_file(manifest_path)?;
178
179 let source_path = full_path.join(SOURCE_DIRECTORY);
181
182 std::fs::create_dir(&source_path)
183 .map_err(|e| crate::errors::failed_to_create_source_directory(source_path.display(), e))?;
184
185 let name_no_aleo = package_name.strip_suffix(".aleo").unwrap_or(&package_name);
186
187 if is_library {
188 let lib_path = source_path.join("lib.leo");
190
191 std::fs::write(&lib_path, lib_template(name_no_aleo)).map_err(|e| {
192 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", lib_path.display()), e)
193 })?;
194
195 let tests_path = full_path.join(TESTS_DIRECTORY);
197
198 std::fs::create_dir(&tests_path)
199 .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
200
201 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
202
203 std::fs::write(&test_file_path, lib_test_template(name_no_aleo)).map_err(|e| {
204 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
205 })?;
206 } else {
207 let main_path = source_path.join(MAIN_FILENAME);
209
210 std::fs::write(&main_path, main_template(name_no_aleo)).map_err(|e| {
211 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e)
212 })?;
213
214 let tests_path = full_path.join(TESTS_DIRECTORY);
216
217 std::fs::create_dir(&tests_path)
218 .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
219
220 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
221
222 std::fs::write(&test_file_path, test_template(name_no_aleo)).map_err(|e| {
223 crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
224 })?;
225 }
226
227 Ok(full_path)
228 }
229
230 pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
234 path: P,
235 home_path: Q,
236 network: Option<NetworkName>,
237 endpoint: Option<&str>,
238 network_retries: u32,
239 ) -> Result<Self> {
240 Self::from_directory_impl(
241 path.as_ref(),
242 home_path.as_ref(),
243 false,
244 false,
245 false,
246 false,
247 network,
248 endpoint,
249 network_retries,
250 )
251 }
252
253 pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
256 path: P,
257 home_path: Q,
258 no_cache: bool,
259 no_local: bool,
260 network: Option<NetworkName>,
261 endpoint: Option<&str>,
262 network_retries: u32,
263 ) -> Result<Self> {
264 Self::from_directory_impl(
265 path.as_ref(),
266 home_path.as_ref(),
267 true,
268 false,
269 no_cache,
270 no_local,
271 network,
272 endpoint,
273 network_retries,
274 )
275 }
276
277 pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
280 path: P,
281 home_path: Q,
282 no_cache: bool,
283 no_local: bool,
284 network: Option<NetworkName>,
285 endpoint: Option<&str>,
286 network_retries: u32,
287 ) -> Result<Self> {
288 Self::from_directory_impl(
289 path.as_ref(),
290 home_path.as_ref(),
291 true,
292 true,
293 no_cache,
294 no_local,
295 network,
296 endpoint,
297 network_retries,
298 )
299 }
300
301 pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
302 let path = self.tests_directory();
303 let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
306 data.into_iter()
307 }
308
309 fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
310 path.read_dir()
311 .ok()
312 .into_iter()
313 .flatten()
314 .flat_map(|maybe_filename| maybe_filename.ok())
315 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
316 .flat_map(move |entry| {
317 let path = entry.path();
318 if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
319 })
320 }
321
322 #[allow(clippy::too_many_arguments)]
323 fn from_directory_impl(
324 path: &Path,
325 home_path: &Path,
326 build_graph: bool,
327 with_tests: bool,
328 no_cache: bool,
329 no_local: bool,
330 network: Option<NetworkName>,
331 endpoint: Option<&str>,
332 network_retries: u32,
333 ) -> Result<Self> {
334 let map_err = |path: &Path, err| {
335 crate::errors::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
336 };
337
338 let path = path.canonicalize().map_err(|err| map_err(path, err))?;
339
340 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
341
342 let (compilation_units, digraph) = if build_graph {
343 let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
344
345 let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
346
347 let mut digraph = DiGraph::<Symbol>::new(Default::default());
348
349 let declared_deps = collect_declared_deps(&path, &manifest, with_tests)?;
352
353 let first_dependency = Dependency {
354 name: manifest.program.clone(),
355 location: Location::Local,
356 path: Some(path.clone()),
357 edition: None,
358 };
359
360 let test_dependencies: Vec<Dependency> = if with_tests {
361 let tests_directory = path.join(TESTS_DIRECTORY);
362 let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
363 .map(|path| Dependency {
364 name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
366 edition: None,
367 location: Location::Test,
368 path: Some(path.to_path_buf()),
369 })
370 .collect();
371 if let Some(deps) = manifest.dev_dependencies.as_ref() {
372 test_dependencies.extend(deps.iter().cloned());
373 }
374 test_dependencies
375 } else {
376 Vec::new()
377 };
378
379 for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
380 Self::graph_build(
381 &home_path,
382 network,
383 endpoint,
384 &first_dependency,
385 dependency,
386 &mut map,
387 &mut digraph,
388 no_cache,
389 no_local,
390 network_retries,
391 &declared_deps,
392 )?;
393 }
394
395 let ordered_dependency_symbols =
396 digraph.post_order().map_err(|_| crate::errors::circular_dependency_error())?;
397
398 (
399 ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect(),
400 digraph,
401 )
402 } else {
403 (Vec::new(), DiGraph::default())
404 };
405
406 Ok(Package { base_directory: path, compilation_units, manifest, dep_graph: digraph })
407 }
408
409 #[allow(clippy::too_many_arguments)]
410 fn graph_build(
411 home_path: &Path,
412 network: Option<NetworkName>,
413 endpoint: Option<&str>,
414 main_program: &Dependency,
415 new: Dependency,
416 map: &mut IndexMap<Symbol, (Dependency, CompilationUnit)>,
417 graph: &mut DiGraph<Symbol>,
418 no_cache: bool,
419 no_local: bool,
420 network_retries: u32,
421 declared_deps: &IndexMap<Symbol, Dependency>,
422 ) -> Result<()> {
423 let name_symbol = symbol(&new.name)?;
424
425 let unit = match map.entry(name_symbol) {
426 Entry::Occupied(occupied) => {
427 let existing_dep = &occupied.get().0;
430 assert_eq!(new.name, existing_dep.name);
431 if new.location != existing_dep.location
432 || new.path != existing_dep.path
433 || new.edition != existing_dep.edition
434 {
435 return Err(crate::errors::conflicting_dependency(existing_dep, new).into());
436 }
437 return Ok(());
438 }
439 Entry::Vacant(vacant) => {
440 let unit = match (new.path.as_ref(), new.location) {
441 (Some(path), Location::Local) if !no_local => {
442 if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
444 CompilationUnit::from_aleo_path(name_symbol, path, declared_deps)?
445 } else {
446 CompilationUnit::from_package_path(name_symbol, path)?
447 }
448 }
449 (Some(path), Location::Test) => {
450 CompilationUnit::from_test_path(path, main_program.clone())?
453 }
454 (_, Location::Network) | (Some(_), Location::Local) => {
455 let Some(endpoint) = endpoint else {
457 return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
458 };
459 let Some(network) = network else {
460 return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
461 };
462 CompilationUnit::fetch(
463 name_symbol,
464 new.edition,
465 home_path,
466 network,
467 endpoint,
468 no_cache,
469 network_retries,
470 )?
471 }
472 (_, Location::Workspace) => {
473 return Err(anyhow!(
474 "Workspace dependency `{}` was not resolved before graph building. This is a compiler bug.",
475 new.name
476 )
477 .into());
478 }
479 _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
480 };
481
482 vacant.insert((new, unit.clone()));
483
484 unit
485 }
486 };
487
488 graph.add_node(name_symbol);
489
490 for dependency in unit.dependencies.iter() {
491 let dependency_symbol = symbol(&dependency.name)?;
492 graph.add_edge(name_symbol, dependency_symbol);
493 Self::graph_build(
494 home_path,
495 network,
496 endpoint,
497 main_program,
498 dependency.clone(),
499 map,
500 graph,
501 no_cache,
502 no_local,
503 network_retries,
504 declared_deps,
505 )?;
506 }
507
508 Ok(())
509 }
510}
511
512fn main_template(name: &str) -> String {
513 format!(
514 r#"// The '{name}' program.
515program {name}.aleo {{
516 // This is the constructor for the program.
517 // The constructor allows you to manage program upgrades.
518 // It is called when the program is deployed or upgraded.
519 // It is currently configured to **prevent** upgrades.
520 // Other configurations include:
521 // - @admin(address="aleo1...")
522 // - @checksum(mapping="credits.aleo/fixme", key="0field")
523 // - @custom
524 // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
525 @noupgrade
526 constructor() {{}}
527
528 fn main(public a: u32, b: u32) -> u32 {{
529 let c: u32 = a + b;
530 return c;
531 }}
532}}
533"#
534 )
535}
536
537fn test_template(name: &str) -> String {
538 format!(
539 r#"// The 'test_{name}' test program.
540import {name}.aleo;
541program test_{name}.aleo {{
542 @test
543 @should_fail
544 fn test_main_fails() {{
545 let result: u32 = {name}.aleo::main(2u32, 3u32);
546 assert_eq(result, 3u32);
547 }}
548
549 @noupgrade
550 constructor() {{}}
551}}
552"#
553 )
554}
555
556fn lib_template(name: &str) -> String {
557 format!(
558 r#"// The '{name}' library.
559
560// Returns the identity of x.
561fn example(x: u32) -> u32 {{
562 return x;
563}}
564"#
565 )
566}
567
568fn lib_test_template(name: &str) -> String {
569 format!(
570 r#"// The 'test_{name}' test program.
571program test_{name}.aleo {{
572 @test
573 fn test_example() {{
574 assert_eq({name}::example(42u32), 42u32);
575 }}
576
577 @noupgrade
578 constructor() {{}}
579}}
580"#
581 )
582}
583
584fn collect_declared_deps(
591 root_path: &Path,
592 manifest: &Manifest,
593 with_tests: bool,
594) -> Result<IndexMap<Symbol, Dependency>> {
595 let mut declared = IndexMap::new();
596 collect_declared_deps_recursive(root_path, manifest, with_tests, &mut declared)?;
597 Ok(declared)
598}
599
600fn collect_declared_deps_recursive(
601 base_path: &Path,
602 manifest: &Manifest,
603 include_dev: bool,
604 declared: &mut IndexMap<Symbol, Dependency>,
605) -> Result<()> {
606 let deps = manifest.dependencies.iter().flatten();
607 let dev: Vec<&Dependency> =
608 if include_dev { manifest.dev_dependencies.iter().flatten().collect() } else { Vec::new() };
609 for dep in deps.chain(dev) {
610 let dep = canonicalize_dependency_path_relative_to(base_path, dep.clone())?;
611 let dep = if dep.location == Location::Workspace { resolve_workspace_dependency(base_path, dep)? } else { dep };
613 let sym = symbol(&dep.name)?;
614 let Entry::Vacant(e) = declared.entry(sym) else {
618 continue;
619 };
620 e.insert(dep.clone());
621 if dep.location == Location::Local
622 && let Some(path) = &dep.path
623 {
624 let manifest_path = path.join(MANIFEST_FILENAME);
625 if path.is_dir() && manifest_path.exists() {
626 let child = Manifest::read_from_file(manifest_path)?;
627 collect_declared_deps_recursive(path, &child, false, declared)?;
629 }
630 }
631 }
632 Ok(())
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 fn dummy_package(base: &str) -> Package {
640 Package {
641 base_directory: PathBuf::from(base),
642 compilation_units: Vec::new(),
643 manifest: Manifest {
644 program: "demo.aleo".to_string(),
645 version: "0.1.0".to_string(),
646 description: String::new(),
647 license: "MIT".to_string(),
648 leo: "0.0.0".to_string(),
649 dependencies: None,
650 dev_dependencies: None,
651 },
652 dep_graph: DiGraph::default(),
653 }
654 }
655
656 #[test]
657 fn bare_unit_name_strips_aleo_suffix() {
658 assert_eq!(crate::bare_unit_name("token.aleo"), "token");
659 assert_eq!(crate::bare_unit_name("token"), "token");
660 assert_eq!(crate::bare_unit_name("credits.aleo"), "credits");
661 }
662
663 #[test]
664 fn unit_paths_are_keyed_by_bare_name() {
665 let pkg = dummy_package("/tmp/demo");
666 assert_eq!(pkg.unit_build_directory("token.aleo"), PathBuf::from("/tmp/demo/build/token"));
669 assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/demo/build/token"));
670 assert_eq!(pkg.unit_bytecode_path("token.aleo"), PathBuf::from("/tmp/demo/build/token/token.aleo"));
671 assert_eq!(pkg.unit_abi_path("token"), PathBuf::from("/tmp/demo/build/token/abi.json"));
672 assert_eq!(pkg.unit_interfaces_directory("token"), PathBuf::from("/tmp/demo/build/token/interfaces"));
673 assert_eq!(pkg.unit_snapshots_directory("token"), PathBuf::from("/tmp/demo/build/token/snapshots"));
674 }
675
676 #[test]
677 fn libraries_are_keyed_like_programs() {
678 let pkg = dummy_package("/tmp/demo");
681 assert_eq!(pkg.unit_build_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib"));
682 assert_eq!(pkg.unit_interfaces_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib/interfaces"));
683 }
684
685 #[test]
686 fn build_directory_is_the_single_root() {
687 let pkg = dummy_package("/tmp/demo");
688 assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/demo/build"));
689 assert!(pkg.unit_bytecode_path("x").starts_with(pkg.build_directory()));
691 assert!(pkg.unit_interfaces_directory("credits.aleo").starts_with(pkg.build_directory()));
692 }
693}