1use crate::*;
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
27#[derive(Clone, Debug)]
29pub struct Program {
30 pub name: Symbol,
32 pub data: ProgramData,
33 pub edition: Option<u16>,
34 pub dependencies: IndexSet<Dependency>,
35 pub is_local: bool,
36 pub is_test: bool,
37}
38
39impl Program {
40 pub fn from_aleo_path<P: AsRef<Path>>(name: Symbol, path: P, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
43 Self::from_aleo_path_impl(name, path.as_ref(), map)
44 }
45
46 fn from_aleo_path_impl(name: Symbol, path: &Path, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
47 let bytecode = std::fs::read_to_string(path).map_err(|e| {
48 UtilError::util_file_io_error(format_args!("Trying to read aleo file at {}", path.display()), e)
49 })?;
50
51 let dependencies = parse_dependencies_from_aleo(name, &bytecode, map)?;
52
53 Ok(Program {
54 name,
55 data: ProgramData::Bytecode(bytecode),
56 edition: None,
57 dependencies,
58 is_local: true,
59 is_test: false,
60 })
61 }
62
63 pub fn from_package_path<P: AsRef<Path>>(name: Symbol, path: P) -> Result<Self> {
66 Self::from_package_path_impl(name, path.as_ref())
67 }
68
69 fn from_package_path_impl(name: Symbol, path: &Path) -> Result<Self> {
70 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
71 let manifest_symbol = crate::symbol(&manifest.program)?;
72 if name != manifest_symbol {
73 return Err(PackageError::conflicting_manifest(
74 format_args!("{name}.aleo"),
75 format_args!("{manifest_symbol}.aleo"),
76 )
77 .into());
78 }
79 let source_directory = path.join(SOURCE_DIRECTORY);
80 let count = source_directory
81 .read_dir()
82 .map_err(|e| {
83 UtilError::util_file_io_error(
84 format_args!("Failed to read directory {}", source_directory.display()),
85 e,
86 )
87 })?
88 .count();
89
90 let source_path = source_directory.join(MAIN_FILENAME);
91
92 if !source_path.exists() || count != 1 {
93 return Err(PackageError::source_directory_can_contain_only_one_file(source_directory.display()).into());
94 }
95
96 Ok(Program {
97 name,
98 data: ProgramData::SourcePath { directory: path.to_path_buf(), source: source_path },
99 edition: None,
100 dependencies: manifest
101 .dependencies
102 .unwrap_or_default()
103 .into_iter()
104 .map(|dependency| canonicalize_dependency_path_relative_to(path, dependency))
105 .collect::<Result<IndexSet<_>, _>>()?,
106 is_local: true,
107 is_test: false,
108 })
109 }
110
111 pub fn from_test_path<P: AsRef<Path>>(source_path: P, main_program: Dependency) -> Result<Self> {
118 Self::from_path_test_impl(source_path.as_ref(), main_program)
119 }
120
121 fn from_path_test_impl(source_path: &Path, main_program: Dependency) -> Result<Self> {
122 let name = filename_no_leo_extension(source_path)
123 .ok_or_else(|| PackageError::failed_path(source_path.display(), ""))?;
124 let test_directory = source_path.parent().ok_or_else(|| {
125 UtilError::failed_to_open_file(format_args!("Failed to find directory for test {}", source_path.display()))
126 })?;
127 let package_directory = test_directory.parent().ok_or_else(|| {
128 UtilError::failed_to_open_file(format_args!("Failed to find package for test {}", source_path.display()))
129 })?;
130 let manifest = Manifest::read_from_file(package_directory.join(MANIFEST_FILENAME))?;
131 let mut dependencies = manifest
132 .dev_dependencies
133 .unwrap_or_default()
134 .into_iter()
135 .map(|dependency| canonicalize_dependency_path_relative_to(package_directory, dependency))
136 .collect::<Result<IndexSet<_>, _>>()?;
137 dependencies.insert(main_program);
138
139 Ok(Program {
140 name: Symbol::intern(name),
141 edition: None,
142 data: ProgramData::SourcePath {
143 directory: test_directory.to_path_buf(),
144 source: source_path.to_path_buf(),
145 },
146 dependencies,
147 is_local: true,
148 is_test: true,
149 })
150 }
151
152 pub fn fetch<P: AsRef<Path>>(
155 name: Symbol,
156 edition: Option<u16>,
157 home_path: P,
158 network: NetworkName,
159 endpoint: &str,
160 no_cache: bool,
161 ) -> Result<Self> {
162 Self::fetch_impl(name, edition, home_path.as_ref(), network, endpoint, no_cache)
163 }
164
165 fn fetch_impl(
166 name: Symbol,
167 edition: Option<u16>,
168 home_path: &Path,
169 network: NetworkName,
170 endpoint: &str,
171 no_cache: bool,
172 ) -> Result<Self> {
173 let cache_directory = home_path.join(format!("registry/{network}"));
175
176 let edition = match edition {
178 _ if name == Symbol::intern("credits") => Ok(0), Some(edition) => Ok(edition),
180 None => {
181 if name == Symbol::intern("credits") {
182 Ok(0)
184 } else {
185 let url = format!("{endpoint}/{network}/program/{name}.aleo/latest_edition");
186 fetch_from_network(&url).and_then(|contents| {
187 contents.parse::<u16>().map_err(|e| {
188 UtilError::failed_to_retrieve_from_endpoint(
189 url,
190 format!("Failed to parse edition as u16: {e}"),
191 )
192 })
193 })
194 }
195 }
196 };
197
198 let edition = edition.unwrap_or_else(|err| {
200 println!("Warning: Could not fetch edition for program `{name}`: {err}. Defaulting to edition 0.");
201 0
202 });
203
204 let cache_directory = cache_directory.join(format!("{name}/{edition}"));
206 let full_cache_path = cache_directory.join(format!("{name}.aleo"));
207 if !cache_directory.exists() {
208 std::fs::create_dir_all(&cache_directory).map_err(|err| {
210 UtilError::util_file_io_error(format!("Could not write path {}", cache_directory.display()), err)
211 })?;
212 }
213
214 let existing_bytecode = match full_cache_path.exists() {
216 false => None,
217 true => {
218 let existing_contents = std::fs::read_to_string(&full_cache_path).map_err(|e| {
219 UtilError::util_file_io_error(
220 format_args!("Trying to read cached file at {}", full_cache_path.display()),
221 e,
222 )
223 })?;
224 Some(existing_contents)
225 }
226 };
227
228 let bytecode = match (existing_bytecode, no_cache) {
229 (Some(bytecode), false) => bytecode,
231 (existing, _) => {
233 let primary_url = if name == Symbol::intern("credits") {
235 format!("{endpoint}/{network}/program/credits.aleo")
236 } else {
237 format!("{endpoint}/{network}/program/{name}.aleo/{edition}")
238 };
239 let secondary_url = format!("{endpoint}/{network}/program/{name}.aleo");
240 let contents = fetch_from_network(&primary_url)
241 .or_else(|_| fetch_from_network(&secondary_url))
242 .map_err(|err| {
243 UtilError::failed_to_retrieve_from_endpoint(
244 primary_url,
245 format_args!("Failed to fetch program `{name}` from network `{network}`: {err}"),
246 )
247 })?;
248
249 if let Some(existing_contents) = existing {
251 if existing_contents != contents {
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
259 std::fs::write(&full_cache_path, &contents).map_err(|err| {
261 UtilError::util_file_io_error(
262 format_args!("Could not open file `{}`", full_cache_path.display()),
263 err,
264 )
265 })?;
266
267 contents
268 }
269 };
270
271 let dependencies = parse_dependencies_from_aleo(name, &bytecode, &IndexMap::new())?;
272
273 Ok(Program {
274 name,
275 data: ProgramData::Bytecode(bytecode),
276 edition: Some(edition),
277 dependencies,
278 is_local: false,
279 is_test: false,
280 })
281 }
282}
283
284fn canonicalize_dependency_path_relative_to(base: &Path, mut dependency: Dependency) -> Result<Dependency> {
289 if let Some(path) = &mut dependency.path {
290 if !path.is_absolute() {
291 let joined = base.join(&path);
292 *path = joined.canonicalize().map_err(|e| PackageError::failed_path(joined.display(), e))?;
293 }
294 }
295 Ok(dependency)
296}
297
298fn parse_dependencies_from_aleo(
300 name: Symbol,
301 bytecode: &str,
302 existing: &IndexMap<Symbol, Dependency>,
303) -> Result<IndexSet<Dependency>> {
304 let svm_program: SvmProgram<TestnetV0> = bytecode.parse().map_err(|_| UtilError::snarkvm_parsing_error(name))?;
306 let dependencies = svm_program
307 .imports()
308 .keys()
309 .map(|program_id| {
310 if let Some(dependency) = existing.get(&Symbol::intern(&program_id.name().to_string())) {
313 dependency.clone()
314 } else {
315 let name = program_id.to_string();
316 Dependency { name, location: Location::Network, path: None, edition: None }
317 }
318 })
319 .collect();
320 Ok(dependencies)
321}