Skip to main content

leo_package/
package.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use 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/// Either the bytecode of an Aleo program (if it was a network dependency) or
28/// a path to its source (if it was local).
29#[derive(Clone, Debug)]
30pub enum ProgramData {
31    Bytecode(String),
32    /// For a local dependency, `directory` is the directory of the package
33    /// For a test dependency, `directory` is the directory of the test file.
34    SourcePath {
35        directory: PathBuf,
36        source: PathBuf,
37    },
38}
39
40/// A Leo package.
41#[derive(Clone, Debug)]
42pub struct Package {
43    /// The directory on the filesystem where the package is located, canonicalized.
44    pub base_directory: PathBuf,
45
46    /// A topologically sorted list of all compilation units in this package, whether
47    /// dependencies or the main program.
48    ///
49    /// Any unit's dependent unit will appear before it, so that compiling
50    /// them in order should give access to all stubs necessary to compile each
51    /// compilation unit.
52    pub compilation_units: Vec<CompilationUnit>,
53
54    /// The manifest file of this package.
55    pub manifest: Manifest,
56
57    /// The dependency graph of the package.
58    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    /// Create a Leo package by the name `package_name` in a subdirectory of `path`.
83    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        // Verify that there is no existing directory at the path.
109        if full_path.exists() {
110            return Err(
111                PackageError::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
112            );
113        }
114
115        // Create the package directory.
116        std::fs::create_dir(&full_path)
117            .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
118
119        // Change the current working directory to the package directory.
120        std::env::set_current_dir(&full_path)
121            .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
122
123        // Create .gitignore
124        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        // Create manifest
131        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        // Create src/
145        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            // Create lib.leo with a placeholder function.
154            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            // Create tests directory with a starter test file.
161            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            // Create main.leo
173            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            // Create tests directory
180            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    /// Examine the Leo package at `path` to create a `Package`, but don't find dependencies.
196    ///
197    /// This may be useful if you just need other information like the manifest file.
198    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            /* build_graph */ false,
209            /* with_tests */ false,
210            /* no_cache */ false,
211            /* no_local */ false,
212            network,
213            endpoint,
214            network_retries,
215        )
216    }
217
218    /// Examine the Leo package at `path` to create a `Package`, including all its dependencies,
219    /// obtaining dependencies from the file system or network and topologically sorting them.
220    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            /* build_graph */ true,
233            /* with_tests */ false,
234            no_cache,
235            no_local,
236            network,
237            endpoint,
238            network_retries,
239        )
240    }
241
242    /// Examine the Leo package at `path` to create a `Package`, including all its dependencies
243    /// and its tests, obtaining dependencies from the file system or network and topologically sorting them.
244    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            /* build_graph */ true,
257            /* with_tests */ 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        // This allocation isn't ideal but it's not performance critical and
269        // easily resolves lifetime issues.
270        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        // This allocation isn't ideal but it's not performance critical and
277        // easily resolves lifetime issues.
278        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            // Pre-collect all declared dependencies from the manifest tree so that
323            // .aleo file import classification doesn't depend on processing order.
324            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                        // We just made sure it has a ".leo" extension.
338                        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                // We've already visited this dependency. Just make sure it's compatible with
401                // the one we already have.
402                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                        // It's a local dependency.
416                        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                        // It's a test dependency - the path points to the source file,
424                        // not a package.
425                        CompilationUnit::from_test_path(path, main_program.clone())?
426                    }
427                    (_, Location::Network) | (Some(_), Location::Local) => {
428                        // It's a network dependency.
429                        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
550/// Walk the manifest tree and collect all declared dependencies.
551///
552/// This gives `parse_dependencies_from_aleo` full knowledge of which programs are
553/// declared as local dependencies, regardless of the order they appear in the manifest.
554/// Without this, `.aleo` file imports are classified against a snapshot of
555/// already-processed dependencies, requiring the user to list them in topological order.
556fn 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        // Only recurse into newly discovered dependencies to avoid infinite
579        // recursion on circular manifests (cycles are caught later by
580        // `DiGraph::post_order`).
581        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                // dev_dependencies are not transitive.
592                collect_declared_deps_recursive(path, &child, false, declared)?;
593            }
594        }
595    }
596    Ok(())
597}