1use crate::*;
18
19use leo_ast::DiGraph;
20use leo_errors::{CliError, PackageError, Result, UtilError};
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 outputs_directory(&self) -> PathBuf {
63 self.base_directory.join(OUTPUTS_DIRECTORY)
64 }
65
66 pub fn imports_directory(&self) -> PathBuf {
67 self.base_directory.join(IMPORTS_DIRECTORY)
68 }
69
70 pub fn build_directory(&self) -> PathBuf {
71 self.base_directory.join(BUILD_DIRECTORY)
72 }
73
74 pub fn source_directory(&self) -> PathBuf {
75 self.base_directory.join(SOURCE_DIRECTORY)
76 }
77
78 pub fn tests_directory(&self) -> PathBuf {
79 self.base_directory.join(TESTS_DIRECTORY)
80 }
81
82 pub fn initialize<P: AsRef<Path>>(package_name: &str, path: P, is_library: bool) -> Result<PathBuf> {
84 Self::initialize_impl(package_name, path.as_ref(), is_library)
85 }
86
87 fn initialize_impl(package_name: &str, path: &Path, is_library: bool) -> Result<PathBuf> {
88 let package_name = if is_library {
89 if !crate::is_valid_library_name(package_name) {
90 return Err(CliError::invalid_package_name("library", package_name).into());
91 }
92
93 package_name.to_string()
94 } else {
95 let program_name =
96 if package_name.ends_with(".aleo") { package_name.to_string() } else { format!("{package_name}.aleo") };
97
98 if !crate::is_valid_program_name(&program_name) {
99 return Err(CliError::invalid_package_name("program", &program_name).into());
100 }
101
102 program_name
103 };
104
105 let path = path.canonicalize().map_err(|e| PackageError::failed_path(path.display(), e))?;
106 let full_path = path.join(package_name.strip_suffix(".aleo").unwrap_or(&package_name));
107
108 if full_path.exists() {
110 return Err(
111 PackageError::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
112 );
113 }
114
115 std::fs::create_dir(&full_path)
117 .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
118
119 std::env::set_current_dir(&full_path)
121 .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
122
123 const GITIGNORE_TEMPLATE: &str = ".env\n*.avm\n*.prover\n*.verifier\noutputs/\n";
125 const GITIGNORE_FILENAME: &str = ".gitignore";
126
127 let gitignore_path = full_path.join(GITIGNORE_FILENAME);
128 std::fs::write(gitignore_path, GITIGNORE_TEMPLATE).map_err(PackageError::io_error_gitignore_file)?;
129
130 let manifest = Manifest {
132 program: package_name.clone(),
133 version: "0.1.0".to_string(),
134 description: String::new(),
135 license: "MIT".to_string(),
136 leo: env!("CARGO_PKG_VERSION").to_string(),
137 dependencies: None,
138 dev_dependencies: None,
139 };
140
141 let manifest_path = full_path.join(MANIFEST_FILENAME);
142 manifest.write_to_file(manifest_path)?;
143
144 let source_path = full_path.join(SOURCE_DIRECTORY);
146
147 std::fs::create_dir(&source_path)
148 .map_err(|e| PackageError::failed_to_create_source_directory(source_path.display(), e))?;
149
150 let name_no_aleo = package_name.strip_suffix(".aleo").unwrap_or(&package_name);
151
152 if is_library {
153 let lib_path = source_path.join("lib.leo");
155
156 std::fs::write(&lib_path, lib_template(name_no_aleo)).map_err(|e| {
157 UtilError::util_file_io_error(format_args!("Failed to write `{}`", lib_path.display()), e)
158 })?;
159
160 let tests_path = full_path.join(TESTS_DIRECTORY);
162
163 std::fs::create_dir(&tests_path)
164 .map_err(|e| PackageError::failed_to_create_source_directory(tests_path.display(), e))?;
165
166 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
167
168 std::fs::write(&test_file_path, lib_test_template(name_no_aleo)).map_err(|e| {
169 UtilError::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
170 })?;
171 } else {
172 let main_path = source_path.join(MAIN_FILENAME);
174
175 std::fs::write(&main_path, main_template(name_no_aleo)).map_err(|e| {
176 UtilError::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e)
177 })?;
178
179 let tests_path = full_path.join(TESTS_DIRECTORY);
181
182 std::fs::create_dir(&tests_path)
183 .map_err(|e| PackageError::failed_to_create_source_directory(tests_path.display(), e))?;
184
185 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
186
187 std::fs::write(&test_file_path, test_template(name_no_aleo)).map_err(|e| {
188 UtilError::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
189 })?;
190 }
191
192 Ok(full_path)
193 }
194
195 pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
199 path: P,
200 home_path: Q,
201 network: Option<NetworkName>,
202 endpoint: Option<&str>,
203 network_retries: u32,
204 ) -> Result<Self> {
205 Self::from_directory_impl(
206 path.as_ref(),
207 home_path.as_ref(),
208 false,
209 false,
210 false,
211 false,
212 network,
213 endpoint,
214 network_retries,
215 )
216 }
217
218 pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
221 path: P,
222 home_path: Q,
223 no_cache: bool,
224 no_local: bool,
225 network: Option<NetworkName>,
226 endpoint: Option<&str>,
227 network_retries: u32,
228 ) -> Result<Self> {
229 Self::from_directory_impl(
230 path.as_ref(),
231 home_path.as_ref(),
232 true,
233 false,
234 no_cache,
235 no_local,
236 network,
237 endpoint,
238 network_retries,
239 )
240 }
241
242 pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
245 path: P,
246 home_path: Q,
247 no_cache: bool,
248 no_local: bool,
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 true,
257 true,
258 no_cache,
259 no_local,
260 network,
261 endpoint,
262 network_retries,
263 )
264 }
265
266 pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
267 let path = self.tests_directory();
268 let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
271 data.into_iter()
272 }
273
274 pub fn import_files(&self) -> impl Iterator<Item = PathBuf> {
275 let path = self.imports_directory();
276 let data: Vec<PathBuf> = Self::files_with_extension(&path, "aleo").collect();
279 data.into_iter()
280 }
281
282 fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
283 path.read_dir()
284 .ok()
285 .into_iter()
286 .flatten()
287 .flat_map(|maybe_filename| maybe_filename.ok())
288 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
289 .flat_map(move |entry| {
290 let path = entry.path();
291 if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
292 })
293 }
294
295 #[allow(clippy::too_many_arguments)]
296 fn from_directory_impl(
297 path: &Path,
298 home_path: &Path,
299 build_graph: bool,
300 with_tests: bool,
301 no_cache: bool,
302 no_local: bool,
303 network: Option<NetworkName>,
304 endpoint: Option<&str>,
305 network_retries: u32,
306 ) -> Result<Self> {
307 let map_err = |path: &Path, err| {
308 UtilError::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
309 };
310
311 let path = path.canonicalize().map_err(|err| map_err(path, err))?;
312
313 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
314
315 let (compilation_units, digraph) = if build_graph {
316 let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
317
318 let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
319
320 let mut digraph = DiGraph::<Symbol>::new(Default::default());
321
322 let declared_deps = collect_declared_deps(&path, &manifest, with_tests)?;
325
326 let first_dependency = Dependency {
327 name: manifest.program.clone(),
328 location: Location::Local,
329 path: Some(path.clone()),
330 edition: None,
331 };
332
333 let test_dependencies: Vec<Dependency> = if with_tests {
334 let tests_directory = path.join(TESTS_DIRECTORY);
335 let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
336 .map(|path| Dependency {
337 name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
339 edition: None,
340 location: Location::Test,
341 path: Some(path.to_path_buf()),
342 })
343 .collect();
344 if let Some(deps) = manifest.dev_dependencies.as_ref() {
345 test_dependencies.extend(deps.iter().cloned());
346 }
347 test_dependencies
348 } else {
349 Vec::new()
350 };
351
352 for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
353 Self::graph_build(
354 &home_path,
355 network,
356 endpoint,
357 &first_dependency,
358 dependency,
359 &mut map,
360 &mut digraph,
361 no_cache,
362 no_local,
363 network_retries,
364 &declared_deps,
365 )?;
366 }
367
368 let ordered_dependency_symbols =
369 digraph.post_order().map_err(|_| UtilError::circular_dependency_error())?;
370
371 (
372 ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect(),
373 digraph,
374 )
375 } else {
376 (Vec::new(), DiGraph::default())
377 };
378
379 Ok(Package { base_directory: path, compilation_units, manifest, dep_graph: digraph })
380 }
381
382 #[allow(clippy::too_many_arguments)]
383 fn graph_build(
384 home_path: &Path,
385 network: Option<NetworkName>,
386 endpoint: Option<&str>,
387 main_program: &Dependency,
388 new: Dependency,
389 map: &mut IndexMap<Symbol, (Dependency, CompilationUnit)>,
390 graph: &mut DiGraph<Symbol>,
391 no_cache: bool,
392 no_local: bool,
393 network_retries: u32,
394 declared_deps: &IndexMap<Symbol, Dependency>,
395 ) -> Result<()> {
396 let name_symbol = symbol(&new.name)?;
397
398 let unit = match map.entry(name_symbol) {
399 Entry::Occupied(occupied) => {
400 let existing_dep = &occupied.get().0;
403 assert_eq!(new.name, existing_dep.name);
404 if new.location != existing_dep.location
405 || new.path != existing_dep.path
406 || new.edition != existing_dep.edition
407 {
408 return Err(PackageError::conflicting_dependency(existing_dep, new).into());
409 }
410 return Ok(());
411 }
412 Entry::Vacant(vacant) => {
413 let unit = match (new.path.as_ref(), new.location) {
414 (Some(path), Location::Local) if !no_local => {
415 if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
417 CompilationUnit::from_aleo_path(name_symbol, path, declared_deps)?
418 } else {
419 CompilationUnit::from_package_path(name_symbol, path)?
420 }
421 }
422 (Some(path), Location::Test) => {
423 CompilationUnit::from_test_path(path, main_program.clone())?
426 }
427 (_, Location::Network) | (Some(_), Location::Local) => {
428 let Some(endpoint) = endpoint else {
430 return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
431 };
432 let Some(network) = network else {
433 return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
434 };
435 CompilationUnit::fetch(
436 name_symbol,
437 new.edition,
438 home_path,
439 network,
440 endpoint,
441 no_cache,
442 network_retries,
443 )?
444 }
445 _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
446 };
447
448 vacant.insert((new, unit.clone()));
449
450 unit
451 }
452 };
453
454 graph.add_node(name_symbol);
455
456 for dependency in unit.dependencies.iter() {
457 let dependency_symbol = symbol(&dependency.name)?;
458 graph.add_edge(name_symbol, dependency_symbol);
459 Self::graph_build(
460 home_path,
461 network,
462 endpoint,
463 main_program,
464 dependency.clone(),
465 map,
466 graph,
467 no_cache,
468 no_local,
469 network_retries,
470 declared_deps,
471 )?;
472 }
473
474 Ok(())
475 }
476}
477
478fn main_template(name: &str) -> String {
479 format!(
480 r#"// The '{name}' program.
481program {name}.aleo {{
482 // This is the constructor for the program.
483 // The constructor allows you to manage program upgrades.
484 // It is called when the program is deployed or upgraded.
485 // It is currently configured to **prevent** upgrades.
486 // Other configurations include:
487 // - @admin(address="aleo1...")
488 // - @checksum(mapping="credits.aleo/fixme", key="0field")
489 // - @custom
490 // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
491 @noupgrade
492 constructor() {{}}
493
494 fn main(public a: u32, b: u32) -> u32 {{
495 let c: u32 = a + b;
496 return c;
497 }}
498}}
499"#
500 )
501}
502
503fn test_template(name: &str) -> String {
504 format!(
505 r#"// The 'test_{name}' test program.
506import {name}.aleo;
507program test_{name}.aleo {{
508 @test
509 @should_fail
510 fn test_main_fails() {{
511 let result: u32 = {name}.aleo::main(2u32, 3u32);
512 assert_eq(result, 3u32);
513 }}
514
515 @noupgrade
516 constructor() {{}}
517}}
518"#
519 )
520}
521
522fn lib_template(name: &str) -> String {
523 format!(
524 r#"// The '{name}' library.
525
526// Returns the identity of x.
527fn example(x: u32) -> u32 {{
528 return x;
529}}
530"#
531 )
532}
533
534fn lib_test_template(name: &str) -> String {
535 format!(
536 r#"// The 'test_{name}' test program.
537program test_{name}.aleo {{
538 @test
539 fn test_example() {{
540 assert_eq({name}::example(42u32), 42u32);
541 }}
542
543 @noupgrade
544 constructor() {{}}
545}}
546"#
547 )
548}
549
550fn collect_declared_deps(
557 root_path: &Path,
558 manifest: &Manifest,
559 with_tests: bool,
560) -> Result<IndexMap<Symbol, Dependency>> {
561 let mut declared = IndexMap::new();
562 collect_declared_deps_recursive(root_path, manifest, with_tests, &mut declared)?;
563 Ok(declared)
564}
565
566fn collect_declared_deps_recursive(
567 base_path: &Path,
568 manifest: &Manifest,
569 include_dev: bool,
570 declared: &mut IndexMap<Symbol, Dependency>,
571) -> Result<()> {
572 let deps = manifest.dependencies.iter().flatten();
573 let dev: Vec<&Dependency> =
574 if include_dev { manifest.dev_dependencies.iter().flatten().collect() } else { Vec::new() };
575 for dep in deps.chain(dev) {
576 let dep = canonicalize_dependency_path_relative_to(base_path, dep.clone())?;
577 let sym = symbol(&dep.name)?;
578 let Entry::Vacant(e) = declared.entry(sym) else {
582 continue;
583 };
584 e.insert(dep.clone());
585 if dep.location == Location::Local
586 && let Some(path) = &dep.path
587 {
588 let manifest_path = path.join(MANIFEST_FILENAME);
589 if path.is_dir() && manifest_path.exists() {
590 let child = Manifest::read_from_file(manifest_path)?;
591 collect_declared_deps_recursive(path, &child, false, declared)?;
593 }
594 }
595 }
596 Ok(())
597}