1use crate::*;
18
19use leo_errors::Backtraced;
20
21use serde::{Deserialize, Serialize};
22use std::path::Path;
23
24pub const MANIFEST_FILENAME: &str = "program.json";
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Manifest {
29 pub program: String,
30 pub version: String,
31 pub description: String,
32 pub license: String,
33 #[serde(default = "current_version")]
34 pub leo: String,
35 pub dependencies: Option<Vec<Dependency>>,
36 pub dev_dependencies: Option<Vec<Dependency>>,
37 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
38 pub no_std: bool,
39}
40
41impl Manifest {
42 pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), Backtraced> {
44 let mut contents = serde_json::to_string_pretty(&self)
46 .map_err(|err| crate::errors::failed_to_serialize_manifest_file(path.as_ref().display(), err))?;
47
48 contents.push('\n');
50
51 std::fs::write(path, contents).map_err(crate::errors::failed_to_write_manifest)
53 }
54
55 pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self, Backtraced> {
57 let contents = std::fs::read_to_string(&path)
59 .map_err(|_| crate::errors::failed_to_load_package(path.as_ref().display()))?;
60 let manifest: Self = serde_json::from_str(&contents)
62 .map_err(|err| crate::errors::failed_to_deserialize_manifest_file(path.as_ref().display(), err))?;
63 manifest.validate_dependencies()?;
64 manifest.validate_reserved_names()?;
65 Ok(manifest)
66 }
67
68 fn validate_dependencies(&self) -> Result<(), Backtraced> {
69 for dependency in self.dependencies.iter().flatten().chain(self.dev_dependencies.iter().flatten()) {
70 dependency.validate_manifest_shape()?;
71 }
72 Ok(())
73 }
74
75 fn validate_reserved_names(&self) -> Result<(), Backtraced> {
76 let program_bare = crate::bare_unit_name(&self.program);
77 if program_bare == "std" {
78 return Err(crate::errors::reserved_std_name("program name"));
79 }
80 for dep in self.dependencies.iter().flatten().chain(self.dev_dependencies.iter().flatten()) {
81 let dep_bare = crate::bare_unit_name(&dep.name);
82 if dep_bare == "std" {
83 return Err(crate::errors::reserved_std_name("dependency name"));
84 }
85 }
86 Ok(())
87 }
88}
89
90fn current_version() -> String {
92 env!("CARGO_PKG_VERSION").to_string()
93}
94
95impl Dependency {
96 fn validate_manifest_shape(&self) -> Result<(), Backtraced> {
97 let invalid = |reason: String| Err(crate::errors::invalid_manifest_dependency(&self.name, reason));
100 let location = match self.location {
101 Location::Network => "network",
102 Location::Local => "local",
103 Location::Workspace => "workspace",
104 Location::Test => "test",
105 Location::Git => "git",
106 };
107
108 let path_required = matches!(self.location, Location::Local | Location::Test);
109 if path_required && self.path.is_none() {
110 return invalid(format!("`{location}` dependencies must specify `path`"));
111 }
112 if !path_required && self.path.is_some() {
113 return invalid(format!("`{location}` dependencies cannot specify `path`"));
114 }
115 if self.location != Location::Network && self.edition.is_some() {
116 return invalid(format!("`{location}` dependencies cannot specify `edition`"));
117 }
118 if self.location != Location::Git && self.git.is_some() {
119 return invalid(format!("`{location}` dependencies cannot specify `git`"));
120 }
121
122 if self.location == Location::Git {
123 let Some(git) = &self.git else {
124 return invalid("`git` dependencies must specify `git`".to_string());
125 };
126 if let Err(reason) = git.reference() {
127 return invalid(reason.to_string());
128 }
129 if !crate::is_valid_package_name(crate::bare_unit_name(&self.name)) {
132 return invalid("a git dependency name must be a valid program or library name".to_string());
133 }
134 }
135 Ok(())
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use crate::test_util::{manifest_json, read_manifest};
142
143 #[test]
144 fn manifest_rejects_network_dependency_with_path() {
145 let err =
146 read_manifest(&manifest_json(r#"[{"name":"foo.aleo","location":"network","path":"../foo"}]"#, "null"))
147 .unwrap_err();
148
149 assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
150 assert!(err.to_string().contains("`network` dependencies cannot specify `path`"));
151 }
152
153 #[test]
154 fn manifest_rejects_local_dependency_without_path() {
155 let err = read_manifest(&manifest_json(r#"[{"name":"foo.aleo","location":"local"}]"#, "null")).unwrap_err();
156
157 assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
158 assert!(err.to_string().contains("`local` dependencies must specify `path`"));
159 }
160
161 #[test]
162 fn manifest_rejects_invalid_dev_dependency_shape() {
163 let err =
164 read_manifest(&manifest_json("null", r#"[{"name":"foo.aleo","location":"workspace","path":"../foo"}]"#))
165 .unwrap_err();
166
167 assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
168 assert!(err.to_string().contains("`workspace` dependencies cannot specify `path`"));
169 }
170
171 #[test]
172 fn manifest_accepts_location_specific_dependency_fields() {
173 let manifest = read_manifest(&manifest_json(
174 r#"[
175 {"name":"network_dep.aleo","location":"network","edition":1},
176 {"name":"local_dep.aleo","location":"local","path":"../local_dep"},
177 {"name":"workspace_dep.aleo","location":"workspace"}
178]"#,
179 "null",
180 ))
181 .unwrap();
182
183 assert_eq!(manifest.dependencies.unwrap().len(), 3);
184 }
185}