1use 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
27fn 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 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#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum PackageKind {
50 Program,
52 Library,
54 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#[derive(Clone, Debug)]
74pub struct CompilationUnit {
75 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 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 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 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 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 let name = Symbol::intern(name.to_string().strip_suffix(".aleo").unwrap_or(&name.to_string()));
232
233 let cache_directory = home_path.join(format!("registry/{network}"));
235
236 let edition = match edition {
239 _ if name == Symbol::intern("credits") => 0,
241 Some(edition) => edition,
242 None if !no_cache => {
243 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 None => crate::fetch_latest_edition(&name.to_string(), endpoint, network, network_retries)?,
251 };
252
253 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 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 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 (Some(bytecode), false) => bytecode,
282 (existing, _) => {
284 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 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 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 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
337pub(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
351fn parse_dependencies_from_aleo(
353 name: Symbol,
354 bytecode: &str,
355 existing: &IndexMap<Symbol, Dependency>,
356) -> Result<IndexSet<Dependency>> {
357 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 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 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}