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))
156 .map_err(|e| UtilError::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e))?;
157
158 Ok(full_path)
159 }
160
161 pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
165 path: P,
166 home_path: Q,
167 network: Option<NetworkName>,
168 endpoint: Option<&str>,
169 ) -> Result<Self> {
170 Self::from_directory_impl(
171 path.as_ref(),
172 home_path.as_ref(),
173 false,
174 false,
175 false,
176 false,
177 network,
178 endpoint,
179 )
180 }
181
182 pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
185 path: P,
186 home_path: Q,
187 no_cache: bool,
188 no_local: bool,
189 network: Option<NetworkName>,
190 endpoint: Option<&str>,
191 ) -> Result<Self> {
192 Self::from_directory_impl(
193 path.as_ref(),
194 home_path.as_ref(),
195 true,
196 false,
197 no_cache,
198 no_local,
199 network,
200 endpoint,
201 )
202 }
203
204 pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
207 path: P,
208 home_path: Q,
209 no_cache: bool,
210 no_local: bool,
211 network: Option<NetworkName>,
212 endpoint: Option<&str>,
213 ) -> Result<Self> {
214 Self::from_directory_impl(
215 path.as_ref(),
216 home_path.as_ref(),
217 true,
218 true,
219 no_cache,
220 no_local,
221 network,
222 endpoint,
223 )
224 }
225
226 pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
227 let path = self.tests_directory();
228 let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
231 data.into_iter()
232 }
233
234 pub fn import_files(&self) -> impl Iterator<Item = PathBuf> {
235 let path = self.imports_directory();
236 let data: Vec<PathBuf> = Self::files_with_extension(&path, "aleo").collect();
239 data.into_iter()
240 }
241
242 fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
243 path.read_dir()
244 .ok()
245 .into_iter()
246 .flatten()
247 .flat_map(|maybe_filename| maybe_filename.ok())
248 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
249 .flat_map(move |entry| {
250 let path = entry.path();
251 if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
252 })
253 }
254
255 #[allow(clippy::too_many_arguments)]
256 fn from_directory_impl(
257 path: &Path,
258 home_path: &Path,
259 build_graph: bool,
260 with_tests: bool,
261 no_cache: bool,
262 no_local: bool,
263 network: Option<NetworkName>,
264 endpoint: Option<&str>,
265 ) -> Result<Self> {
266 let map_err = |path: &Path, err| {
267 UtilError::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
268 };
269
270 let path = path.canonicalize().map_err(|err| map_err(path, err))?;
271
272 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
273
274 let programs: Vec<Program> = if build_graph {
275 let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
276
277 let mut map: IndexMap<Symbol, (Dependency, Program)> = IndexMap::new();
278
279 let mut digraph = DiGraph::<Symbol>::new(Default::default());
280
281 let first_dependency = Dependency {
282 name: manifest.program.clone(),
283 location: Location::Local,
284 path: Some(path.clone()),
285 edition: None,
286 };
287
288 let test_dependencies: Vec<Dependency> = if with_tests {
289 let tests_directory = path.join(TESTS_DIRECTORY);
290 let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
291 .map(|path| Dependency {
292 name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
294 edition: None,
295 location: Location::Test,
296 path: Some(path.to_path_buf()),
297 })
298 .collect();
299 if let Some(deps) = manifest.dev_dependencies.as_ref() {
300 test_dependencies.extend(deps.iter().cloned());
301 }
302 test_dependencies
303 } else {
304 Vec::new()
305 };
306
307 for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
308 Self::graph_build(
309 &home_path,
310 network,
311 endpoint,
312 &first_dependency,
313 dependency,
314 &mut map,
315 &mut digraph,
316 no_cache,
317 no_local,
318 )?;
319 }
320
321 let ordered_dependency_symbols =
322 digraph.post_order().map_err(|_| UtilError::circular_dependency_error())?;
323
324 ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect()
325 } else {
326 Vec::new()
327 };
328
329 Ok(Package { base_directory: path, programs, manifest })
330 }
331
332 #[allow(clippy::too_many_arguments)]
333 fn graph_build(
334 home_path: &Path,
335 network: Option<NetworkName>,
336 endpoint: Option<&str>,
337 main_program: &Dependency,
338 new: Dependency,
339 map: &mut IndexMap<Symbol, (Dependency, Program)>,
340 graph: &mut DiGraph<Symbol>,
341 no_cache: bool,
342 no_local: bool,
343 ) -> Result<()> {
344 let name_symbol = symbol(&new.name)?;
345
346 let dependencies = map.clone().into_iter().map(|(name, (dep, _))| (name, dep)).collect();
348
349 let program = match map.entry(name_symbol) {
350 Entry::Occupied(occupied) => {
351 let existing_dep = &occupied.get().0;
354 assert_eq!(new.name, existing_dep.name);
355 if new.location != existing_dep.location
356 || new.path != existing_dep.path
357 || new.edition != existing_dep.edition
358 {
359 return Err(PackageError::conflicting_dependency(existing_dep, new).into());
360 }
361 return Ok(());
362 }
363 Entry::Vacant(vacant) => {
364 let program = match (new.path.as_ref(), new.location) {
365 (Some(path), Location::Local) if !no_local => {
366 if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
368 Program::from_aleo_path(name_symbol, path, &dependencies)?
369 } else {
370 Program::from_package_path(name_symbol, path)?
371 }
372 }
373 (Some(path), Location::Test) => {
374 Program::from_test_path(path, main_program.clone())?
377 }
378 (_, Location::Network) | (Some(_), Location::Local) => {
379 let Some(endpoint) = endpoint else {
381 return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
382 };
383 let Some(network) = network else {
384 return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
385 };
386 Program::fetch(name_symbol, new.edition, home_path, network, endpoint, no_cache)?
387 }
388 _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
389 };
390
391 vacant.insert((new, program.clone()));
392
393 program
394 }
395 };
396
397 graph.add_node(name_symbol);
398
399 for dependency in program.dependencies.iter() {
400 let dependency_symbol = symbol(&dependency.name)?;
401 graph.add_edge(name_symbol, dependency_symbol);
402 Self::graph_build(
403 home_path,
404 network,
405 endpoint,
406 main_program,
407 dependency.clone(),
408 map,
409 graph,
410 no_cache,
411 no_local,
412 )?;
413 }
414
415 Ok(())
416 }
417}
418
419fn main_template(name: &str) -> String {
420 format!(
421 r#"// The '{name}' program.
422program {name}.aleo {{
423 // This is the constructor for the program.
424 // The constructor allows you to manage program upgrades.
425 // It is called when the program is deployed or upgraded.
426 // It is currently configured to **prevent** upgrades.
427 // Other configurations include:
428 // - @admin(address="aleo1...")
429 // - @checksum(mapping="credits.aleo/fixme", key="0field")
430 // - @custom
431 // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
432 @noupgrade
433 async constructor() {{}}
434
435 transition main(public a: u32, b: u32) -> u32 {{
436 let c: u32 = a + b;
437 return c;
438 }}
439}}
440"#
441 )
442}
443
444fn test_template(name: &str) -> String {
445 format!(
446 r#"// The 'test_{name}' test program.
447import {name}.aleo;
448program test_{name}.aleo {{
449 @test
450 script test_it() {{
451 let result: u32 = {name}.aleo/main(1u32, 2u32);
452 assert_eq(result, 3u32);
453 }}
454
455 @test
456 @should_fail
457 transition do_nothing() {{
458 let result: u32 = {name}.aleo/main(2u32, 3u32);
459 assert_eq(result, 3u32);
460 }}
461
462 @noupgrade
463 async constructor() {{}}
464}}
465"#
466 )
467}