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 programs: Vec<Program>,
53
54 pub manifest: Manifest,
56}
57
58impl Package {
59 pub fn outputs_directory(&self) -> PathBuf {
60 self.base_directory.join(OUTPUTS_DIRECTORY)
61 }
62
63 pub fn imports_directory(&self) -> PathBuf {
64 self.base_directory.join(IMPORTS_DIRECTORY)
65 }
66
67 pub fn build_directory(&self) -> PathBuf {
68 self.base_directory.join(BUILD_DIRECTORY)
69 }
70
71 pub fn source_directory(&self) -> PathBuf {
72 self.base_directory.join(SOURCE_DIRECTORY)
73 }
74
75 pub fn tests_directory(&self) -> PathBuf {
76 self.base_directory.join(TESTS_DIRECTORY)
77 }
78
79 pub fn initialize<P: AsRef<Path>>(package_name: &str, path: P) -> Result<PathBuf> {
81 Self::initialize_impl(package_name, path.as_ref())
82 }
83
84 fn initialize_impl(package_name: &str, path: &Path) -> Result<PathBuf> {
85 let package_name =
86 if package_name.ends_with(".aleo") { package_name.to_string() } else { format!("{package_name}.aleo") };
87
88 if !crate::is_valid_aleo_name(&package_name) {
89 return Err(CliError::invalid_program_name(package_name).into());
90 }
91
92 let path = path.canonicalize().map_err(|e| PackageError::failed_path(path.display(), e))?;
93 let full_path = path.join(package_name.strip_suffix(".aleo").unwrap());
94
95 if full_path.exists() {
97 return Err(
98 PackageError::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
99 );
100 }
101
102 std::fs::create_dir(&full_path)
104 .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
105
106 std::env::set_current_dir(&full_path)
108 .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
109
110 const GITIGNORE_TEMPLATE: &str = ".env\n*.avm\n*.prover\n*.verifier\noutputs/\n";
112
113 const GITIGNORE_FILENAME: &str = ".gitignore";
114
115 let gitignore_path = full_path.join(GITIGNORE_FILENAME);
116
117 std::fs::write(gitignore_path, GITIGNORE_TEMPLATE).map_err(PackageError::io_error_gitignore_file)?;
118
119 let manifest = Manifest {
121 program: package_name.clone(),
122 version: "0.1.0".to_string(),
123 description: String::new(),
124 license: "MIT".to_string(),
125 leo: env!("CARGO_PKG_VERSION").to_string(),
126 dependencies: None,
127 dev_dependencies: None,
128 };
129
130 let manifest_path = full_path.join(MANIFEST_FILENAME);
131
132 manifest.write_to_file(manifest_path)?;
133
134 let source_path = full_path.join(SOURCE_DIRECTORY);
136
137 std::fs::create_dir(&source_path)
138 .map_err(|e| PackageError::failed_to_create_source_directory(source_path.display(), e))?;
139
140 let main_path = source_path.join(MAIN_FILENAME);
142
143 let name_no_aleo = package_name.strip_suffix(".aleo").unwrap();
144
145 std::fs::write(&main_path, main_template(name_no_aleo))
146 .map_err(|e| UtilError::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e))?;
147
148 let tests_path = full_path.join(TESTS_DIRECTORY);
150
151 std::fs::create_dir(&tests_path)
152 .map_err(|e| PackageError::failed_to_create_source_directory(tests_path.display(), e))?;
153
154 let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
155 std::fs::write(&test_file_path, test_template(name_no_aleo)).map_err(|e| {
156 UtilError::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
157 })?;
158
159 Ok(full_path)
160 }
161
162 pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
166 path: P,
167 home_path: Q,
168 network: Option<NetworkName>,
169 endpoint: Option<&str>,
170 ) -> Result<Self> {
171 Self::from_directory_impl(
172 path.as_ref(),
173 home_path.as_ref(),
174 false,
175 false,
176 false,
177 false,
178 network,
179 endpoint,
180 )
181 }
182
183 pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
186 path: P,
187 home_path: Q,
188 no_cache: bool,
189 no_local: bool,
190 network: Option<NetworkName>,
191 endpoint: Option<&str>,
192 ) -> Result<Self> {
193 Self::from_directory_impl(
194 path.as_ref(),
195 home_path.as_ref(),
196 true,
197 false,
198 no_cache,
199 no_local,
200 network,
201 endpoint,
202 )
203 }
204
205 pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
208 path: P,
209 home_path: Q,
210 no_cache: bool,
211 no_local: bool,
212 network: Option<NetworkName>,
213 endpoint: Option<&str>,
214 ) -> Result<Self> {
215 Self::from_directory_impl(
216 path.as_ref(),
217 home_path.as_ref(),
218 true,
219 true,
220 no_cache,
221 no_local,
222 network,
223 endpoint,
224 )
225 }
226
227 pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
228 let path = self.tests_directory();
229 let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
232 data.into_iter()
233 }
234
235 pub fn import_files(&self) -> impl Iterator<Item = PathBuf> {
236 let path = self.imports_directory();
237 let data: Vec<PathBuf> = Self::files_with_extension(&path, "aleo").collect();
240 data.into_iter()
241 }
242
243 fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
244 path.read_dir()
245 .ok()
246 .into_iter()
247 .flatten()
248 .flat_map(|maybe_filename| maybe_filename.ok())
249 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
250 .flat_map(move |entry| {
251 let path = entry.path();
252 if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
253 })
254 }
255
256 #[allow(clippy::too_many_arguments)]
257 fn from_directory_impl(
258 path: &Path,
259 home_path: &Path,
260 build_graph: bool,
261 with_tests: bool,
262 no_cache: bool,
263 no_local: bool,
264 network: Option<NetworkName>,
265 endpoint: Option<&str>,
266 ) -> Result<Self> {
267 let map_err = |path: &Path, err| {
268 UtilError::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
269 };
270
271 let path = path.canonicalize().map_err(|err| map_err(path, err))?;
272
273 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
274
275 let programs: Vec<Program> = if build_graph {
276 let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
277
278 let mut map: IndexMap<Symbol, (Dependency, Program)> = IndexMap::new();
279
280 let mut digraph = DiGraph::<Symbol>::new(Default::default());
281
282 let first_dependency = Dependency {
283 name: manifest.program.clone(),
284 location: Location::Local,
285 path: Some(path.clone()),
286 edition: None,
287 };
288
289 let test_dependencies: Vec<Dependency> = if with_tests {
290 let tests_directory = path.join(TESTS_DIRECTORY);
291 let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
292 .map(|path| Dependency {
293 name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
295 edition: None,
296 location: Location::Test,
297 path: Some(path.to_path_buf()),
298 })
299 .collect();
300 if let Some(deps) = manifest.dev_dependencies.as_ref() {
301 test_dependencies.extend(deps.iter().cloned());
302 }
303 test_dependencies
304 } else {
305 Vec::new()
306 };
307
308 for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
309 Self::graph_build(
310 &home_path,
311 network,
312 endpoint,
313 &first_dependency,
314 dependency,
315 &mut map,
316 &mut digraph,
317 no_cache,
318 no_local,
319 )?;
320 }
321
322 let ordered_dependency_symbols =
323 digraph.post_order().map_err(|_| UtilError::circular_dependency_error())?;
324
325 ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect()
326 } else {
327 Vec::new()
328 };
329
330 Ok(Package { base_directory: path, programs, manifest })
331 }
332
333 #[allow(clippy::too_many_arguments)]
334 fn graph_build(
335 home_path: &Path,
336 network: Option<NetworkName>,
337 endpoint: Option<&str>,
338 main_program: &Dependency,
339 new: Dependency,
340 map: &mut IndexMap<Symbol, (Dependency, Program)>,
341 graph: &mut DiGraph<Symbol>,
342 no_cache: bool,
343 no_local: bool,
344 ) -> Result<()> {
345 let name_symbol = symbol(&new.name)?;
346
347 let dependencies = map.clone().into_iter().map(|(name, (dep, _))| (name, dep)).collect();
349
350 let program = match map.entry(name_symbol) {
351 Entry::Occupied(occupied) => {
352 let existing_dep = &occupied.get().0;
355 assert_eq!(new.name, existing_dep.name);
356 if new.location != existing_dep.location
357 || new.path != existing_dep.path
358 || new.edition != existing_dep.edition
359 {
360 return Err(PackageError::conflicting_dependency(existing_dep, new).into());
361 }
362 return Ok(());
363 }
364 Entry::Vacant(vacant) => {
365 let program = match (new.path.as_ref(), new.location) {
366 (Some(path), Location::Local) if !no_local => {
367 if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
369 Program::from_aleo_path(name_symbol, path, &dependencies)?
370 } else {
371 Program::from_package_path(name_symbol, path)?
372 }
373 }
374 (Some(path), Location::Test) => {
375 Program::from_test_path(path, main_program.clone())?
378 }
379 (_, Location::Network) | (Some(_), Location::Local) => {
380 let Some(endpoint) = endpoint else {
382 return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
383 };
384 let Some(network) = network else {
385 return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
386 };
387 Program::fetch(name_symbol, new.edition, home_path, network, endpoint, no_cache)?
388 }
389 _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
390 };
391
392 vacant.insert((new, program.clone()));
393
394 program
395 }
396 };
397
398 graph.add_node(name_symbol);
399
400 for dependency in program.dependencies.iter() {
401 let dependency_symbol = symbol(&dependency.name)?;
402 graph.add_edge(name_symbol, dependency_symbol);
403 Self::graph_build(
404 home_path,
405 network,
406 endpoint,
407 main_program,
408 dependency.clone(),
409 map,
410 graph,
411 no_cache,
412 no_local,
413 )?;
414 }
415
416 Ok(())
417 }
418}
419
420fn main_template(name: &str) -> String {
421 format!(
422 r#"// The '{name}' program.
423program {name}.aleo {{
424 // This is the constructor for the program.
425 // The constructor allows you to manage program upgrades.
426 // It is called when the program is deployed or upgraded.
427 // It is currently configured to **prevent** upgrades.
428 // Other configurations include:
429 // - @admin(address="aleo1...")
430 // - @checksum(mapping="credits.aleo/fixme", key="0field")
431 // - @custom
432 // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
433 @noupgrade
434 async constructor() {{}}
435
436 transition main(public a: u32, b: u32) -> u32 {{
437 let c: u32 = a + b;
438 return c;
439 }}
440}}
441"#
442 )
443}
444
445fn test_template(name: &str) -> String {
446 format!(
447 r#"// The 'test_{name}' test program.
448import {name}.aleo;
449program test_{name}.aleo {{
450 @test
451 script test_it() {{
452 let result: u32 = {name}.aleo/main(1u32, 2u32);
453 assert_eq(result, 3u32);
454 }}
455
456 @test
457 @should_fail
458 transition do_nothing() {{
459 let result: u32 = {name}.aleo/main(2u32, 3u32);
460 assert_eq(result, 3u32);
461 }}
462
463 @noupgrade
464 async constructor() {{}}
465}}
466"#
467 )
468}