leo_package/
package.rs

1// Copyright (C) 2019-2025 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 programs in this package, whether
47    /// dependencies or the main program.
48    ///
49    /// Any program's dependent program will appear before it, so that compiling
50    /// them in order should give access to all stubs necessary to compile each
51    /// program.
52    pub programs: Vec<Program>,
53
54    /// The manifest file of this package.
55    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    /// Create a Leo package by the name `package_name` in a subdirectory of `path`.
80    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        // Verify that there is no existing directory at the path.
96        if full_path.exists() {
97            return Err(
98                PackageError::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
99            );
100        }
101
102        // Create the package directory.
103        std::fs::create_dir(&full_path)
104            .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
105
106        // Change the current working directory to the package directory.
107        std::env::set_current_dir(&full_path)
108            .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
109
110        // Create the gitignore file.
111        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        // Create the manifest.
120        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        // Create the source directory.
135        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        // Create the main.leo file.
141        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        // Create the tests directory.
149        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    /// Examine the Leo package at `path` to create a `Package`, but don't find dependencies.
162    ///
163    /// This may be useful if you just need other information like the manifest or env file.
164    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            /* build_graph */ false,
174            /* with_tests */ false,
175            /* no_cache */ false,
176            /* no_local */ false,
177            network,
178            endpoint,
179        )
180    }
181
182    /// Examine the Leo package at `path` to create a `Package`, including all its dependencies,
183    /// obtaining dependencies from the file system or network and topologically sorting them.
184    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            /* build_graph */ true,
196            /* with_tests */ false,
197            no_cache,
198            no_local,
199            network,
200            endpoint,
201        )
202    }
203
204    /// Examine the Leo package at `path` to create a `Package`, including all its dependencies
205    /// and its tests, obtaining dependencies from the file system or network and topologically sorting them.
206    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            /* build_graph */ true,
218            /* with_tests */ 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        // This allocation isn't ideal but it's not performance critical and
229        // easily resolves lifetime issues.
230        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        // This allocation isn't ideal but it's not performance critical and
237        // easily resolves lifetime issues.
238        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                        // We just made sure it has a ".leo" extension.
293                        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        // Get the existing dependencies.
347        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                // We've already visited this dependency. Just make sure it's compatible with
352                // the one we already have.
353                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                        // It's a local dependency.
367                        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                        // It's a test dependency - the path points to the source file,
375                        // not a package.
376                        Program::from_test_path(path, main_program.clone())?
377                    }
378                    (_, Location::Network) | (Some(_), Location::Local) => {
379                        // It's a network dependency.
380                        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}