/ adl-cli / package / src / program.rs
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  }