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    #[serde(default, skip_serializing_if = "core::ops::Not::not")]
38    pub no_std: bool,
39}
40
41impl Manifest {
42    /// Write the manifest to the given `path` as a JSON string.
43    pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), Backtraced> {
44        // Serialize the manifest to a JSON string.
45        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        // The seralized string doesn't end in a newline.
49        contents.push('\n');
50
51        // Write the manifest to the file.
52        std::fs::write(path, contents).map_err(crate::errors::failed_to_write_manifest)
53    }
54
55    /// Read and validate a Manifest from the given JSON file.
56    pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self, Backtraced> {
57        // Read the manifest file.
58        let contents = std::fs::read_to_string(&path)
59            .map_err(|_| crate::errors::failed_to_load_package(path.as_ref().display()))?;
60        // Deserialize the manifest.
61        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
90// Returns the current version of Leo.
91fn current_version() -> String {
92    env!("CARGO_PKG_VERSION").to_string()
93}
94
95impl Dependency {
96    fn validate_manifest_shape(&self) -> Result<(), Backtraced> {
97        // Reject any field that has no meaning for the dependency's location, so a stray field
98        // (e.g. `git` on a `local` dependency) errors instead of being silently ignored.
99        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            // The name is matched against package manifests in the checkout, so validate it at
130            // this trust boundary.
131            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}