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