123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- mod rust_template;
- use crate::rust_template::{create_component, create_system};
- use anchor_cli::config::{
- BootstrapMode, Config, ConfigOverride, GenesisEntry, ProgramArch, ProgramDeployment,
- TestValidator, Validator, WithPath,
- };
- use anchor_client::Cluster;
- use anyhow::{anyhow, Result};
- use clap::{Parser, Subcommand};
- use heck::{ToKebabCase, ToSnakeCase};
- use std::collections::BTreeMap;
- use std::fs::{self, File};
- use std::io::Write;
- use std::process::Stdio;
- pub const VERSION: &str = env!("CARGO_PKG_VERSION");
- pub const ANCHOR_VERSION: &str = anchor_cli::VERSION;
- pub const WORLD_PROGRAM: &str = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n";
- #[derive(Debug, Subcommand)]
- pub enum BoltCommand {
- #[clap(about = "Create a new component")]
- Component(ComponentCommand),
- #[clap(about = "Create a new system")]
- System(SystemCommand),
- // Include all existing commands from anchor_cli::Command
- #[clap(flatten)]
- Anchor(anchor_cli::Command),
- }
- #[derive(Debug, Parser)]
- pub struct InitCommand {
- #[clap(short, long, help = "Workspace name")]
- pub workspace_name: String,
- }
- #[derive(Debug, Parser)]
- pub struct ComponentCommand {
- pub name: String,
- }
- #[derive(Debug, Parser)]
- pub struct SystemCommand {
- pub name: String,
- }
- #[derive(Debug, Parser)]
- #[clap(version = VERSION)]
- pub struct Opts {
- #[clap(flatten)]
- pub cfg_override: ConfigOverride,
- #[clap(subcommand)]
- pub command: BoltCommand,
- }
- pub fn entry(opts: Opts) -> Result<()> {
- match opts.command {
- BoltCommand::Anchor(command) => match command {
- anchor_cli::Command::Init {
- name,
- javascript,
- solidity,
- no_git,
- jest,
- template,
- force,
- } => init(
- &opts.cfg_override,
- name,
- javascript,
- solidity,
- no_git,
- jest,
- template,
- force,
- ),
- anchor_cli::Command::Build {
- idl,
- idl_ts,
- verifiable,
- program_name,
- solana_version,
- docker_image,
- bootstrap,
- cargo_args,
- env,
- skip_lint,
- no_docs,
- arch,
- } => build(
- &opts.cfg_override,
- idl,
- idl_ts,
- verifiable,
- skip_lint,
- program_name,
- solana_version,
- docker_image,
- bootstrap,
- None,
- None,
- env,
- cargo_args,
- no_docs,
- arch,
- ),
- _ => {
- let opts = anchor_cli::Opts {
- cfg_override: opts.cfg_override,
- command,
- };
- anchor_cli::entry(opts)
- }
- },
- BoltCommand::Component(command) => new_component(&opts.cfg_override, command.name),
- BoltCommand::System(command) => new_system(&opts.cfg_override, command.name),
- }
- }
- // Bolt Init
- #[allow(clippy::too_many_arguments)]
- fn init(
- cfg_override: &ConfigOverride,
- name: String,
- javascript: bool,
- solidity: bool,
- no_git: bool,
- jest: bool,
- template: anchor_cli::rust_template::ProgramTemplate,
- force: bool,
- ) -> Result<()> {
- if !force && Config::discover(cfg_override)?.is_some() {
- return Err(anyhow!("Workspace already initialized"));
- }
- // We need to format different cases for the dir and the name
- let rust_name = name.to_snake_case();
- let project_name = if name == rust_name {
- rust_name.clone()
- } else {
- name.to_kebab_case()
- };
- // Additional keywords that have not been added to the `syn` crate as reserved words
- // https://github.com/dtolnay/syn/pull/1098
- let extra_keywords = ["async", "await", "try"];
- let component_name = "position";
- let system_name = "movement";
- // Anchor converts to snake case before writing the program name
- if syn::parse_str::<syn::Ident>(&rust_name).is_err()
- || extra_keywords.contains(&rust_name.as_str())
- {
- return Err(anyhow!(
- "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.",
- ));
- }
- if force {
- fs::create_dir_all(&project_name)?;
- } else {
- fs::create_dir(&project_name)?;
- }
- std::env::set_current_dir(&project_name)?;
- fs::create_dir_all("app")?;
- let mut cfg = Config::default();
- if jest {
- cfg.scripts.insert(
- "test".to_owned(),
- if javascript {
- "yarn run jest"
- } else {
- "yarn run jest --preset ts-jest"
- }
- .to_owned(),
- );
- } else {
- cfg.scripts.insert(
- "test".to_owned(),
- if javascript {
- "yarn run mocha -t 1000000 tests/"
- } else {
- "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
- }
- .to_owned(),
- );
- }
- let mut localnet = BTreeMap::new();
- let program_id = anchor_cli::rust_template::get_or_create_program_id(&rust_name);
- localnet.insert(
- rust_name,
- ProgramDeployment {
- address: program_id,
- path: None,
- idl: None,
- },
- );
- if !solidity {
- let component_id = anchor_cli::rust_template::get_or_create_program_id(component_name);
- let system_id = anchor_cli::rust_template::get_or_create_program_id(system_name);
- localnet.insert(
- component_name.to_owned(),
- ProgramDeployment {
- address: component_id,
- path: None,
- idl: None,
- },
- );
- localnet.insert(
- system_name.to_owned(),
- ProgramDeployment {
- address: system_id,
- path: None,
- idl: None,
- },
- );
- cfg.workspace.members.push("programs/*".to_owned());
- cfg.workspace
- .members
- .push("programs-ecs/components/*".to_owned());
- cfg.workspace
- .members
- .push("programs-ecs/systems/*".to_owned());
- }
- // Setup the test validator to clone Bolt programs from devnet
- let validator = Validator {
- url: Some("https://rpc.magicblock.app/devnet/".to_owned()),
- rpc_port: 8899,
- bind_address: "0.0.0.0".to_owned(),
- ledger: ".bolt/test-ledger".to_owned(),
- account: Some(vec![
- // Registry account
- anchor_cli::config::AccountEntry {
- address: "EHLkWwAT9oebVv9ht3mtqrvHhRVMKrt54tF3MfHTey2K".to_owned(),
- filename: "tests/fixtures/registry.json".to_owned(),
- },
- ]),
- ..Default::default()
- };
- let test_validator = TestValidator {
- startup_wait: 5000,
- shutdown_wait: 2000,
- validator: Some(validator),
- genesis: Some(vec![GenesisEntry {
- address: WORLD_PROGRAM.to_owned(),
- program: "tests/fixtures/world.so".to_owned(),
- upgradeable: Some(false),
- }]),
- ..Default::default()
- };
- cfg.test_validator = Some(test_validator);
- cfg.programs.insert(Cluster::Localnet, localnet);
- let toml = cfg.to_string();
- fs::write("Anchor.toml", toml)?;
- // Initialize .gitignore file
- fs::write(".gitignore", rust_template::git_ignore())?;
- // Initialize .prettierignore file
- fs::write(".prettierignore", rust_template::prettier_ignore())?;
- // Remove the default programs if `--force` is passed
- if force {
- let programs_path = std::env::current_dir()?
- .join(if solidity { "solidity" } else { "programs" })
- .join(&project_name);
- fs::create_dir_all(&programs_path)?;
- fs::remove_dir_all(&programs_path)?;
- let programs_ecs_path = std::env::current_dir()?
- .join("programs-ecs")
- .join(&project_name);
- fs::create_dir_all(&programs_ecs_path)?;
- fs::remove_dir_all(&programs_ecs_path)?;
- }
- // Build the program.
- if solidity {
- anchor_cli::solidity_template::create_program(&project_name)?;
- } else {
- create_system(system_name)?;
- create_component(component_name)?;
- anchor_cli::rust_template::create_program(&project_name, template)?;
- // Add the component as a dependency to the system
- std::process::Command::new("cargo")
- .arg("add")
- .arg("--package")
- .arg(system_name)
- .arg("--path")
- .arg(format!("programs-ecs/components/{}", component_name))
- .arg("--features")
- .arg("cpi")
- .stdout(Stdio::inherit())
- .stderr(Stdio::inherit())
- .spawn()
- .map_err(|e| {
- anyhow::format_err!(
- "error adding component as dependency to the system: {}",
- e.to_string()
- )
- })?;
- }
- // Build the test suite.
- fs::create_dir_all("tests/fixtures")?;
- // Build the migrations directory.
- fs::create_dir_all("migrations")?;
- // Create the registry account
- fs::write(
- "tests/fixtures/registry.json",
- rust_template::registry_account(),
- )?;
- // Dump the World program into tests/fixtures/world.so
- std::process::Command::new("solana")
- .arg("program")
- .arg("dump")
- .arg("-u")
- .arg("d")
- .arg(WORLD_PROGRAM)
- .arg("tests/fixtures/world.so")
- .stdout(Stdio::inherit())
- .stderr(Stdio::inherit())
- .spawn()
- .map_err(|e| anyhow::format_err!("solana program dump failed: {}", e.to_string()))?;
- if javascript {
- // Build javascript config
- let mut package_json = File::create("package.json")?;
- package_json.write_all(rust_template::package_json(jest).as_bytes())?;
- if jest {
- let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
- if solidity {
- test.write_all(anchor_cli::solidity_template::jest(&project_name).as_bytes())?;
- } else {
- test.write_all(rust_template::jest(&project_name).as_bytes())?;
- }
- } else {
- let mut test = File::create(format!("tests/{}.js", &project_name))?;
- if solidity {
- test.write_all(anchor_cli::solidity_template::mocha(&project_name).as_bytes())?;
- } else {
- test.write_all(rust_template::mocha(&project_name).as_bytes())?;
- }
- }
- let mut deploy = File::create("migrations/deploy.js")?;
- deploy.write_all(anchor_cli::rust_template::deploy_script().as_bytes())?;
- } else {
- // Build typescript config
- let mut ts_config = File::create("tsconfig.json")?;
- ts_config.write_all(anchor_cli::rust_template::ts_config(jest).as_bytes())?;
- let mut ts_package_json = File::create("package.json")?;
- ts_package_json.write_all(rust_template::ts_package_json(jest).as_bytes())?;
- let mut deploy = File::create("migrations/deploy.ts")?;
- deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?;
- let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
- if solidity {
- mocha.write_all(anchor_cli::solidity_template::ts_mocha(&project_name).as_bytes())?;
- } else {
- mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?;
- }
- }
- let yarn_result = install_node_modules("yarn")?;
- if !yarn_result.status.success() {
- println!("Failed yarn install will attempt to npm install");
- install_node_modules("npm")?;
- }
- if !no_git {
- let git_result = std::process::Command::new("git")
- .arg("init")
- .stdout(Stdio::inherit())
- .stderr(Stdio::inherit())
- .output()
- .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
- if !git_result.status.success() {
- eprintln!("Failed to automatically initialize a new git repository");
- }
- }
- println!("{project_name} initialized");
- Ok(())
- }
- #[allow(clippy::too_many_arguments)]
- pub fn build(
- cfg_override: &ConfigOverride,
- idl: Option<String>,
- idl_ts: Option<String>,
- verifiable: bool,
- skip_lint: bool,
- program_name: Option<String>,
- solana_version: Option<String>,
- docker_image: Option<String>,
- bootstrap: BootstrapMode,
- stdout: Option<File>,
- stderr: Option<File>,
- env_vars: Vec<String>,
- cargo_args: Vec<String>,
- no_docs: bool,
- arch: ProgramArch,
- ) -> Result<()> {
- anchor_cli::build(
- cfg_override,
- idl,
- idl_ts,
- verifiable,
- skip_lint,
- program_name,
- solana_version,
- docker_image,
- bootstrap,
- stdout,
- stderr,
- env_vars,
- cargo_args,
- no_docs,
- arch,
- )
- }
- // Install node modules
- fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
- let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
- "cmd"
- } else {
- cmd
- });
- if cfg!(target_os = "windows") {
- command.arg(format!("/C {} install", cmd));
- } else {
- command.arg("install");
- }
- command
- .stdout(Stdio::inherit())
- .stderr(Stdio::inherit())
- .output()
- .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
- }
- // Create a new component from the template
- fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
- with_workspace(cfg_override, |cfg| {
- match cfg.path().parent() {
- None => {
- println!("Unable to make new component");
- }
- Some(parent) => {
- std::env::set_current_dir(parent)?;
- let cluster = cfg.provider.cluster.clone();
- let programs = cfg.programs.entry(cluster).or_default();
- if programs.contains_key(&name) {
- return Err(anyhow!("Program already exists"));
- }
- programs.insert(
- name.clone(),
- anchor_cli::config::ProgramDeployment {
- address: {
- rust_template::create_component(&name)?;
- anchor_cli::rust_template::get_or_create_program_id(&name)
- },
- path: None,
- idl: None,
- },
- );
- let toml = cfg.to_string();
- fs::write("Anchor.toml", toml)?;
- println!("Created new component: {}", name);
- }
- };
- Ok(())
- })
- }
- // Create a new system from the template
- fn new_system(cfg_override: &ConfigOverride, name: String) -> Result<()> {
- with_workspace(cfg_override, |cfg| {
- match cfg.path().parent() {
- None => {
- println!("Unable to make new system");
- }
- Some(parent) => {
- std::env::set_current_dir(parent)?;
- let cluster = cfg.provider.cluster.clone();
- let programs = cfg.programs.entry(cluster).or_default();
- if programs.contains_key(&name) {
- return Err(anyhow!("Program already exists"));
- }
- programs.insert(
- name.clone(),
- anchor_cli::config::ProgramDeployment {
- address: {
- rust_template::create_system(&name)?;
- anchor_cli::rust_template::get_or_create_program_id(&name)
- },
- path: None,
- idl: None,
- },
- );
- let toml = cfg.to_string();
- fs::write("Anchor.toml", toml)?;
- println!("Created new system: {}", name);
- }
- };
- Ok(())
- })
- }
- // with_workspace ensures the current working directory is always the top level
- // workspace directory, i.e., where the `Anchor.toml` file is located, before
- // and after the closure invocation.
- //
- // The closure passed into this function must never change the working directory
- // to be outside the workspace. Doing so will have undefined behavior.
- fn with_workspace<R>(
- cfg_override: &ConfigOverride,
- f: impl FnOnce(&mut WithPath<Config>) -> R,
- ) -> R {
- set_workspace_dir_or_exit();
- let mut cfg = Config::discover(cfg_override)
- .expect("Previously set the workspace dir")
- .expect("Anchor.toml must always exist");
- let r = f(&mut cfg);
- set_workspace_dir_or_exit();
- r
- }
- fn set_workspace_dir_or_exit() {
- let d = match Config::discover(&ConfigOverride::default()) {
- Err(err) => {
- println!("Workspace configuration error: {err}");
- std::process::exit(1);
- }
- Ok(d) => d,
- };
- match d {
- None => {
- println!("Not in anchor workspace.");
- std::process::exit(1);
- }
- Some(cfg) => {
- match cfg.path().parent() {
- None => {
- println!("Unable to make new program");
- }
- Some(parent) => {
- if std::env::set_current_dir(parent).is_err() {
- println!("Not in anchor workspace.");
- std::process::exit(1);
- }
- }
- };
- }
- }
- }
|