Skip to main content

leo_package/
compilation_unit.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::{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/// The kind of a Leo compilation unit: a deployable program, a library, or a test.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum PackageKind {
50    /// A deployable program with a `main.leo` entry point.
51    Program,
52    /// A library with a `lib.leo` entry point; not directly deployable.
53    Library,
54    /// A test file; compiled only during `leo test`.
55    Test,
56}
57
58impl PackageKind {
59    pub fn is_program(&self) -> bool {
60        matches!(self, Self::Program)
61    }
62
63    pub fn is_library(&self) -> bool {
64        matches!(self, Self::Library)
65    }
66
67    pub fn is_test(&self) -> bool {
68        matches!(self, Self::Test)
69    }
70}
71
72/// Information about a single Leo compilation unit.
73#[derive(Clone, Debug)]
74pub struct CompilationUnit {
75    // The name of the program. For local packages this is the bare name (no ".aleo" suffix,
76    // e.g. `my_program` or `my_lib`). For network-fetched programs this includes the ".aleo"
77    // suffix (e.g. `credits.aleo`). TODO: unify the invariant so the suffix is always absent.
78    pub name: Symbol,
79    pub data: ProgramData,
80    pub edition: Option<u16>,
81    pub dependencies: IndexSet<Dependency>,
82    pub is_local: bool,
83    pub kind: PackageKind,
84}
85
86impl CompilationUnit {
87    /// Given the location `path` of a `.aleo` file, read the filesystem
88    /// to obtain a `CompilationUnit`.
89    pub fn from_aleo_path<P: AsRef<Path>>(name: Symbol, path: P, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
90        Self::from_aleo_path_impl(name, path.as_ref(), map)
91    }
92
93    fn from_aleo_path_impl(name: Symbol, path: &Path, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
94        let bytecode = std::fs::read_to_string(path).map_err(|e| {
95            UtilError::util_file_io_error(format_args!("Trying to read aleo file at {}", path.display()), e)
96        })?;
97
98        let dependencies = parse_dependencies_from_aleo(name, &bytecode, map)?;
99
100        Ok(CompilationUnit {
101            name,
102            data: ProgramData::Bytecode(bytecode),
103            edition: None,
104            dependencies,
105            is_local: true,
106            kind: PackageKind::Program,
107        })
108    }
109
110    /// Given the location `path` of a local Leo package, read the filesystem
111    /// to obtain a `CompilationUnit`.
112    pub fn from_package_path<P: AsRef<Path>>(name: Symbol, path: P) -> Result<Self> {
113        Self::from_package_path_impl(name, path.as_ref())
114    }
115
116    fn from_package_path_impl(name: Symbol, path: &Path) -> Result<Self> {
117        let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
118        let manifest_symbol = crate::symbol(&manifest.program)?;
119        if name != manifest_symbol {
120            return Err(
121                PackageError::conflicting_manifest(format_args!("{name}"), format_args!("{manifest_symbol}")).into()
122            );
123        }
124        let source_directory = path.join(SOURCE_DIRECTORY);
125        source_directory.read_dir().map_err(|e| {
126            UtilError::util_file_io_error(format_args!("Failed to read directory {}", source_directory.display()), e)
127        })?;
128
129        let main_path = source_directory.join(MAIN_FILENAME);
130        let lib_path = source_directory.join(LIB_FILENAME);
131
132        let (source_path, kind) = match (main_path.exists(), lib_path.exists()) {
133            (true, true) => {
134                return Err(PackageError::ambiguous_entry_file(
135                    source_directory.display(),
136                    MAIN_FILENAME,
137                    LIB_FILENAME,
138                )
139                .into());
140            }
141            (true, false) => (main_path, PackageKind::Program),
142            (false, true) => (lib_path, PackageKind::Library),
143            (false, false) => {
144                return Err(
145                    PackageError::invalid_entry_file(source_directory.display(), MAIN_FILENAME, LIB_FILENAME).into()
146                );
147            }
148        };
149
150        Ok(CompilationUnit {
151            name,
152            data: ProgramData::SourcePath { directory: path.to_path_buf(), source: source_path },
153            edition: None,
154            dependencies: manifest
155                .dependencies
156                .unwrap_or_default()
157                .into_iter()
158                .map(|dependency| canonicalize_dependency_path_relative_to(path, dependency))
159                .collect::<Result<IndexSet<_>, _>>()?,
160            is_local: true,
161            kind,
162        })
163    }
164
165    /// Given the path to the source file of a test, create a `CompilationUnit`.
166    ///
167    /// Unlike `CompilationUnit::from_package_path`, the path is to the source file,
168    /// and the name of the program is determined from the filename.
169    ///
170    /// `main_program` must be provided since every test is dependent on it.
171    pub fn from_test_path<P: AsRef<Path>>(source_path: P, main_program: Dependency) -> Result<Self> {
172        Self::from_path_test_impl(source_path.as_ref(), main_program)
173    }
174
175    fn from_path_test_impl(source_path: &Path, main_program: Dependency) -> Result<Self> {
176        let name = filename_no_leo_extension(source_path)
177            .ok_or_else(|| PackageError::failed_path(source_path.display(), ""))?;
178        let test_directory = source_path.parent().ok_or_else(|| {
179            UtilError::failed_to_open_file(format_args!("Failed to find directory for test {}", source_path.display()))
180        })?;
181        let package_directory = test_directory.parent().ok_or_else(|| {
182            UtilError::failed_to_open_file(format_args!("Failed to find package for test {}", source_path.display()))
183        })?;
184        let manifest = Manifest::read_from_file(package_directory.join(MANIFEST_FILENAME))?;
185        let mut dependencies = manifest
186            .dev_dependencies
187            .unwrap_or_default()
188            .into_iter()
189            .map(|dependency| canonicalize_dependency_path_relative_to(package_directory, dependency))
190            .collect::<Result<IndexSet<_>, _>>()?;
191        dependencies.insert(main_program);
192
193        Ok(CompilationUnit {
194            name: Symbol::intern(&(name.to_owned() + ".aleo")),
195            edition: None,
196            data: ProgramData::SourcePath {
197                directory: test_directory.to_path_buf(),
198                source: source_path.to_path_buf(),
199            },
200            dependencies,
201            is_local: true,
202            kind: PackageKind::Test,
203        })
204    }
205
206    /// Given an Aleo program on a network, fetch it to build a `CompilationUnit`.
207    /// If no edition is found, the latest edition is pulled from the network.
208    pub fn fetch<P: AsRef<Path>>(
209        name: Symbol,
210        edition: Option<u16>,
211        home_path: P,
212        network: NetworkName,
213        endpoint: &str,
214        no_cache: bool,
215        network_retries: u32,
216    ) -> Result<Self> {
217        Self::fetch_impl(name, edition, home_path.as_ref(), network, endpoint, no_cache, network_retries)
218    }
219
220    fn fetch_impl(
221        name: Symbol,
222        edition: Option<u16>,
223        home_path: &Path,
224        network: NetworkName,
225        endpoint: &str,
226        no_cache: bool,
227        network_retries: u32,
228    ) -> Result<Self> {
229        // Callers may pass the name with or without the ".aleo" suffix; normalise to bare name
230        // here so cache paths and network URLs are constructed consistently.
231        let name = Symbol::intern(name.to_string().strip_suffix(".aleo").unwrap_or(&name.to_string()));
232
233        // It's not a local program; let's check the cache.
234        let cache_directory = home_path.join(format!("registry/{network}"));
235
236        // If the edition is not specified, try to find a cached version first,
237        // then fall back to querying the network for the latest edition.
238        let edition = match edition {
239            // Credits program always has edition 0.
240            _ if name == Symbol::intern("credits") => 0,
241            Some(edition) => edition,
242            None if !no_cache => {
243                // Check if we have a cached version - avoid network call if possible.
244                match find_cached_edition(&cache_directory, &name.to_string()) {
245                    Some(cached_edition) => cached_edition,
246                    None => crate::fetch_latest_edition(&name.to_string(), endpoint, network, network_retries)?,
247                }
248            }
249            // no_cache is set - user wants fresh data from network.
250            None => crate::fetch_latest_edition(&name.to_string(), endpoint, network, network_retries)?,
251        };
252
253        // Define the full cache path for the program.
254
255        // Build cache paths.
256        let cache_directory = cache_directory.join(format!("{name}/{edition}"));
257        let full_cache_path = cache_directory.join(format!("{name}.aleo"));
258        if !cache_directory.exists() {
259            // Create directory if it doesn't exist.
260            std::fs::create_dir_all(&cache_directory).map_err(|err| {
261                UtilError::util_file_io_error(format!("Could not write path {}", cache_directory.display()), err)
262            })?;
263        }
264
265        // Get the existing bytecode if the file exists.
266        let existing_bytecode = match full_cache_path.exists() {
267            false => None,
268            true => {
269                let existing_contents = std::fs::read_to_string(&full_cache_path).map_err(|e| {
270                    UtilError::util_file_io_error(
271                        format_args!("Trying to read cached file at {}", full_cache_path.display()),
272                        e,
273                    )
274                })?;
275                Some(existing_contents)
276            }
277        };
278
279        let bytecode = match (existing_bytecode, no_cache) {
280            // If we are using the cache, we can just return the bytecode.
281            (Some(bytecode), false) => bytecode,
282            // Otherwise, we need to fetch it from the network.
283            (existing, _) => {
284                // Define the primary URL to fetch the program from.
285                let primary_url = if name == Symbol::intern("credits") {
286                    format!("{endpoint}/{network}/program/credits.aleo")
287                } else {
288                    format!("{endpoint}/{network}/program/{name}.aleo/{edition}")
289                };
290                let secondary_url = format!("{endpoint}/{network}/program/{name}.aleo");
291                let contents = fetch_from_network(&primary_url, network_retries)
292                    .or_else(|_| fetch_from_network(&secondary_url, network_retries))
293                    .map_err(|err| {
294                        UtilError::failed_to_retrieve_from_endpoint(
295                            primary_url,
296                            format_args!("Failed to fetch program `{name}` from network `{network}`: {err}"),
297                        )
298                    })?;
299
300                // If the file already exists, compare it to the new contents.
301                if let Some(existing_contents) = existing
302                    && existing_contents != contents
303                {
304                    println!(
305                        "Warning: The cached file at `{}` is different from the one fetched from the network. The cached file will be overwritten.",
306                        full_cache_path.display()
307                    );
308                }
309
310                // Write the bytecode to the cache.
311                std::fs::write(&full_cache_path, &contents).map_err(|err| {
312                    UtilError::util_file_io_error(
313                        format_args!("Could not open file `{}`", full_cache_path.display()),
314                        err,
315                    )
316                })?;
317
318                contents
319            }
320        };
321
322        let dependencies = parse_dependencies_from_aleo(name, &bytecode, &IndexMap::new())?;
323
324        Ok(CompilationUnit {
325            // Network programs store the name with the ".aleo" suffix (unlike local packages).
326            // TODO: unify the invariant so the suffix is always absent.
327            name: Symbol::intern(&(name.to_string() + ".aleo")),
328            data: ProgramData::Bytecode(bytecode),
329            edition: Some(edition),
330            dependencies,
331            is_local: false,
332            kind: PackageKind::Program,
333        })
334    }
335}
336
337/// If `dependency` has a relative path, assume it's relative to `base` and canonicalize it.
338///
339/// This needs to be done when collecting local dependencies from manifests which
340/// may be located at different places on the file system.
341pub(crate) fn canonicalize_dependency_path_relative_to(base: &Path, mut dependency: Dependency) -> Result<Dependency> {
342    if let Some(path) = &mut dependency.path
343        && !path.is_absolute()
344    {
345        let joined = base.join(&path);
346        *path = joined.canonicalize().map_err(|e| PackageError::failed_path(joined.display(), e))?;
347    }
348    Ok(dependency)
349}
350
351/// Parse the `.aleo` file's imports and construct `Dependency`s.
352fn parse_dependencies_from_aleo(
353    name: Symbol,
354    bytecode: &str,
355    existing: &IndexMap<Symbol, Dependency>,
356) -> Result<IndexSet<Dependency>> {
357    // Check if the program size exceeds the maximum allowed limit.
358    let program_size = bytecode.len();
359
360    if program_size > MAX_PROGRAM_SIZE {
361        return Err(leo_errors::LeoError::UtilError(UtilError::program_size_limit_exceeded(
362            name,
363            program_size,
364            MAX_PROGRAM_SIZE,
365        )));
366    }
367
368    // Parse the bytecode into an SVM program.
369    let svm_program: SvmProgram<TestnetV0> = bytecode.parse().map_err(|_| UtilError::snarkvm_parsing_error(name))?;
370    let dependencies = svm_program
371        .imports()
372        .keys()
373        .map(|program_id| {
374            // If the dependency already exists, use it.
375            // Otherwise, assume it's a network dependency.
376            if let Some(dependency) = existing.get(&Symbol::intern(&program_id.to_string())) {
377                dependency.clone()
378            } else {
379                let name = program_id.to_string();
380                Dependency { name, location: Location::Network, path: None, edition: None }
381            }
382        })
383        .collect();
384    Ok(dependencies)
385}