program.rs
1 // Copyright (C) 2019-2025 ADnet Contributors 2 // This file is part of the ADL library. 3 4 // The ADL 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 ADL 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 ADL library. If not, see <https://www.gnu.org/licenses/>. 16 17 use crate::{MAX_PROGRAM_SIZE, *}; 18 19 use adl_errors::{PackageError, Result, UtilError}; 20 use adl_span::Symbol; 21 22 use snarkvm::prelude::{Program as SvmProgram, TestnetV0}; 23 24 use indexmap::{IndexMap, IndexSet}; 25 use 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. 29 fn 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 /// Information about an ALPHA program. 48 #[derive(Clone, Debug)] 49 pub struct Program { 50 // The name of the program (no ".alpha" suffix). 51 pub name: Symbol, 52 pub data: ProgramData, 53 pub edition: Option<u16>, 54 pub dependencies: IndexSet<Dependency>, 55 pub is_local: bool, 56 pub is_test: bool, 57 } 58 59 impl Program { 60 /// Given the location `path` of a `.alpha` file, read the filesystem 61 /// to obtain a `Program`. 62 pub fn from_alpha_path<P: AsRef<Path>>(name: Symbol, path: P, map: &IndexMap<Symbol, Dependency>) -> Result<Self> { 63 Self::from_alpha_path_impl(name, path.as_ref(), map) 64 } 65 66 fn from_alpha_path_impl(name: Symbol, path: &Path, map: &IndexMap<Symbol, Dependency>) -> Result<Self> { 67 let bytecode = std::fs::read_to_string(path).map_err(|e| { 68 UtilError::util_file_io_error(format_args!("Trying to read ALPHA file at {}", path.display()), e) 69 })?; 70 71 let dependencies = parse_dependencies_from_alpha(name, &bytecode, map)?; 72 73 Ok(Program { 74 name, 75 data: ProgramData::Bytecode(bytecode), 76 edition: None, 77 dependencies, 78 is_local: true, 79 is_test: false, 80 }) 81 } 82 83 /// Given the location `path` of a local ADL package, read the filesystem 84 /// to obtain a `Program`. 85 pub fn from_package_path<P: AsRef<Path>>(name: Symbol, path: P) -> Result<Self> { 86 Self::from_package_path_impl(name, path.as_ref()) 87 } 88 89 fn from_package_path_impl(name: Symbol, path: &Path) -> Result<Self> { 90 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?; 91 let manifest_symbol = crate::symbol(&manifest.program)?; 92 if name != manifest_symbol { 93 return Err(PackageError::conflicting_manifest( 94 format_args!("{name}.alpha"), 95 format_args!("{manifest_symbol}.alpha"), 96 ) 97 .into()); 98 } 99 let source_directory = path.join(SOURCE_DIRECTORY); 100 source_directory.read_dir().map_err(|e| { 101 UtilError::util_file_io_error(format_args!("Failed to read directory {}", source_directory.display()), e) 102 })?; 103 104 let source_path = source_directory.join(MAIN_FILENAME); 105 106 Ok(Program { 107 name, 108 data: ProgramData::SourcePath { directory: path.to_path_buf(), source: source_path }, 109 edition: None, 110 dependencies: manifest 111 .dependencies 112 .unwrap_or_default() 113 .into_iter() 114 .map(|dependency| canonicalize_dependency_path_relative_to(path, dependency)) 115 .collect::<Result<IndexSet<_>, _>>()?, 116 is_local: true, 117 is_test: false, 118 }) 119 } 120 121 /// Given the path to the source file of a test, create a `Program`. 122 /// 123 /// Unlike `Program::from_package_path`, the path is to the source file, 124 /// and the name of the program is determined from the filename. 125 /// 126 /// `main_program` must be provided since every test is dependent on it. 127 pub fn from_test_path<P: AsRef<Path>>(source_path: P, main_program: Dependency) -> Result<Self> { 128 Self::from_path_test_impl(source_path.as_ref(), main_program) 129 } 130 131 fn from_path_test_impl(source_path: &Path, main_program: Dependency) -> Result<Self> { 132 let name = filename_no_adl_extension(source_path) 133 .ok_or_else(|| PackageError::failed_path(source_path.display(), ""))?; 134 let test_directory = source_path.parent().ok_or_else(|| { 135 UtilError::failed_to_open_file(format_args!("Failed to find directory for test {}", source_path.display())) 136 })?; 137 let package_directory = test_directory.parent().ok_or_else(|| { 138 UtilError::failed_to_open_file(format_args!("Failed to find package for test {}", source_path.display())) 139 })?; 140 let manifest = Manifest::read_from_file(package_directory.join(MANIFEST_FILENAME))?; 141 let mut dependencies = manifest 142 .dev_dependencies 143 .unwrap_or_default() 144 .into_iter() 145 .map(|dependency| canonicalize_dependency_path_relative_to(package_directory, dependency)) 146 .collect::<Result<IndexSet<_>, _>>()?; 147 dependencies.insert(main_program); 148 149 Ok(Program { 150 name: Symbol::intern(name), 151 edition: None, 152 data: ProgramData::SourcePath { 153 directory: test_directory.to_path_buf(), 154 source: source_path.to_path_buf(), 155 }, 156 dependencies, 157 is_local: true, 158 is_test: true, 159 }) 160 } 161 162 /// Given an ALPHA program on a network, fetch it to build a `Program`. 163 /// If no edition is found, the latest edition is pulled from the network. 164 pub fn fetch<P: AsRef<Path>>( 165 name: Symbol, 166 edition: Option<u16>, 167 home_path: P, 168 network: NetworkName, 169 endpoint: &str, 170 no_cache: bool, 171 ) -> Result<Self> { 172 Self::fetch_impl(name, edition, home_path.as_ref(), network, endpoint, no_cache) 173 } 174 175 fn fetch_impl( 176 name: Symbol, 177 edition: Option<u16>, 178 home_path: &Path, 179 network: NetworkName, 180 endpoint: &str, 181 no_cache: bool, 182 ) -> Result<Self> { 183 // It's not a local program; let's check the cache. 184 let cache_directory = home_path.join(format!("registry/{network}")); 185 186 // If the edition is not specified, try to find a cached version first, 187 // then fall back to querying the network for the latest edition. 188 let edition = match edition { 189 // Credits program always has edition 0. 190 _ if name == Symbol::intern("credits") => 0, 191 Some(edition) => edition, 192 None if !no_cache => { 193 // Check if we have a cached version - avoid network call if possible. 194 match find_cached_edition(&cache_directory, &name.to_string()) { 195 Some(cached_edition) => cached_edition, 196 None => crate::fetch_latest_edition(&name.to_string(), endpoint, network)?, 197 } 198 } 199 // no_cache is set - user wants fresh data from network. 200 None => crate::fetch_latest_edition(&name.to_string(), endpoint, network)?, 201 }; 202 203 // Define the full cache path for the program. 204 let cache_directory = cache_directory.join(format!("{name}/{edition}")); 205 let full_cache_path = cache_directory.join(format!("{name}.alpha")); 206 if !cache_directory.exists() { 207 // Create directory if it doesn't exist. 208 std::fs::create_dir_all(&cache_directory).map_err(|err| { 209 UtilError::util_file_io_error(format!("Could not write path {}", cache_directory.display()), err) 210 })?; 211 } 212 213 // Get the existing bytecode if the file exists. 214 let existing_bytecode = match full_cache_path.exists() { 215 false => None, 216 true => { 217 let existing_contents = std::fs::read_to_string(&full_cache_path).map_err(|e| { 218 UtilError::util_file_io_error( 219 format_args!("Trying to read cached file at {}", full_cache_path.display()), 220 e, 221 ) 222 })?; 223 Some(existing_contents) 224 } 225 }; 226 227 let bytecode = match (existing_bytecode, no_cache) { 228 // If we are using the cache, we can just return the bytecode. 229 (Some(bytecode), false) => bytecode, 230 // Otherwise, we need to fetch it from the network. 231 (existing, _) => { 232 // Define the primary URL to fetch the program from. 233 let primary_url = if name == Symbol::intern("credits") { 234 format!("{endpoint}/{network}/program/credits.alpha") 235 } else { 236 format!("{endpoint}/{network}/program/{name}.alpha/{edition}") 237 }; 238 let secondary_url = format!("{endpoint}/{network}/program/{name}.alpha"); 239 let contents = fetch_from_network(&primary_url) 240 .or_else(|_| fetch_from_network(&secondary_url)) 241 .map_err(|err| { 242 UtilError::failed_to_retrieve_from_endpoint( 243 primary_url, 244 format_args!("Failed to fetch program `{name}` from network `{network}`: {err}"), 245 ) 246 })?; 247 248 // If the file already exists, compare it to the new contents. 249 if let Some(existing_contents) = existing 250 && existing_contents != contents 251 { 252 println!( 253 "Warning: The cached file at `{}` is different from the one fetched from the network. The cached file will be overwritten.", 254 full_cache_path.display() 255 ); 256 } 257 258 // Write the bytecode to the cache. 259 std::fs::write(&full_cache_path, &contents).map_err(|err| { 260 UtilError::util_file_io_error( 261 format_args!("Could not open file `{}`", full_cache_path.display()), 262 err, 263 ) 264 })?; 265 266 contents 267 } 268 }; 269 270 let dependencies = parse_dependencies_from_alpha(name, &bytecode, &IndexMap::new())?; 271 272 Ok(Program { 273 name, 274 data: ProgramData::Bytecode(bytecode), 275 edition: Some(edition), 276 dependencies, 277 is_local: false, 278 is_test: false, 279 }) 280 } 281 } 282 283 /// If `dependency` has a relative path, assume it's relative to `base` and canonicalize it. 284 /// 285 /// This needs to be done when collecting local dependencies from manifests which 286 /// may be located at different places on the file system. 287 fn canonicalize_dependency_path_relative_to(base: &Path, mut dependency: Dependency) -> Result<Dependency> { 288 if let Some(path) = &mut dependency.path 289 && !path.is_absolute() 290 { 291 let joined = base.join(&path); 292 *path = joined.canonicalize().map_err(|e| PackageError::failed_path(joined.display(), e))?; 293 } 294 Ok(dependency) 295 } 296 297 /// Parse the `.alpha` file's imports and construct `Dependency`s. 298 fn parse_dependencies_from_alpha( 299 name: Symbol, 300 bytecode: &str, 301 existing: &IndexMap<Symbol, Dependency>, 302 ) -> Result<IndexSet<Dependency>> { 303 // Check if the program size exceeds the maximum allowed limit. 304 let program_size = bytecode.len(); 305 306 if program_size > MAX_PROGRAM_SIZE { 307 return Err(adl_errors::AdlError::UtilError(UtilError::program_size_limit_exceeded( 308 name, 309 program_size, 310 MAX_PROGRAM_SIZE, 311 ))); 312 } 313 314 // Parse the bytecode into an SVM program. 315 let svm_program: SvmProgram<TestnetV0> = bytecode.parse().map_err(|_| UtilError::snarkvm_parsing_error(name))?; 316 let dependencies = svm_program 317 .imports() 318 .keys() 319 .map(|program_id| { 320 // If the dependency already exists, use it. 321 // Otherwise, assume it's a network dependency. 322 if let Some(dependency) = existing.get(&Symbol::intern(&program_id.name().to_string())) { 323 dependency.clone() 324 } else { 325 let name = program_id.to_string(); 326 Dependency { name, location: Location::Network, path: None, edition: None } 327 } 328 }) 329 .collect(); 330 Ok(dependencies) 331 }