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