Skip to main content

leo_package/
manifest.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo 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 Leo 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 Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use 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/// Struct representation of program's `program.json` specification.
27#[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}
38
39impl Manifest {
40    /// Write the manifest to the given `path` as a JSON string.
41    pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), Backtraced> {
42        // Serialize the manifest to a JSON string.
43        let mut contents = serde_json::to_string_pretty(&self)
44            .map_err(|err| crate::errors::failed_to_serialize_manifest_file(path.as_ref().display(), err))?;
45
46        // The seralized string doesn't end in a newline.
47        contents.push('\n');
48
49        // Write the manifest to the file.
50        std::fs::write(path, contents).map_err(crate::errors::failed_to_write_manifest)
51    }
52
53    /// Read and validate a Manifest from the given JSON file.
54    pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self, Backtraced> {
55        // Read the manifest file.
56        let contents = std::fs::read_to_string(&path)
57            .map_err(|_| crate::errors::failed_to_load_package(path.as_ref().display()))?;
58        // Deserialize the manifest.
59        let manifest: Self = serde_json::from_str(&contents)
60            .map_err(|err| crate::errors::failed_to_deserialize_manifest_file(path.as_ref().display(), err))?;
61        manifest.validate_dependencies()?;
62        Ok(manifest)
63    }
64
65    fn validate_dependencies(&self) -> Result<(), Backtraced> {
66        for dependency in self.dependencies.iter().flatten().chain(self.dev_dependencies.iter().flatten()) {
67            dependency.validate_manifest_shape()?;
68        }
69        Ok(())
70    }
71}
72
73// Returns the current version of Leo.
74fn current_version() -> String {
75    env!("CARGO_PKG_VERSION").to_string()
76}
77
78impl Dependency {
79    fn validate_manifest_shape(&self) -> Result<(), Backtraced> {
80        match self.location {
81            Location::Network => {
82                if self.path.is_some() {
83                    return Err(crate::errors::invalid_manifest_dependency(
84                        &self.name,
85                        "`network` dependencies cannot specify `path`",
86                    ));
87                }
88            }
89            Location::Local => {
90                if self.path.is_none() {
91                    return Err(crate::errors::invalid_manifest_dependency(
92                        &self.name,
93                        "`local` dependencies must specify `path`",
94                    ));
95                }
96                if self.edition.is_some() {
97                    return Err(crate::errors::invalid_manifest_dependency(
98                        &self.name,
99                        "`local` dependencies cannot specify `edition`",
100                    ));
101                }
102            }
103            Location::Workspace => {
104                if self.path.is_some() {
105                    return Err(crate::errors::invalid_manifest_dependency(
106                        &self.name,
107                        "`workspace` dependencies cannot specify `path`",
108                    ));
109                }
110                if self.edition.is_some() {
111                    return Err(crate::errors::invalid_manifest_dependency(
112                        &self.name,
113                        "`workspace` dependencies cannot specify `edition`",
114                    ));
115                }
116            }
117            Location::Test => {
118                if self.path.is_none() {
119                    return Err(crate::errors::invalid_manifest_dependency(
120                        &self.name,
121                        "`test` dependencies must specify `path`",
122                    ));
123                }
124                if self.edition.is_some() {
125                    return Err(crate::errors::invalid_manifest_dependency(
126                        &self.name,
127                        "`test` dependencies cannot specify `edition`",
128                    ));
129                }
130            }
131        }
132        Ok(())
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use std::{
140        fs,
141        time::{SystemTime, UNIX_EPOCH},
142    };
143
144    fn manifest_json(dependencies: &str, dev_dependencies: &str) -> String {
145        format!(
146            r#"{{
147  "program": "test.aleo",
148  "version": "0.1.0",
149  "description": "",
150  "license": "MIT",
151  "dependencies": {dependencies},
152  "dev_dependencies": {dev_dependencies}
153}}"#
154        )
155    }
156
157    fn read_manifest(contents: &str) -> Result<Manifest, Backtraced> {
158        let unique = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
159        let dir = std::env::temp_dir().join(format!("leo-manifest-test-{unique}"));
160        fs::create_dir(&dir).unwrap();
161        let path = dir.join(MANIFEST_FILENAME);
162        fs::write(&path, contents).unwrap();
163        let result = Manifest::read_from_file(&path);
164        fs::remove_dir_all(dir).unwrap();
165        result
166    }
167
168    #[test]
169    fn manifest_rejects_network_dependency_with_path() {
170        let err =
171            read_manifest(&manifest_json(r#"[{"name":"foo.aleo","location":"network","path":"../foo"}]"#, "null"))
172                .unwrap_err();
173
174        assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
175        assert!(err.to_string().contains("`network` dependencies cannot specify `path`"));
176    }
177
178    #[test]
179    fn manifest_rejects_local_dependency_without_path() {
180        let err = read_manifest(&manifest_json(r#"[{"name":"foo.aleo","location":"local"}]"#, "null")).unwrap_err();
181
182        assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
183        assert!(err.to_string().contains("`local` dependencies must specify `path`"));
184    }
185
186    #[test]
187    fn manifest_rejects_invalid_dev_dependency_shape() {
188        let err =
189            read_manifest(&manifest_json("null", r#"[{"name":"foo.aleo","location":"workspace","path":"../foo"}]"#))
190                .unwrap_err();
191
192        assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
193        assert!(err.to_string().contains("`workspace` dependencies cannot specify `path`"));
194    }
195
196    #[test]
197    fn manifest_accepts_location_specific_dependency_fields() {
198        let manifest = read_manifest(&manifest_json(
199            r#"[
200  {"name":"network_dep.aleo","location":"network","edition":1},
201  {"name":"local_dep.aleo","location":"local","path":"../local_dep"},
202  {"name":"workspace_dep.aleo","location":"workspace"}
203]"#,
204            "null",
205        ))
206        .unwrap();
207
208        assert_eq!(manifest.dependencies.unwrap().len(), 3);
209    }
210}