lib.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. mod rust_template;
  2. use crate::rust_template::{create_component, create_system};
  3. use anchor_cli::config::{
  4. BootstrapMode, Config, ConfigOverride, GenesisEntry, ProgramArch, ProgramDeployment,
  5. TestValidator, Validator, WithPath,
  6. };
  7. use anchor_client::Cluster;
  8. use anyhow::{anyhow, Result};
  9. use clap::{Parser, Subcommand};
  10. use heck::{ToKebabCase, ToSnakeCase};
  11. use std::collections::BTreeMap;
  12. use std::fs::{self, File};
  13. use std::io::Write;
  14. use std::process::Stdio;
  15. pub const VERSION: &str = env!("CARGO_PKG_VERSION");
  16. pub const ANCHOR_VERSION: &str = anchor_cli::VERSION;
  17. pub const WORLD_PROGRAM: &str = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n";
  18. #[derive(Debug, Subcommand)]
  19. pub enum BoltCommand {
  20. #[clap(about = "Create a new component")]
  21. Component(ComponentCommand),
  22. #[clap(about = "Create a new system")]
  23. System(SystemCommand),
  24. // Include all existing commands from anchor_cli::Command
  25. #[clap(flatten)]
  26. Anchor(anchor_cli::Command),
  27. }
  28. #[derive(Debug, Parser)]
  29. pub struct InitCommand {
  30. #[clap(short, long, help = "Workspace name")]
  31. pub workspace_name: String,
  32. }
  33. #[derive(Debug, Parser)]
  34. pub struct ComponentCommand {
  35. pub name: String,
  36. }
  37. #[derive(Debug, Parser)]
  38. pub struct SystemCommand {
  39. pub name: String,
  40. }
  41. #[derive(Debug, Parser)]
  42. #[clap(version = VERSION)]
  43. pub struct Opts {
  44. #[clap(flatten)]
  45. pub cfg_override: ConfigOverride,
  46. #[clap(subcommand)]
  47. pub command: BoltCommand,
  48. }
  49. pub fn entry(opts: Opts) -> Result<()> {
  50. match opts.command {
  51. BoltCommand::Anchor(command) => match command {
  52. anchor_cli::Command::Init {
  53. name,
  54. javascript,
  55. solidity,
  56. no_git,
  57. jest,
  58. template,
  59. force,
  60. } => init(
  61. &opts.cfg_override,
  62. name,
  63. javascript,
  64. solidity,
  65. no_git,
  66. jest,
  67. template,
  68. force,
  69. ),
  70. anchor_cli::Command::Build {
  71. idl,
  72. idl_ts,
  73. verifiable,
  74. program_name,
  75. solana_version,
  76. docker_image,
  77. bootstrap,
  78. cargo_args,
  79. env,
  80. skip_lint,
  81. no_docs,
  82. arch,
  83. } => build(
  84. &opts.cfg_override,
  85. idl,
  86. idl_ts,
  87. verifiable,
  88. skip_lint,
  89. program_name,
  90. solana_version,
  91. docker_image,
  92. bootstrap,
  93. None,
  94. None,
  95. env,
  96. cargo_args,
  97. no_docs,
  98. arch,
  99. ),
  100. _ => {
  101. let opts = anchor_cli::Opts {
  102. cfg_override: opts.cfg_override,
  103. command,
  104. };
  105. anchor_cli::entry(opts)
  106. }
  107. },
  108. BoltCommand::Component(command) => new_component(&opts.cfg_override, command.name),
  109. BoltCommand::System(command) => new_system(&opts.cfg_override, command.name),
  110. }
  111. }
  112. // Bolt Init
  113. #[allow(clippy::too_many_arguments)]
  114. fn init(
  115. cfg_override: &ConfigOverride,
  116. name: String,
  117. javascript: bool,
  118. solidity: bool,
  119. no_git: bool,
  120. jest: bool,
  121. template: anchor_cli::rust_template::ProgramTemplate,
  122. force: bool,
  123. ) -> Result<()> {
  124. if !force && Config::discover(cfg_override)?.is_some() {
  125. return Err(anyhow!("Workspace already initialized"));
  126. }
  127. // We need to format different cases for the dir and the name
  128. let rust_name = name.to_snake_case();
  129. let project_name = if name == rust_name {
  130. rust_name.clone()
  131. } else {
  132. name.to_kebab_case()
  133. };
  134. // Additional keywords that have not been added to the `syn` crate as reserved words
  135. // https://github.com/dtolnay/syn/pull/1098
  136. let extra_keywords = ["async", "await", "try"];
  137. let component_name = "position";
  138. let system_name = "movement";
  139. // Anchor converts to snake case before writing the program name
  140. if syn::parse_str::<syn::Ident>(&rust_name).is_err()
  141. || extra_keywords.contains(&rust_name.as_str())
  142. {
  143. return Err(anyhow!(
  144. "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.",
  145. ));
  146. }
  147. if force {
  148. fs::create_dir_all(&project_name)?;
  149. } else {
  150. fs::create_dir(&project_name)?;
  151. }
  152. std::env::set_current_dir(&project_name)?;
  153. fs::create_dir_all("app")?;
  154. let mut cfg = Config::default();
  155. if jest {
  156. cfg.scripts.insert(
  157. "test".to_owned(),
  158. if javascript {
  159. "yarn run jest"
  160. } else {
  161. "yarn run jest --preset ts-jest"
  162. }
  163. .to_owned(),
  164. );
  165. } else {
  166. cfg.scripts.insert(
  167. "test".to_owned(),
  168. if javascript {
  169. "yarn run mocha -t 1000000 tests/"
  170. } else {
  171. "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
  172. }
  173. .to_owned(),
  174. );
  175. }
  176. let mut localnet = BTreeMap::new();
  177. let program_id = anchor_cli::rust_template::get_or_create_program_id(&rust_name);
  178. localnet.insert(
  179. rust_name,
  180. ProgramDeployment {
  181. address: program_id,
  182. path: None,
  183. idl: None,
  184. },
  185. );
  186. if !solidity {
  187. let component_id = anchor_cli::rust_template::get_or_create_program_id(component_name);
  188. let system_id = anchor_cli::rust_template::get_or_create_program_id(system_name);
  189. localnet.insert(
  190. component_name.to_owned(),
  191. ProgramDeployment {
  192. address: component_id,
  193. path: None,
  194. idl: None,
  195. },
  196. );
  197. localnet.insert(
  198. system_name.to_owned(),
  199. ProgramDeployment {
  200. address: system_id,
  201. path: None,
  202. idl: None,
  203. },
  204. );
  205. cfg.workspace.members.push("programs/*".to_owned());
  206. cfg.workspace
  207. .members
  208. .push("programs-ecs/components/*".to_owned());
  209. cfg.workspace
  210. .members
  211. .push("programs-ecs/systems/*".to_owned());
  212. }
  213. // Setup the test validator to clone Bolt programs from devnet
  214. let validator = Validator {
  215. url: Some("https://rpc.magicblock.app/devnet/".to_owned()),
  216. rpc_port: 8899,
  217. bind_address: "0.0.0.0".to_owned(),
  218. ledger: ".bolt/test-ledger".to_owned(),
  219. account: Some(vec![
  220. // Registry account
  221. anchor_cli::config::AccountEntry {
  222. address: "EHLkWwAT9oebVv9ht3mtqrvHhRVMKrt54tF3MfHTey2K".to_owned(),
  223. filename: "tests/fixtures/registry.json".to_owned(),
  224. },
  225. ]),
  226. ..Default::default()
  227. };
  228. let test_validator = TestValidator {
  229. startup_wait: 5000,
  230. shutdown_wait: 2000,
  231. validator: Some(validator),
  232. genesis: Some(vec![GenesisEntry {
  233. address: WORLD_PROGRAM.to_owned(),
  234. program: "tests/fixtures/world.so".to_owned(),
  235. upgradeable: Some(false),
  236. }]),
  237. ..Default::default()
  238. };
  239. cfg.test_validator = Some(test_validator);
  240. cfg.programs.insert(Cluster::Localnet, localnet);
  241. let toml = cfg.to_string();
  242. fs::write("Anchor.toml", toml)?;
  243. // Initialize .gitignore file
  244. fs::write(".gitignore", rust_template::git_ignore())?;
  245. // Initialize .prettierignore file
  246. fs::write(".prettierignore", rust_template::prettier_ignore())?;
  247. // Remove the default programs if `--force` is passed
  248. if force {
  249. let programs_path = std::env::current_dir()?
  250. .join(if solidity { "solidity" } else { "programs" })
  251. .join(&project_name);
  252. fs::create_dir_all(&programs_path)?;
  253. fs::remove_dir_all(&programs_path)?;
  254. let programs_ecs_path = std::env::current_dir()?
  255. .join("programs-ecs")
  256. .join(&project_name);
  257. fs::create_dir_all(&programs_ecs_path)?;
  258. fs::remove_dir_all(&programs_ecs_path)?;
  259. }
  260. // Build the program.
  261. if solidity {
  262. anchor_cli::solidity_template::create_program(&project_name)?;
  263. } else {
  264. create_system(system_name)?;
  265. create_component(component_name)?;
  266. anchor_cli::rust_template::create_program(&project_name, template)?;
  267. // Add the component as a dependency to the system
  268. std::process::Command::new("cargo")
  269. .arg("add")
  270. .arg("--package")
  271. .arg(system_name)
  272. .arg("--path")
  273. .arg(format!("programs-ecs/components/{}", component_name))
  274. .arg("--features")
  275. .arg("cpi")
  276. .stdout(Stdio::inherit())
  277. .stderr(Stdio::inherit())
  278. .spawn()
  279. .map_err(|e| {
  280. anyhow::format_err!(
  281. "error adding component as dependency to the system: {}",
  282. e.to_string()
  283. )
  284. })?;
  285. }
  286. // Build the test suite.
  287. fs::create_dir_all("tests/fixtures")?;
  288. // Build the migrations directory.
  289. fs::create_dir_all("migrations")?;
  290. // Create the registry account
  291. fs::write(
  292. "tests/fixtures/registry.json",
  293. rust_template::registry_account(),
  294. )?;
  295. // Dump the World program into tests/fixtures/world.so
  296. std::process::Command::new("solana")
  297. .arg("program")
  298. .arg("dump")
  299. .arg("-u")
  300. .arg("d")
  301. .arg(WORLD_PROGRAM)
  302. .arg("tests/fixtures/world.so")
  303. .stdout(Stdio::inherit())
  304. .stderr(Stdio::inherit())
  305. .spawn()
  306. .map_err(|e| anyhow::format_err!("solana program dump failed: {}", e.to_string()))?;
  307. if javascript {
  308. // Build javascript config
  309. let mut package_json = File::create("package.json")?;
  310. package_json.write_all(rust_template::package_json(jest).as_bytes())?;
  311. if jest {
  312. let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
  313. if solidity {
  314. test.write_all(anchor_cli::solidity_template::jest(&project_name).as_bytes())?;
  315. } else {
  316. test.write_all(rust_template::jest(&project_name).as_bytes())?;
  317. }
  318. } else {
  319. let mut test = File::create(format!("tests/{}.js", &project_name))?;
  320. if solidity {
  321. test.write_all(anchor_cli::solidity_template::mocha(&project_name).as_bytes())?;
  322. } else {
  323. test.write_all(rust_template::mocha(&project_name).as_bytes())?;
  324. }
  325. }
  326. let mut deploy = File::create("migrations/deploy.js")?;
  327. deploy.write_all(anchor_cli::rust_template::deploy_script().as_bytes())?;
  328. } else {
  329. // Build typescript config
  330. let mut ts_config = File::create("tsconfig.json")?;
  331. ts_config.write_all(anchor_cli::rust_template::ts_config(jest).as_bytes())?;
  332. let mut ts_package_json = File::create("package.json")?;
  333. ts_package_json.write_all(rust_template::ts_package_json(jest).as_bytes())?;
  334. let mut deploy = File::create("migrations/deploy.ts")?;
  335. deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?;
  336. let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
  337. if solidity {
  338. mocha.write_all(anchor_cli::solidity_template::ts_mocha(&project_name).as_bytes())?;
  339. } else {
  340. mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?;
  341. }
  342. }
  343. let yarn_result = install_node_modules("yarn")?;
  344. if !yarn_result.status.success() {
  345. println!("Failed yarn install will attempt to npm install");
  346. install_node_modules("npm")?;
  347. }
  348. if !no_git {
  349. let git_result = std::process::Command::new("git")
  350. .arg("init")
  351. .stdout(Stdio::inherit())
  352. .stderr(Stdio::inherit())
  353. .output()
  354. .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
  355. if !git_result.status.success() {
  356. eprintln!("Failed to automatically initialize a new git repository");
  357. }
  358. }
  359. println!("{project_name} initialized");
  360. Ok(())
  361. }
  362. #[allow(clippy::too_many_arguments)]
  363. pub fn build(
  364. cfg_override: &ConfigOverride,
  365. idl: Option<String>,
  366. idl_ts: Option<String>,
  367. verifiable: bool,
  368. skip_lint: bool,
  369. program_name: Option<String>,
  370. solana_version: Option<String>,
  371. docker_image: Option<String>,
  372. bootstrap: BootstrapMode,
  373. stdout: Option<File>,
  374. stderr: Option<File>,
  375. env_vars: Vec<String>,
  376. cargo_args: Vec<String>,
  377. no_docs: bool,
  378. arch: ProgramArch,
  379. ) -> Result<()> {
  380. anchor_cli::build(
  381. cfg_override,
  382. idl,
  383. idl_ts,
  384. verifiable,
  385. skip_lint,
  386. program_name,
  387. solana_version,
  388. docker_image,
  389. bootstrap,
  390. stdout,
  391. stderr,
  392. env_vars,
  393. cargo_args,
  394. no_docs,
  395. arch,
  396. )
  397. }
  398. // Install node modules
  399. fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
  400. let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
  401. "cmd"
  402. } else {
  403. cmd
  404. });
  405. if cfg!(target_os = "windows") {
  406. command.arg(format!("/C {} install", cmd));
  407. } else {
  408. command.arg("install");
  409. }
  410. command
  411. .stdout(Stdio::inherit())
  412. .stderr(Stdio::inherit())
  413. .output()
  414. .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
  415. }
  416. // Create a new component from the template
  417. fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
  418. with_workspace(cfg_override, |cfg| {
  419. match cfg.path().parent() {
  420. None => {
  421. println!("Unable to make new component");
  422. }
  423. Some(parent) => {
  424. std::env::set_current_dir(parent)?;
  425. let cluster = cfg.provider.cluster.clone();
  426. let programs = cfg.programs.entry(cluster).or_default();
  427. if programs.contains_key(&name) {
  428. return Err(anyhow!("Program already exists"));
  429. }
  430. programs.insert(
  431. name.clone(),
  432. anchor_cli::config::ProgramDeployment {
  433. address: {
  434. rust_template::create_component(&name)?;
  435. anchor_cli::rust_template::get_or_create_program_id(&name)
  436. },
  437. path: None,
  438. idl: None,
  439. },
  440. );
  441. let toml = cfg.to_string();
  442. fs::write("Anchor.toml", toml)?;
  443. println!("Created new component: {}", name);
  444. }
  445. };
  446. Ok(())
  447. })
  448. }
  449. // Create a new system from the template
  450. fn new_system(cfg_override: &ConfigOverride, name: String) -> Result<()> {
  451. with_workspace(cfg_override, |cfg| {
  452. match cfg.path().parent() {
  453. None => {
  454. println!("Unable to make new system");
  455. }
  456. Some(parent) => {
  457. std::env::set_current_dir(parent)?;
  458. let cluster = cfg.provider.cluster.clone();
  459. let programs = cfg.programs.entry(cluster).or_default();
  460. if programs.contains_key(&name) {
  461. return Err(anyhow!("Program already exists"));
  462. }
  463. programs.insert(
  464. name.clone(),
  465. anchor_cli::config::ProgramDeployment {
  466. address: {
  467. rust_template::create_system(&name)?;
  468. anchor_cli::rust_template::get_or_create_program_id(&name)
  469. },
  470. path: None,
  471. idl: None,
  472. },
  473. );
  474. let toml = cfg.to_string();
  475. fs::write("Anchor.toml", toml)?;
  476. println!("Created new system: {}", name);
  477. }
  478. };
  479. Ok(())
  480. })
  481. }
  482. // with_workspace ensures the current working directory is always the top level
  483. // workspace directory, i.e., where the `Anchor.toml` file is located, before
  484. // and after the closure invocation.
  485. //
  486. // The closure passed into this function must never change the working directory
  487. // to be outside the workspace. Doing so will have undefined behavior.
  488. fn with_workspace<R>(
  489. cfg_override: &ConfigOverride,
  490. f: impl FnOnce(&mut WithPath<Config>) -> R,
  491. ) -> R {
  492. set_workspace_dir_or_exit();
  493. let mut cfg = Config::discover(cfg_override)
  494. .expect("Previously set the workspace dir")
  495. .expect("Anchor.toml must always exist");
  496. let r = f(&mut cfg);
  497. set_workspace_dir_or_exit();
  498. r
  499. }
  500. fn set_workspace_dir_or_exit() {
  501. let d = match Config::discover(&ConfigOverride::default()) {
  502. Err(err) => {
  503. println!("Workspace configuration error: {err}");
  504. std::process::exit(1);
  505. }
  506. Ok(d) => d,
  507. };
  508. match d {
  509. None => {
  510. println!("Not in anchor workspace.");
  511. std::process::exit(1);
  512. }
  513. Some(cfg) => {
  514. match cfg.path().parent() {
  515. None => {
  516. println!("Unable to make new program");
  517. }
  518. Some(parent) => {
  519. if std::env::set_current_dir(parent).is_err() {
  520. println!("Not in anchor workspace.");
  521. std::process::exit(1);
  522. }
  523. }
  524. };
  525. }
  526. }
  527. }