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_errors::{CliError, PackageError, Result, UtilError};
20use leo_passes::DiGraph;
21use leo_span::Symbol;
22
23use anyhow::anyhow;
24use indexmap::{IndexMap, map::Entry};
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    SourcePath(PathBuf),
33}
34
35/// A Leo package.
36#[derive(Clone, Debug)]
37pub struct Package {
38    /// The directory on the filesystem where the package is located, canonicalized.
39    pub base_directory: PathBuf,
40
41    /// A topologically sorted list of all programs in this package, whether
42    /// dependencies or the main program.
43    ///
44    /// Any program's dependent program will appear before it, so that compiling
45    /// them in order should give access to all stubs necessary to compile each
46    /// program.
47    pub programs: Vec<Program>,
48
49    /// The manifest file of this package.
50    pub manifest: Manifest,
51
52    /// The .env file of this package.
53    pub env: Env,
54}
55
56impl Package {
57    pub fn outputs_directory(&self) -> PathBuf {
58        self.base_directory.join(OUTPUTS_DIRECTORY)
59    }
60
61    pub fn imports_directory(&self) -> PathBuf {
62        self.base_directory.join(IMPORTS_DIRECTORY)
63    }
64
65    pub fn build_directory(&self) -> PathBuf {
66        self.base_directory.join(BUILD_DIRECTORY)
67    }
68
69    pub fn source_directory(&self) -> PathBuf {
70        self.base_directory.join(SOURCE_DIRECTORY)
71    }
72
73    pub fn tests_directory(&self) -> PathBuf {
74        self.base_directory.join(TESTS_DIRECTORY)
75    }
76
77    /// Create a Leo package by the name `package_name` in a subdirectory of `path`.
78    pub fn initialize<P: AsRef<Path>>(
79        package_name: &str,
80        path: P,
81        network: NetworkName,
82        endpoint: &str,
83    ) -> Result<PathBuf> {
84        Self::initialize_impl(package_name, path.as_ref(), network, endpoint)
85    }
86
87    fn initialize_impl(package_name: &str, path: &Path, network: NetworkName, endpoint: &str) -> Result<PathBuf> {
88        let package_name =
89            if package_name.ends_with(".aleo") { package_name.to_string() } else { format!("{package_name}.aleo") };
90
91        if !crate::is_valid_aleo_name(&package_name) {
92            return Err(CliError::invalid_program_name(package_name).into());
93        }
94
95        let path = path.canonicalize().map_err(|e| PackageError::failed_path(path.display(), e))?;
96        let full_path = path.join(package_name.strip_suffix(".aleo").unwrap());
97
98        // Verify that there is no existing directory at the path.
99        if full_path.exists() {
100            return Err(
101                PackageError::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
102            );
103        }
104
105        // Create the package directory.
106        std::fs::create_dir(&full_path)
107            .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
108
109        // Change the current working directory to the package directory.
110        std::env::set_current_dir(&full_path)
111            .map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
112
113        // Create the gitignore file.
114        const GITIGNORE_TEMPLATE: &str = ".env\n*.avm\n*.prover\n*.verifier\noutputs/\n";
115
116        const GITIGNORE_FILENAME: &str = ".gitignore";
117
118        let gitignore_path = full_path.join(GITIGNORE_FILENAME);
119
120        std::fs::write(gitignore_path, GITIGNORE_TEMPLATE).map_err(PackageError::io_error_gitignore_file)?;
121
122        // Create the .env file.
123        let env = Env { network, private_key: TEST_PRIVATE_KEY.to_string(), endpoint: endpoint.to_string() };
124
125        let env_path = full_path.join(ENV_FILENAME);
126
127        env.write_to_file(env_path)?;
128
129        // Create the manifest.
130        let manifest = Manifest {
131            program: package_name.clone(),
132            version: "0.1.0".to_string(),
133            description: String::new(),
134            license: "MIT".to_string(),
135            dependencies: None,
136            dev_dependencies: None,
137        };
138
139        let manifest_path = full_path.join(MANIFEST_FILENAME);
140
141        manifest.write_to_file(manifest_path)?;
142
143        // Create the source directory.
144        let source_path = full_path.join(SOURCE_DIRECTORY);
145
146        std::fs::create_dir(&source_path)
147            .map_err(|e| PackageError::failed_to_create_source_directory(source_path.display(), e))?;
148
149        // Create the main.leo file.
150        let main_path = source_path.join(MAIN_FILENAME);
151
152        let name_no_aleo = package_name.strip_suffix(".aleo").unwrap();
153
154        std::fs::write(&main_path, main_template(name_no_aleo))
155            .map_err(|e| UtilError::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e))?;
156
157        // Create the tests directory.
158        let tests_path = full_path.join(TESTS_DIRECTORY);
159
160        std::fs::create_dir(&tests_path)
161            .map_err(|e| PackageError::failed_to_create_source_directory(tests_path.display(), e))?;
162
163        let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
164        std::fs::write(&test_file_path, test_template(name_no_aleo))
165            .map_err(|e| UtilError::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e))?;
166
167        Ok(full_path)
168    }
169
170    /// Examine the Leo package at `path` to create a `Package`, but don't find dependencies.
171    ///
172    /// This may be useful if you just need other information like the manifest or env file.
173    pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(path: P, home_path: Q) -> Result<Self> {
174        Self::from_directory_impl(
175            path.as_ref(),
176            home_path.as_ref(),
177            /* build_graph */ false,
178            /* with_tests */ false,
179            /* no_cache */ false,
180        )
181    }
182
183    /// Examine the Leo package at `path` to create a `Package`, including all its dependencies,
184    /// obtaining dependencies from the file system or network and topologically sorting them.
185    pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(path: P, home_path: Q, no_cache: bool) -> Result<Self> {
186        Self::from_directory_impl(
187            path.as_ref(),
188            home_path.as_ref(),
189            /* build_graph */ true,
190            /* with_tests */ false,
191            no_cache,
192        )
193    }
194
195    /// Examine the Leo package at `path` to create a `Package`, including all its dependencies
196    /// and its tests, obtaining dependencies from the file system or network and topologically sorting them.
197    pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
198        path: P,
199        home_path: Q,
200        no_cache: bool,
201    ) -> Result<Self> {
202        Self::from_directory_impl(
203            path.as_ref(),
204            home_path.as_ref(),
205            /* build_graph */ true,
206            /* with_tests */ true,
207            no_cache,
208        )
209    }
210
211    pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
212        let path = self.tests_directory();
213        // This allocation isn't ideal but it's not performance critical and
214        // easily resolves lifetime issues.
215        let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
216        data.into_iter()
217    }
218
219    pub fn import_files(&self) -> impl Iterator<Item = PathBuf> {
220        let path = self.imports_directory();
221        // This allocation isn't ideal but it's not performance critical and
222        // easily resolves lifetime issues.
223        let data: Vec<PathBuf> = Self::files_with_extension(&path, "aleo").collect();
224        data.into_iter()
225    }
226
227    fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
228        path.read_dir()
229            .ok()
230            .into_iter()
231            .flatten()
232            .flat_map(|maybe_filename| maybe_filename.ok())
233            .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
234            .flat_map(move |entry| {
235                let path = entry.path();
236                if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
237            })
238    }
239
240    fn from_directory_impl(
241        path: &Path,
242        home_path: &Path,
243        build_graph: bool,
244        with_tests: bool,
245        no_cache: bool,
246    ) -> Result<Self> {
247        let map_err = |path: &Path, err| {
248            UtilError::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
249        };
250
251        let path = path.canonicalize().map_err(|err| map_err(path, err))?;
252
253        let env = Env::read_from_file_or_environment(&path)?;
254
255        let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
256
257        let programs: Vec<Program> = if build_graph {
258            let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
259
260            let mut map: IndexMap<Symbol, (Dependency, Program)> = IndexMap::new();
261
262            let mut digraph = DiGraph::<Symbol>::new(Default::default());
263
264            let first_dependency =
265                Dependency { name: manifest.program.clone(), location: Location::Local, path: Some(path.clone()) };
266
267            let test_dependencies: Vec<Dependency> = if with_tests {
268                let tests_directory = path.join(TESTS_DIRECTORY);
269                let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
270                    .map(|path| Dependency {
271                        // We just made sure it has a ".leo" extension.
272                        name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
273                        location: Location::Test,
274                        path: Some(path.to_path_buf()),
275                    })
276                    .collect();
277                if let Some(deps) = manifest.dev_dependencies.as_ref() {
278                    test_dependencies.extend(deps.iter().cloned());
279                }
280                test_dependencies
281            } else {
282                Vec::new()
283            };
284
285            for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
286                Self::graph_build(
287                    &home_path,
288                    env.network,
289                    &env.endpoint,
290                    &first_dependency,
291                    dependency,
292                    &mut map,
293                    &mut digraph,
294                    no_cache,
295                )?;
296            }
297
298            let ordered_dependency_symbols =
299                digraph.post_order().map_err(|_| UtilError::circular_dependency_error())?;
300
301            ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect()
302        } else {
303            Vec::new()
304        };
305
306        Ok(Package { base_directory: path, programs, env, manifest })
307    }
308
309    #[allow(clippy::too_many_arguments)]
310    fn graph_build(
311        home_path: &Path,
312        network: NetworkName,
313        endpoint: &str,
314        main_program: &Dependency,
315        new: Dependency,
316        map: &mut IndexMap<Symbol, (Dependency, Program)>,
317        graph: &mut DiGraph<Symbol>,
318        no_cache: bool,
319    ) -> Result<()> {
320        let name_symbol = crate::symbol(&new.name)?;
321
322        let program = match map.entry(name_symbol) {
323            Entry::Occupied(occupied) => {
324                // We've already visited this dependency. Just make sure it's compatible with
325                // the one we already have.
326                let existing_dep = &occupied.get().0;
327                assert_eq!(new.name, existing_dep.name);
328                if new.location != existing_dep.location || new.path != existing_dep.path {
329                    return Err(PackageError::conflicting_dependency(format_args!("{name_symbol}.aleo")).into());
330                }
331                return Ok(());
332            }
333            Entry::Vacant(vacant) => {
334                let program = match (new.path.as_ref(), new.location) {
335                    (Some(path), Location::Local) => {
336                        // It's a local dependency.
337                        Program::from_path(name_symbol, path.clone())?
338                    }
339                    (Some(path), Location::Test) => {
340                        // It's a test dependency - the path points to the source file,
341                        // not a package.
342                        Program::from_path_test(path, main_program.clone())?
343                    }
344                    (_, Location::Network) => {
345                        // It's a network dependency.
346                        Program::fetch(name_symbol, home_path, network, endpoint, no_cache)?
347                    }
348                    _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
349                };
350
351                vacant.insert((new, program.clone()));
352
353                program
354            }
355        };
356
357        graph.add_node(name_symbol);
358
359        for dependency in program.dependencies.iter() {
360            let dependency_symbol = crate::symbol(&dependency.name)?;
361            graph.add_edge(name_symbol, dependency_symbol);
362            Self::graph_build(home_path, network, endpoint, main_program, dependency.clone(), map, graph, no_cache)?;
363        }
364
365        Ok(())
366    }
367
368    /// Get the program ID, program, and optional manifest for all programs in the package.
369    /// This method assumes that the package has already been built (`leo build` has been run).
370    #[allow(clippy::type_complexity)]
371    pub fn get_programs_and_manifests<P: AsRef<Path>>(
372        &self,
373        home_path: P,
374    ) -> Result<Vec<(String, String, Option<Manifest>)>> {
375        self.get_programs_and_manifests_impl(home_path.as_ref())
376    }
377
378    #[allow(clippy::type_complexity)]
379    fn get_programs_and_manifests_impl(&self, home_path: &Path) -> Result<Vec<(String, String, Option<Manifest>)>> {
380        self.programs
381            .iter()
382            .map(|program| {
383                match &program.data {
384                    ProgramData::Bytecode(bytecode) => Ok((program.name.to_string(), bytecode.clone(), None)),
385                    ProgramData::SourcePath(path) => {
386                        // Get the path to the built bytecode.
387                        let bytecode_path = if path.as_path() == self.source_directory().join("main.leo") {
388                            self.build_directory().join("main.aleo")
389                        } else {
390                            self.imports_directory().join(format!("{}.aleo", program.name))
391                        };
392                        // Fetch the bytecode.
393                        let bytecode = std::fs::read_to_string(&bytecode_path)
394                            .map_err(|e| PackageError::failed_to_read_file(bytecode_path.display(), e))?;
395                        // Get the package from the directory.
396                        let mut path = path.clone();
397                        path.pop();
398                        path.pop();
399                        let package = Package::from_directory_no_graph(&path, home_path)?;
400                        // Return the bytecode and the manifest.
401                        Ok((program.name.to_string(), bytecode, Some(package.manifest.clone())))
402                    }
403                }
404            })
405            .collect::<Result<Vec<_>>>()
406    }
407}
408
409fn main_template(name: &str) -> String {
410    format!(
411        r#"// The '{name}' program.
412program {name}.aleo {{
413    transition main(public a: u32, b: u32) -> u32 {{
414        let c: u32 = a + b;
415        return c;
416    }}
417}}
418"#
419    )
420}
421
422fn test_template(name: &str) -> String {
423    format!(
424        r#"// The 'test_{name}' test program.
425import {name}.aleo;
426program test_{name}.aleo {{
427    @test
428    script test_it() {{
429        let result: u32 = {name}.aleo/main(1u32, 2u32);
430        assert_eq(result, 3u32);
431    }}
432
433    @test
434    @should_fail
435    transition do_nothing() {{
436        let result: u32 = {name}.aleo/main(2u32, 3u32);
437        assert_eq(result, 3u32);
438    }}
439}}
440"#
441    )
442}