df_sol/
lib.rs

1use crate::rust_template::{create_anchor_toml, ProgramTemplate};
2use anyhow::{anyhow, Result};
3use clap::Parser;
4use heck::{ToKebabCase, ToSnakeCase};
5use solana_sdk::signature::Keypair;
6use std::fs::{self, File};
7use std::io::prelude::*;
8use std::path::{Path, PathBuf};
9use std::process::Stdio;
10use std::string::ToString;
11
12pub mod rust_template;
13const VERSION: &str = env!("CARGO_PKG_VERSION");
14#[derive(Debug, Parser)]
15#[clap(version = VERSION)]
16pub struct Opts {
17    #[clap(subcommand)]
18    pub command: Command,
19}
20
21#[derive(Debug, Parser)]
22pub enum Command {
23    Init {
24        /// Workspace name
25        name: String,
26        /// Don't install JavaScript dependencies
27        #[clap(long)]
28        no_install: bool,
29        /// Don't initialize git
30        #[clap(long)]
31        no_git: bool,
32        /// Rust program template to use
33        #[clap(value_enum, short, long, default_value = "basic")]
34        template: ProgramTemplate,
35        /// Initialize even if there are files
36        #[clap(long, action)]
37        force: bool,
38    },
39}
40
41pub fn entry(opts: Opts) -> Result<()> {
42    let result = process_command(opts);
43
44    result
45}
46
47fn process_command(opts: Opts) -> Result<()> {
48    match opts.command {
49        Command::Init {
50            name,
51            no_install,
52            no_git,
53            template,
54            force,
55        } => init(name, no_install, no_git, template, force),
56    }
57}
58
59#[allow(clippy::too_many_arguments)]
60fn init(
61    name: String,
62    no_install: bool,
63    no_git: bool,
64    template: ProgramTemplate,
65    force: bool,
66) -> Result<()> {
67    // We need to format different cases for the dir and the name
68    let rust_name = name.to_snake_case();
69    let project_name = if name == rust_name {
70        rust_name.clone()
71    } else {
72        name.to_kebab_case()
73    };
74
75    // Additional keywords that have not been added to the `syn` crate as reserved words
76    // https://github.com/dtolnay/syn/pull/1098
77    let extra_keywords = ["async", "await", "try"];
78    // Anchor converts to snake case before writing the program name
79    if syn::parse_str::<syn::Ident>(&rust_name).is_err()
80        || extra_keywords.contains(&rust_name.as_str())
81    {
82        return Err(anyhow!(
83            "Anchor workspace name must be a valid Rust identifier. It may not be a Rust reserved word, start with a digit, or include certain disallowed characters. See https://doc.rust-lang.org/reference/identifiers.html for more detail.",
84        ));
85    }
86
87    if force {
88        fs::create_dir_all(&project_name)?;
89    } else {
90        fs::create_dir(&project_name)?;
91    }
92    std::env::set_current_dir(&project_name)?;
93    fs::create_dir_all("app")?;
94
95    let test_script = rust_template::get_test_script();
96    let program_id = rust_template::get_or_create_program_id(&rust_name);
97    let toml = create_anchor_toml(program_id.to_string(), test_script.to_string(), template);
98    fs::write("Anchor.toml", toml)?;
99
100    // Initialize .gitignore file
101    fs::write(".gitignore", rust_template::git_ignore())?;
102
103    // Initialize .prettierignore file
104    fs::write(".prettierignore", rust_template::prettier_ignore())?;
105
106    // Initialize wallet.json
107    fs::write("wallet.json", create_keypair())?;
108
109    // Initialize README.md
110    fs::write("README.md", rust_template::readme(template))?;
111
112    // Initialize devbox.json
113    fs::write("devbox.json", rust_template::devbox_json())?;
114
115    // Remove the default program if `--force` is passed
116    if force {
117        fs::remove_dir_all(
118            std::env::current_dir()?
119                .join("programs")
120                .join(&project_name),
121        )?;
122    }
123
124    // Build the program.
125    rust_template::create_program(&project_name, template)?;
126
127    // Build the migrations directory.
128    fs::create_dir_all("migrations")?;
129
130    let license = get_npm_init_license()?;
131
132    // Build typescript config
133    let mut ts_config = File::create("tsconfig.json")?;
134    ts_config.write_all(rust_template::ts_config().as_bytes())?;
135
136    let mut ts_package_json = File::create("package.json")?;
137    ts_package_json.write_all(rust_template::ts_package_json(license, template).as_bytes())?;
138
139    let mut deploy = File::create("migrations/deploy.ts")?;
140    deploy.write_all(rust_template::ts_deploy_script().as_bytes())?;
141
142    rust_template::create_test_files(&project_name, template)?;
143
144    if !no_install {
145        let yarn_result = install_node_modules("yarn")?;
146        if !yarn_result.status.success() {
147            println!("Failed yarn install will attempt to npm install");
148            install_node_modules("npm")?;
149        }
150    }
151
152    if !no_git {
153        let git_result = std::process::Command::new("git")
154            .arg("init")
155            .stdout(Stdio::inherit())
156            .stderr(Stdio::inherit())
157            .output()
158            .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
159        if !git_result.status.success() {
160            eprintln!("Failed to automatically initialize a new git repository");
161        }
162    }
163
164    println!("{project_name} initialized");
165
166    Ok(())
167}
168
169/// Array of (path, content) tuple.
170pub type Files = Vec<(PathBuf, String)>;
171
172/// Create files from the given (path, content) tuple array.
173///
174/// # Example
175///
176/// ```ignore
177/// crate_files(vec![("programs/my_program/src/lib.rs".into(), "// Content".into())])?;
178/// ```
179pub fn create_files(files: &Files) -> Result<()> {
180    for (path, content) in files {
181        let path = Path::new(path);
182        if path.exists() {
183            continue;
184        }
185
186        match path.extension() {
187            Some(_) => {
188                fs::create_dir_all(path.parent().unwrap())?;
189                fs::write(path, content)?;
190            }
191            None => fs::create_dir_all(path)?,
192        }
193    }
194
195    Ok(())
196}
197
198/// Override or create files from the given (path, content) tuple array.
199///
200/// # Example
201///
202/// ```ignore
203/// override_or_create_files(vec![("programs/my_program/src/lib.rs".into(), "// Content".into())])?;
204/// ```
205pub fn override_or_create_files(files: &Files) -> Result<()> {
206    for (path, content) in files {
207        let path = Path::new(path);
208        if path.exists() {
209            let mut f = fs::OpenOptions::new()
210                .write(true)
211                .truncate(true)
212                .open(path)?;
213            f.write_all(content.as_bytes())?;
214            f.flush()?;
215        } else {
216            fs::create_dir_all(path.parent().unwrap())?;
217            fs::write(path, content)?;
218        }
219    }
220
221    Ok(())
222}
223
224fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
225    if cfg!(target_os = "windows") {
226        std::process::Command::new("cmd")
227            .arg(format!("/C {cmd} install"))
228            .stdout(Stdio::inherit())
229            .stderr(Stdio::inherit())
230            .output()
231            .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
232    } else {
233        std::process::Command::new(cmd)
234            .arg("install")
235            .stdout(Stdio::inherit())
236            .stderr(Stdio::inherit())
237            .output()
238            .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
239    }
240}
241
242/// Get the system's default license - what 'npm init' would use.
243fn get_npm_init_license() -> Result<String> {
244    let npm_init_license_output = std::process::Command::new("npm")
245        .arg("config")
246        .arg("get")
247        .arg("init-license")
248        .output()?;
249
250    if !npm_init_license_output.status.success() {
251        return Err(anyhow!("Failed to get npm init license"));
252    }
253
254    let license = String::from_utf8(npm_init_license_output.stdout)?;
255    Ok(license.trim().to_string())
256}
257
258fn create_keypair() -> String {
259    let keypair = Keypair::new();
260    let keypair_bytes = keypair.to_bytes();
261    // Convert keypair to base58 strings
262    let serialized = serde_json::to_string(&keypair_bytes.to_vec());
263
264    match serialized {
265        Ok(v) => return v,
266        Err(_e) => return "".parse().unwrap(),
267    }
268}