leo_lang/cli/commands/common/util.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 leo_errors::CliError;
18use leo_package::{Package, ProgramData};
19use leo_span::Symbol;
20
21use indexmap::IndexSet;
22use snarkvm::prelude::{ConsensusVersion, Network, Program};
23use std::path::PathBuf;
24use walkdir::WalkDir;
25
26/// Threshold percentage for program size warnings.
27const PROGRAM_SIZE_WARNING_THRESHOLD: usize = 90;
28
29/// Formats program size as KB and returns a warning message if approaching the limit.
30///
31/// Both `size` and `max_size` are expected in bytes.
32/// Returns `(size_kb, max_kb, warning)` where `warning` is `Some` if size exceeds 90% of max.
33pub fn format_program_size(size: usize, max_size: usize) -> (f64, f64, Option<String>) {
34 let size_kb = size as f64 / 1024.0;
35 let max_kb = max_size as f64 / 1024.0;
36 let percentage = (size as f64 / max_size as f64) * 100.0;
37
38 let warning = if size > max_size * PROGRAM_SIZE_WARNING_THRESHOLD / 100 {
39 Some(format!("approaching the size limit ({percentage:.1}% of {max_kb:.2} KB)"))
40 } else {
41 None
42 };
43
44 (size_kb, max_kb, warning)
45}
46
47/// Collects paths to Leo source files for each program in the package.
48///
49/// For each non-test program, it searches the `src` directory for `.leo` files.
50/// It separates the `main.leo` file from the rest and returns a tuple:
51/// (`main.leo` path, list of other `.leo` file paths).
52/// Test programs are included with an empty list of additional files.
53/// Programs with bytecode data are ignored.
54///
55/// # Arguments
56/// * `package` - Reference to the package containing programs.
57///
58/// # Returns
59/// A vector of tuples with the main file and other source files.
60pub fn collect_leo_paths(package: &Package) -> Vec<(PathBuf, Vec<PathBuf>)> {
61 let mut partitioned_leo_paths = Vec::new();
62 for program in &package.programs {
63 match &program.data {
64 ProgramData::SourcePath { directory, source } => {
65 if program.is_test {
66 partitioned_leo_paths.push((source.clone(), vec![]));
67 } else {
68 let src_dir = directory.join("src");
69 if !src_dir.exists() {
70 continue;
71 }
72
73 let mut all_files: Vec<PathBuf> = WalkDir::new(&src_dir)
74 .into_iter()
75 .filter_map(Result::ok)
76 .filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("leo"))
77 .map(|entry| entry.into_path())
78 .collect();
79
80 if let Some(index) =
81 all_files.iter().position(|p| p.file_name().and_then(|s| s.to_str()) == Some("main.leo"))
82 {
83 let main = all_files.remove(index);
84 partitioned_leo_paths.push((main, all_files));
85 }
86 }
87 }
88 ProgramData::Bytecode(..) => {}
89 }
90 }
91 partitioned_leo_paths
92}
93
94/// Collects paths to `.aleo` files that are external (non-local) dependencies.
95///
96/// Scans the package's `imports` directory and filters out files that match
97/// the names of local source-based dependencies.
98/// Only retains `.aleo` files corresponding to true external dependencies.
99///
100/// # Arguments
101/// * `package` - Reference to the package whose imports are being examined.
102///
103/// # Returns
104/// A vector of paths to `.aleo` files not associated with local source dependencies.
105pub fn collect_aleo_paths(package: &Package) -> Vec<PathBuf> {
106 let local_dependency_symbols: IndexSet<Symbol> = package
107 .programs
108 .iter()
109 .flat_map(|program| match &program.data {
110 ProgramData::SourcePath { .. } => {
111 // It's a local Leo dependency.
112 Some(program.name)
113 }
114 ProgramData::Bytecode(..) => {
115 // It's a network dependency or local .aleo dependency.
116 None
117 }
118 })
119 .collect();
120
121 package
122 .imports_directory()
123 .read_dir()
124 .ok()
125 .into_iter()
126 .flatten()
127 .flat_map(|maybe_filename| maybe_filename.ok())
128 .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
129 .flat_map(|entry| {
130 let path = entry.path();
131 if let Some(filename) = leo_package::filename_no_aleo_extension(&path) {
132 let symbol = Symbol::intern(filename);
133 if local_dependency_symbols.contains(&symbol) { None } else { Some(path) }
134 } else {
135 None
136 }
137 })
138 .collect()
139}
140
141/// Default edition for local programs during local operations (run, execute, synthesize).
142///
143/// Local programs don't have an on-chain edition yet. We default to edition 1 to avoid
144/// snarkVM's V8+ check that rejects edition 0 programs without constructors. That check
145/// is only relevant for deployed programs, not local development.
146pub const LOCAL_PROGRAM_DEFAULT_EDITION: leo_package::Edition = 1;
147
148/// Prints a program's ID and source (local or network edition).
149pub fn print_program_source(id: &str, edition: Option<leo_package::Edition>) {
150 match (id, edition) {
151 ("credits.aleo", _) => println!(" - {id} (already included)"),
152 (_, Some(e)) => println!(" - {id} (edition: {e})"),
153 (_, None) => println!(" - {id} (local)"),
154 }
155}
156
157/// Checks if any programs violate edition/constructor requirements.
158///
159/// Programs at edition 0 without a constructor cannot be executed after ConsensusVersion::V8.
160/// This check should be performed before attempting execution to provide a clear error message.
161///
162/// # Arguments
163/// * `programs` - Slice of (program, edition) tuples to check
164/// * `consensus_version` - The current consensus version
165/// * `action` - Description of the action being attempted (e.g., "deploy", "execute", "upgrade")
166///
167/// # Returns
168/// `Ok(())` if all programs pass the check, or an error with a descriptive message if not.
169pub fn check_edition_constructor_requirements<N: Network>(
170 programs: &[(Program<N>, leo_package::Edition)],
171 consensus_version: ConsensusVersion,
172 action: &str,
173) -> Result<(), CliError> {
174 // Only check for V8+ consensus versions.
175 if consensus_version < ConsensusVersion::V8 {
176 return Ok(());
177 }
178
179 for (program, edition) in programs {
180 // Programs at edition 0 without a constructor cannot be executed after V8.
181 if *edition == 0 && !program.contains_constructor() {
182 let id = program.id();
183 // Skip credits.aleo as it's a special case.
184 if id.to_string() != "credits.aleo" {
185 return Err(CliError::custom(format!(
186 "Cannot {action} with dependency '{id}' (edition 0)\n\n\
187 Programs at edition 0 without a constructor cannot be executed under \
188 consensus version V8 or later (current: V{}).\n\n\
189 The program '{id}' must be upgraded on-chain before it can be used.",
190 consensus_version as u8
191 )));
192 }
193 }
194 }
195
196 Ok(())
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use snarkvm::prelude::TestnetV0;
203 use std::str::FromStr;
204
205 #[test]
206 fn test_edition_constructor_error_message() {
207 // A program without a constructor at edition 0 should fail under V8+
208 let program = Program::<TestnetV0>::from_str(
209 "program old_program.aleo;\nfunction main:\n input r0 as u32.public;\n output r0 as u32.public;\n",
210 )
211 .unwrap();
212
213 let result = check_edition_constructor_requirements(&[(program, 0)], ConsensusVersion::V9, "deploy");
214 assert!(result.is_err());
215 }
216}