Skip to main content

leo_package/
lib.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
17//! This crate deals with Leo packages on the file system and network.
18//!
19//! The main type is `Package`, which deals with Leo packages on the local filesystem.
20//! A Leo package directory is intended to have a structure like this:
21//! .
22//! ├── program.json
23//! ├── build
24//! │   ├── my_program
25//! │   │   ├── my_program.aleo
26//! │   │   └── abi.json
27//! │   └── credits
28//! │       └── credits.aleo
29//! ├── src
30//! │   └── main.leo
31//! └── tests
32//!     └── test_something.leo
33//!
34//! Inside `build`, every compilation unit - the package's own program or
35//! library, its local dependencies, and fetched network imports - gets its own
36//! `build/<name>/` directory with the same shape. When compiler-debug AST
37//! snapshots are requested they appear under `build/<name>/snapshots/`.
38//!
39//! The file `program.json` is a manifest containing the program name, version, description,
40//! and license, together with information about its dependencies.
41//!
42//! Such a directory structure, together with a `.gitignore` file, may be created
43//! on the file system using `Package::initialize`.
44//! ```no_run
45//! # use leo_ast::NetworkName;
46//! # use leo_package::{Package};
47//! let path = Package::initialize("my_package", "path/to/parent", false).unwrap();
48//! ```
49//!
50//! `tests` is where unit test files may be placed.
51//!
52//! Given an existing directory with such a structure, a `Package` may be created from it with
53//! `Package::from_directory`:
54//! ```no_run
55//! # use leo_ast::NetworkName;
56//! use leo_package::Package;
57//! let package = Package::from_directory("path/to/package", "/home/me/.aleo", false, false, Some(NetworkName::TestnetV0), Some("http://localhost:3030"), 3).unwrap();
58//! ```
59//! This will read the manifest and keep their data in `package.manifest`.
60//! It will also process dependencies and store them in topological order in `package.compilation_units`. This processing
61//! will involve fetching bytecode from the network for network dependencies.
62//! If the `no_cache` option (3rd parameter) is set to `true`, the package will not use the dependency cache.
63//! The endpoint and network are optional and are only needed if the package has network dependencies.
64//!
65//! If you want to simply read the manifest file without processing dependencies, use
66//! `Package::from_directory_no_graph`.
67//!
68//! `CompilationUnit` generally doesn't need to be created directly, as `Package` will create `CompilationUnit`s
69//! for the main program and all dependencies. However, if you'd like to fetch bytecode for
70//! a program, you can use `CompilationUnit::fetch`.
71
72#![forbid(unsafe_code)]
73
74mod errors;
75
76use leo_ast::NetworkName;
77use leo_errors::{Backtraced, Result};
78use leo_span::Symbol;
79
80use std::path::Path;
81
82mod dependency;
83pub use dependency::*;
84
85mod location;
86pub use location::*;
87
88mod manifest;
89pub use manifest::*;
90
91mod package;
92pub use package::*;
93
94mod compilation_unit;
95pub use compilation_unit::*;
96
97mod workspace;
98pub use workspace::*;
99
100pub const SOURCE_DIRECTORY: &str = "src";
101
102pub const MAIN_FILENAME: &str = "main.leo";
103
104pub const LIB_FILENAME: &str = "lib.leo";
105
106pub const BUILD_DIRECTORY: &str = "build";
107
108pub const ABI_FILENAME: &str = "abi.json";
109
110/// Name of the per-unit subdirectory holding interface ABI JSON files.
111pub const INTERFACES_DIRNAME: &str = "interfaces";
112
113/// Name of the per-unit subdirectory holding compiler-debug AST snapshots.
114/// Created lazily on first write; absent on builds that don't request snapshots.
115pub const SNAPSHOTS_DIRNAME: &str = "snapshots";
116
117pub const TESTS_DIRECTORY: &str = "tests";
118
119/// Maximum allowed program size in bytes.
120pub const MAX_PROGRAM_SIZE: usize =
121    <snarkvm::prelude::TestnetV0 as snarkvm::prelude::Network>::MAX_PROGRAM_SIZE.last().unwrap().1;
122
123/// The edition of a deployed program on the Aleo network.
124/// Edition 0 is the initial deployment, and increments with each upgrade.
125pub type Edition = u16;
126
127/// Strips a trailing `.aleo` (the Aleo program-ID suffix) from a compilation
128/// unit name, yielding the bare name.
129///
130/// `CompilationUnit` names are bare for local packages but `.aleo`-suffixed for
131/// network programs; build paths key on the bare name so the two are unified.
132pub fn bare_unit_name(name: &str) -> &str {
133    name.strip_suffix(".aleo").unwrap_or(name)
134}
135
136/// Converts a valid program or library name into a `Symbol`.
137///
138/// Names must either end with `.aleo` or contain no periods; otherwise an error is returned.
139fn symbol(name: &str) -> Result<Symbol> {
140    if name.ends_with(".aleo") || !name.contains('.') {
141        Ok(Symbol::intern(name))
142    } else {
143        Err(crate::errors::invalid_network_name(name).into())
144    }
145}
146
147/// Checks whether a string is a valid Aleo program name.
148///
149/// A valid program name must end with `.aleo` and the base name (without the
150/// suffix) must satisfy Aleo package naming rules.
151pub fn is_valid_program_name(name: &str) -> bool {
152    let Some(rest) = name.strip_suffix(".aleo") else {
153        tracing::error!("Program names must end with `.aleo`.");
154        return false;
155    };
156
157    is_valid_package_name(rest)
158}
159
160/// Checks whether a string is a valid Aleo library name.
161///
162/// Library names must satisfy Aleo package naming rules but do not require
163/// a `.aleo` suffix.
164pub fn is_valid_library_name(name: &str) -> bool {
165    is_valid_package_name(name)
166}
167
168/// Checks whether a string satisfies general Aleo package naming rules.
169///
170/// Names must be nonempty, start with a letter, contain only ASCII alphanumeric
171/// characters or underscores, avoid reserved keywords, and not contain "aleo".
172fn is_valid_package_name(name: &str) -> bool {
173    // Check that the name is nonempty.
174    if name.is_empty() {
175        tracing::error!("Aleo names must be nonempty");
176        return false;
177    }
178
179    let first = name.chars().next().unwrap();
180
181    // Check that the first character is not an underscore.
182    if first == '_' {
183        tracing::error!("Aleo names cannot begin with an underscore");
184        return false;
185    }
186
187    // Check that the first character is not a number.
188    if first.is_numeric() {
189        tracing::error!("Aleo names cannot begin with a number");
190        return false;
191    }
192
193    // Check valid characters.
194    if name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
195        tracing::error!("Aleo names can only contain ASCII alphanumeric characters and underscores.");
196        return false;
197    }
198
199    // Check reserved keywords.
200    if reserved_keywords().any(|kw| kw == name) {
201        tracing::error!(
202            "Aleo names cannot be a SnarkVM reserved keyword. Reserved keywords are: {}.",
203            reserved_keywords().collect::<Vec<_>>().join(", ")
204        );
205        return false;
206    }
207
208    // Disallow "aleo"
209    if name.contains("aleo") {
210        tracing::error!("Aleo names cannot contain the keyword `aleo`.");
211        return false;
212    }
213
214    true
215}
216
217/// Get the list of all reserved and restricted keywords from snarkVM.
218/// These keywords cannot be used as program names.
219/// See: https://github.com/ProvableHQ/snarkVM/blob/046a2964f75576b2c4afbab9aa9eabc43ceb6dc3/synthesizer/program/src/lib.rs#L192
220pub fn reserved_keywords() -> impl Iterator<Item = &'static str> {
221    use snarkvm::prelude::{Program, TestnetV0};
222
223    // Flatten RESTRICTED_KEYWORDS by ignoring ConsensusVersion
224    let restricted = Program::<TestnetV0>::RESTRICTED_KEYWORDS.iter().flat_map(|(_, kws)| kws.iter().copied());
225
226    Program::<TestnetV0>::KEYWORDS.iter().copied().chain(restricted)
227}
228
229/// Creates a configured ureq agent for Leo network requests.
230///
231/// Disables `http_status_as_error` so 4xx/5xx responses return `Ok(Response)`
232/// instead of `Err(StatusCode)`. This preserves response bodies which often
233/// contain useful error details from the server.
234pub fn create_http_agent() -> ureq::Agent {
235    ureq::Agent::config_builder().max_redirects(0).http_status_as_error(false).build().new_agent()
236}
237
238/// Retries a fallible network operation with exponential backoff.
239///
240/// Attempts the operation `retries + 1` times. Delays between attempts are
241/// 1 s, 2 s, 4 s, …, capped at 64 s. Returns the result of the last attempt.
242///
243/// Only use this for idempotent, read-only network calls (GET requests);
244/// never use it for state-mutating calls such as transaction broadcasts.
245pub fn retry_network_call<T, E: std::fmt::Display>(
246    network_retries: u32,
247    mut f: impl FnMut() -> std::result::Result<T, E>,
248) -> std::result::Result<T, E> {
249    let mut result = f();
250    for attempt in 1..=network_retries {
251        if result.is_ok() {
252            break;
253        }
254        let delay_secs = 2u64.pow(attempt - 1).min(64);
255        eprintln!("⚠️  Network request failed, retrying in {delay_secs}s (attempt {attempt}/{network_retries})...");
256        std::thread::sleep(std::time::Duration::from_secs(delay_secs));
257        result = f();
258    }
259    result
260}
261
262// Fetch the given endpoint url and return the sanitized response.
263pub fn fetch_from_network(url: &str, network_retries: u32) -> Result<String, Backtraced> {
264    fetch_from_network_plain(url, network_retries).map(|s| s.replace("\\n", "\n").replace('\"', ""))
265}
266
267pub fn fetch_from_network_plain(url: &str, network_retries: u32) -> Result<String, Backtraced> {
268    // Retry only on transport-level failures (connection errors, timeouts, etc.).
269    // HTTP 3xx/4xx/5xx responses are not retried since they reflect persistent conditions.
270    let agent = create_http_agent();
271    let mut response = retry_network_call(network_retries, || {
272        agent
273            .get(url)
274            .header("X-Leo-Version", env!("CARGO_PKG_VERSION"))
275            .call()
276            .map_err(|e| crate::errors::failed_to_retrieve_from_endpoint(url, e))
277    })?;
278    match response.status().as_u16() {
279        200..=299 => Ok(response.body_mut().read_to_string().unwrap()),
280        301 => Err(crate::errors::endpoint_moved_error(url)),
281        _ => Err(crate::errors::network_error(url, response.status())),
282    }
283}
284
285/// Fetch the given program from the network and return the program as a string.
286// TODO (@d0cd) Unify with `leo_package::CompilationUnit::fetch`.
287pub fn fetch_program_from_network(
288    name: &str,
289    endpoint: &str,
290    network: NetworkName,
291    network_retries: u32,
292) -> Result<String, Backtraced> {
293    let url = format!("{endpoint}/{network}/program/{name}");
294    let program = fetch_from_network(&url, network_retries)?;
295    Ok(program)
296}
297
298/// Fetch the latest edition of a program from the network.
299///
300/// Returns the actual latest edition number for the given program.
301/// This should be used instead of defaulting to arbitrary edition numbers.
302pub fn fetch_latest_edition(
303    name: &str,
304    endpoint: &str,
305    network: NetworkName,
306    network_retries: u32,
307) -> Result<Edition, Backtraced> {
308    // Strip the .aleo suffix if present for the URL.
309    let name_without_suffix = name.strip_suffix(".aleo").unwrap_or(name);
310
311    let url = format!("{endpoint}/{network}/program/{name_without_suffix}.aleo/latest_edition");
312    let contents = fetch_from_network(&url, network_retries)?;
313    contents.parse::<u16>().map_err(|e| {
314        crate::errors::failed_to_retrieve_from_endpoint(url, format!("Failed to parse edition as u16: {e}"))
315    })
316}
317
318// Verify that a fetched program is valid aleo instructions.
319pub fn verify_valid_program(name: &str, program: &str) -> Result<(), Backtraced> {
320    use snarkvm::prelude::{Program, TestnetV0};
321    use std::str::FromStr as _;
322
323    // Check if the program size exceeds the maximum allowed limit.
324    let program_size = program.len();
325
326    if program_size > MAX_PROGRAM_SIZE {
327        return Err(crate::errors::program_size_limit_exceeded(name, program_size, MAX_PROGRAM_SIZE));
328    }
329
330    // Parse the program to verify it's valid Aleo instructions.
331    match Program::<TestnetV0>::from_str(program) {
332        Ok(_) => Ok(()),
333        Err(_) => Err(crate::errors::snarkvm_parsing_error(name)),
334    }
335}
336
337pub fn filename_no_leo_extension(path: &Path) -> Option<&str> {
338    filename_no_extension(path, ".leo")
339}
340
341pub fn filename_no_aleo_extension(path: &Path) -> Option<&str> {
342    filename_no_extension(path, ".aleo")
343}
344
345fn filename_no_extension<'a>(path: &'a Path, extension: &'static str) -> Option<&'a str> {
346    path.file_name().and_then(|os_str| os_str.to_str()).and_then(|s| s.strip_suffix(extension))
347}