leo_package/
package.rs

1// Copyright (C) 2019-2025 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    TEST_PRIVATE_KEY,
19    root::{Env, Gitignore},
20    source::{MainFile, SourceDirectory},
21};
22use leo_errors::{PackageError, Result};
23
24use leo_retriever::{Manifest, NetworkName};
25use serde::Deserialize;
26use snarkvm::prelude::{Network, PrivateKey};
27use std::{path::Path, str::FromStr};
28
29#[derive(Deserialize)]
30pub struct Package {
31    pub name: String,
32    pub version: String,
33    pub description: Option<String>,
34    pub license: Option<String>,
35    pub network: NetworkName,
36}
37
38impl Package {
39    pub fn new(package_name: &str, network: NetworkName) -> Result<Self> {
40        // Check that the package name is a valid Aleo program name.
41        if !Self::is_aleo_name_valid(package_name) {
42            return Err(PackageError::invalid_package_name(package_name).into());
43        }
44
45        Ok(Self {
46            name: package_name.to_owned(),
47            version: "0.1.0".to_owned(),
48            description: None,
49            license: None,
50            network,
51        })
52    }
53
54    /// Returns `true` if it is a valid Aleo name.
55    ///
56    /// Aleo names can only contain ASCII alphanumeric characters and underscores.
57    pub fn is_aleo_name_valid(name: &str) -> bool {
58        // Check that the name is nonempty.
59        if name.is_empty() {
60            tracing::error!("Aleo names must be nonempty");
61            return false;
62        }
63
64        let first = name.chars().next().unwrap();
65
66        // Check that the first character is not an underscore.
67        if first == '_' {
68            tracing::error!("Aleo names cannot begin with an underscore");
69            return false;
70        }
71
72        // Check that the first character is not a number.
73        if first.is_numeric() {
74            tracing::error!("Aleo names cannot begin with a number");
75            return false;
76        }
77
78        // Iterate and check that the name is valid.
79        for current in name.chars() {
80            // Check that the program name contains only ASCII alphanumeric or underscores.
81            if !current.is_ascii_alphanumeric() && current != '_' {
82                tracing::error!("Aleo names must can only contain ASCII alphanumeric characters and underscores.");
83                return false;
84            }
85        }
86
87        true
88    }
89
90    /// Returns `true` if a package is can be initialized at a given path.
91    pub fn can_initialize(package_name: &str, path: &Path) -> bool {
92        // Check that the package name is a valid Aleo program name.
93        if !Self::is_aleo_name_valid(package_name) {
94            return false;
95        }
96
97        let mut result = true;
98        let mut existing_files = vec![];
99
100        // Check if the main file already exists.
101        if MainFile::exists_at(path) {
102            existing_files.push(MainFile::filename());
103            result = false;
104        }
105
106        if !existing_files.is_empty() {
107            tracing::error!("File(s) {:?} already exist", existing_files);
108        }
109
110        result
111    }
112
113    /// Returns `true` if a package is initialized at the given path
114    pub fn is_initialized(package_name: &str, path: &Path) -> bool {
115        // Check that the package name is a valid Aleo program name.
116        if !Self::is_aleo_name_valid(package_name) {
117            return false;
118        }
119
120        // Check if the main file exists.
121        if !MainFile::exists_at(path) {
122            return false;
123        }
124
125        true
126    }
127
128    /// Creates a Leo package at the given path
129    pub fn initialize<N: Network>(package_name: &str, path: &Path, endpoint: String) -> Result<()> {
130        // Construct the path to the package directory.
131        let path = path.join(package_name);
132
133        // Verify that there is no existing directory at the path.
134        if path.exists() {
135            return Err(
136                PackageError::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
137            );
138        }
139
140        // Create the package directory.
141        std::fs::create_dir(&path).map_err(|e| PackageError::failed_to_initialize_package(package_name, &path, e))?;
142
143        // Change the current working directory to the package directory.
144        std::env::set_current_dir(&path)
145            .map_err(|e| PackageError::failed_to_initialize_package(package_name, &path, e))?;
146
147        // Create the .gitignore file.
148        Gitignore::new().write_to(&path)?;
149
150        // Create the .env file.
151        // Include the private key of validator 0 for ease of use with local devnets, as it will automatically be seeded with funds.
152        Env::<N>::new(Some(PrivateKey::<N>::from_str(TEST_PRIVATE_KEY)?), endpoint)?.write_to(&path)?;
153
154        // Create a manifest.
155        let manifest = Manifest::default(package_name);
156        manifest.write_to_dir(&path)?;
157
158        // Create the source directory.
159        SourceDirectory::create(&path)?;
160
161        // Create the main file in the source directory.
162        MainFile::new(package_name).write_to(&path)?;
163
164        // Next, verify that a valid Leo package has been initialized in this directory
165        if !Self::is_initialized(package_name, &path) {
166            return Err(PackageError::failed_to_initialize_package(
167                package_name,
168                &path,
169                "Failed to correctly initialize package",
170            )
171            .into());
172        }
173
174        Ok(())
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_is_package_name_valid() {
184        assert!(Package::is_aleo_name_valid("foo"));
185        assert!(Package::is_aleo_name_valid("foo_bar"));
186        assert!(Package::is_aleo_name_valid("foo1"));
187        assert!(Package::is_aleo_name_valid("foo_bar___baz_"));
188
189        assert!(!Package::is_aleo_name_valid("foo-bar"));
190        assert!(!Package::is_aleo_name_valid("foo-bar-baz"));
191        assert!(!Package::is_aleo_name_valid("foo-1"));
192        assert!(!Package::is_aleo_name_valid(""));
193        assert!(!Package::is_aleo_name_valid("-"));
194        assert!(!Package::is_aleo_name_valid("-foo"));
195        assert!(!Package::is_aleo_name_valid("-foo-"));
196        assert!(!Package::is_aleo_name_valid("_foo"));
197        assert!(!Package::is_aleo_name_valid("foo--bar"));
198        assert!(!Package::is_aleo_name_valid("foo---bar"));
199        assert!(!Package::is_aleo_name_valid("foo--bar--baz"));
200        assert!(!Package::is_aleo_name_valid("foo---bar---baz"));
201        assert!(!Package::is_aleo_name_valid("foo*bar"));
202        assert!(!Package::is_aleo_name_valid("foo,bar"));
203        assert!(!Package::is_aleo_name_valid("1-foo"));
204    }
205}