leo_package/
program.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::{PackageError, Result, UtilError};
20use leo_span::Symbol;
21
22use snarkvm::prelude::{Program as SvmProgram, TestnetV0};
23
24use indexmap::{IndexMap, IndexSet};
25use std::path::Path;
26
27/// Information about an Aleo program.
28#[derive(Clone, Debug)]
29pub struct Program {
30    // The name of the program (no ".aleo" suffix).
31    pub name: Symbol,
32    pub data: ProgramData,
33    pub edition: Option<u16>,
34    pub dependencies: IndexSet<Dependency>,
35    pub is_local: bool,
36    pub is_test: bool,
37}
38
39impl Program {
40    /// Given the location `path` of a `.aleo` file, read the filesystem
41    /// to obtain a `Program`.
42    pub fn from_aleo_path<P: AsRef<Path>>(name: Symbol, path: P, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
43        Self::from_aleo_path_impl(name, path.as_ref(), map)
44    }
45
46    fn from_aleo_path_impl(name: Symbol, path: &Path, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
47        let bytecode = std::fs::read_to_string(path).map_err(|e| {
48            UtilError::util_file_io_error(format_args!("Trying to read aleo file at {}", path.display()), e)
49        })?;
50
51        let dependencies = parse_dependencies_from_aleo(name, &bytecode, map)?;
52
53        Ok(Program {
54            name,
55            data: ProgramData::Bytecode(bytecode),
56            edition: None,
57            dependencies,
58            is_local: true,
59            is_test: false,
60        })
61    }
62
63    /// Given the location `path` of a local Leo package, read the filesystem
64    /// to obtain a `Program`.
65    pub fn from_package_path<P: AsRef<Path>>(name: Symbol, path: P) -> Result<Self> {
66        Self::from_package_path_impl(name, path.as_ref())
67    }
68
69    fn from_package_path_impl(name: Symbol, path: &Path) -> Result<Self> {
70        let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
71        let manifest_symbol = crate::symbol(&manifest.program)?;
72        if name != manifest_symbol {
73            return Err(PackageError::conflicting_manifest(
74                format_args!("{name}.aleo"),
75                format_args!("{manifest_symbol}.aleo"),
76            )
77            .into());
78        }
79        let source_directory = path.join(SOURCE_DIRECTORY);
80        let count = source_directory
81            .read_dir()
82            .map_err(|e| {
83                UtilError::util_file_io_error(
84                    format_args!("Failed to read directory {}", source_directory.display()),
85                    e,
86                )
87            })?
88            .count();
89
90        let source_path = source_directory.join(MAIN_FILENAME);
91
92        if !source_path.exists() || count != 1 {
93            return Err(PackageError::source_directory_can_contain_only_one_file(source_directory.display()).into());
94        }
95
96        Ok(Program {
97            name,
98            data: ProgramData::SourcePath { directory: path.to_path_buf(), source: source_path },
99            edition: None,
100            dependencies: manifest
101                .dependencies
102                .unwrap_or_default()
103                .into_iter()
104                .map(|dependency| canonicalize_dependency_path_relative_to(path, dependency))
105                .collect::<Result<IndexSet<_>, _>>()?,
106            is_local: true,
107            is_test: false,
108        })
109    }
110
111    /// Given the path to the source file of a test, create a `Program`.
112    ///
113    /// Unlike `Program::from_package_path`, the path is to the source file,
114    /// and the name of the program is determined from the filename.
115    ///
116    /// `main_program` must be provided since every test is dependent on it.
117    pub fn from_test_path<P: AsRef<Path>>(source_path: P, main_program: Dependency) -> Result<Self> {
118        Self::from_path_test_impl(source_path.as_ref(), main_program)
119    }
120
121    fn from_path_test_impl(source_path: &Path, main_program: Dependency) -> Result<Self> {
122        let name = filename_no_leo_extension(source_path)
123            .ok_or_else(|| PackageError::failed_path(source_path.display(), ""))?;
124        let test_directory = source_path.parent().ok_or_else(|| {
125            UtilError::failed_to_open_file(format_args!("Failed to find directory for test {}", source_path.display()))
126        })?;
127        let package_directory = test_directory.parent().ok_or_else(|| {
128            UtilError::failed_to_open_file(format_args!("Failed to find package for test {}", source_path.display()))
129        })?;
130        let manifest = Manifest::read_from_file(package_directory.join(MANIFEST_FILENAME))?;
131        let mut dependencies = manifest
132            .dev_dependencies
133            .unwrap_or_default()
134            .into_iter()
135            .map(|dependency| canonicalize_dependency_path_relative_to(package_directory, dependency))
136            .collect::<Result<IndexSet<_>, _>>()?;
137        dependencies.insert(main_program);
138
139        Ok(Program {
140            name: Symbol::intern(name),
141            edition: None,
142            data: ProgramData::SourcePath {
143                directory: test_directory.to_path_buf(),
144                source: source_path.to_path_buf(),
145            },
146            dependencies,
147            is_local: true,
148            is_test: true,
149        })
150    }
151
152    /// Given an Aleo program on a network, fetch it to build a `Program`.
153    /// If no edition is found, the latest edition is pulled from the network.
154    pub fn fetch<P: AsRef<Path>>(
155        name: Symbol,
156        edition: Option<u16>,
157        home_path: P,
158        network: NetworkName,
159        endpoint: &str,
160        no_cache: bool,
161    ) -> Result<Self> {
162        Self::fetch_impl(name, edition, home_path.as_ref(), network, endpoint, no_cache)
163    }
164
165    fn fetch_impl(
166        name: Symbol,
167        edition: Option<u16>,
168        home_path: &Path,
169        network: NetworkName,
170        endpoint: &str,
171        no_cache: bool,
172    ) -> Result<Self> {
173        // It's not a local program; let's check the cache.
174        let cache_directory = home_path.join(format!("registry/{network}"));
175
176        // If the edition is not specified, then query the network for the latest edition.
177        let edition = match edition {
178            _ if name == Symbol::intern("credits") => Ok(0), // Credits program always has edition 0.
179            Some(edition) => Ok(edition),
180            None => {
181                if name == Symbol::intern("credits") {
182                    // credits.aleo is always edition 0 and fetching from the network won't work.
183                    Ok(0)
184                } else {
185                    let url = format!("{endpoint}/{network}/program/{name}.aleo/latest_edition");
186                    fetch_from_network(&url).and_then(|contents| {
187                        contents.parse::<u16>().map_err(|e| {
188                            UtilError::failed_to_retrieve_from_endpoint(
189                                url,
190                                format!("Failed to parse edition as u16: {e}"),
191                            )
192                        })
193                    })
194                }
195            }
196        };
197
198        // If we failed to get the edition, default to 0.
199        let edition = edition.unwrap_or_else(|err| {
200            println!("Warning: Could not fetch edition for program `{name}`: {err}. Defaulting to edition 0.");
201            0
202        });
203
204        // Define the full cache path for the program.
205        let cache_directory = cache_directory.join(format!("{name}/{edition}"));
206        let full_cache_path = cache_directory.join(format!("{name}.aleo"));
207        if !cache_directory.exists() {
208            // Create directory if it doesn't exist.
209            std::fs::create_dir_all(&cache_directory).map_err(|err| {
210                UtilError::util_file_io_error(format!("Could not write path {}", cache_directory.display()), err)
211            })?;
212        }
213
214        // Get the existing bytecode if the file exists.
215        let existing_bytecode = match full_cache_path.exists() {
216            false => None,
217            true => {
218                let existing_contents = std::fs::read_to_string(&full_cache_path).map_err(|e| {
219                    UtilError::util_file_io_error(
220                        format_args!("Trying to read cached file at {}", full_cache_path.display()),
221                        e,
222                    )
223                })?;
224                Some(existing_contents)
225            }
226        };
227
228        let bytecode = match (existing_bytecode, no_cache) {
229            // If we are using the cache, we can just return the bytecode.
230            (Some(bytecode), false) => bytecode,
231            // Otherwise, we need to fetch it from the network.
232            (existing, _) => {
233                // Define the primary URL to fetch the program from.
234                let primary_url = if name == Symbol::intern("credits") {
235                    format!("{endpoint}/{network}/program/credits.aleo")
236                } else {
237                    format!("{endpoint}/{network}/program/{name}.aleo/{edition}")
238                };
239                let secondary_url = format!("{endpoint}/{network}/program/{name}.aleo");
240                let contents = fetch_from_network(&primary_url)
241                    .or_else(|_| fetch_from_network(&secondary_url))
242                    .map_err(|err| {
243                        UtilError::failed_to_retrieve_from_endpoint(
244                            primary_url,
245                            format_args!("Failed to fetch program `{name}` from network `{network}`: {err}"),
246                        )
247                    })?;
248
249                // If the file already exists, compare it to the new contents.
250                if let Some(existing_contents) = existing {
251                    if existing_contents != contents {
252                        println!(
253                            "Warning: The cached file at `{}` is different from the one fetched from the network. The cached file will be overwritten.",
254                            full_cache_path.display()
255                        );
256                    }
257                }
258
259                // Write the bytecode to the cache.
260                std::fs::write(&full_cache_path, &contents).map_err(|err| {
261                    UtilError::util_file_io_error(
262                        format_args!("Could not open file `{}`", full_cache_path.display()),
263                        err,
264                    )
265                })?;
266
267                contents
268            }
269        };
270
271        let dependencies = parse_dependencies_from_aleo(name, &bytecode, &IndexMap::new())?;
272
273        Ok(Program {
274            name,
275            data: ProgramData::Bytecode(bytecode),
276            edition: Some(edition),
277            dependencies,
278            is_local: false,
279            is_test: false,
280        })
281    }
282}
283
284/// If `dependency` has a relative path, assume it's relative to `base` and canonicalize it.
285///
286/// This needs to be done when collecting local dependencies from manifests which
287/// may be located at different places on the file system.
288fn canonicalize_dependency_path_relative_to(base: &Path, mut dependency: Dependency) -> Result<Dependency> {
289    if let Some(path) = &mut dependency.path {
290        if !path.is_absolute() {
291            let joined = base.join(&path);
292            *path = joined.canonicalize().map_err(|e| PackageError::failed_path(joined.display(), e))?;
293        }
294    }
295    Ok(dependency)
296}
297
298/// Parse the `.aleo` file's imports and construct `Dependency`s.
299fn parse_dependencies_from_aleo(
300    name: Symbol,
301    bytecode: &str,
302    existing: &IndexMap<Symbol, Dependency>,
303) -> Result<IndexSet<Dependency>> {
304    // Parse the bytecode into an SVM program.
305    let svm_program: SvmProgram<TestnetV0> = bytecode.parse().map_err(|_| UtilError::snarkvm_parsing_error(name))?;
306    let dependencies = svm_program
307        .imports()
308        .keys()
309        .map(|program_id| {
310            // If the dependency already exists, use it.
311            // Otherwise, assume it's a network dependency.
312            if let Some(dependency) = existing.get(&Symbol::intern(&program_id.name().to_string())) {
313                dependency.clone()
314            } else {
315                let name = program_id.to_string();
316                Dependency { name, location: Location::Network, path: None, edition: None }
317            }
318        })
319        .collect();
320    Ok(dependencies)
321}