lib.rs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. mod rust_template;
  2. use crate::rust_template::{create_component, create_system};
  3. use anchor_cli::config;
  4. use anchor_cli::config::{
  5. BootstrapMode, Config, ConfigOverride, GenesisEntry, ProgramArch, ProgramDeployment,
  6. TestValidator, Validator, WithPath,
  7. };
  8. use anchor_client::Cluster;
  9. use anchor_lang_idl::types::Idl;
  10. use anyhow::{anyhow, Result};
  11. use clap::{Parser, Subcommand};
  12. use heck::{ToKebabCase, ToSnakeCase};
  13. use std::collections::BTreeMap;
  14. use std::fs::{self, create_dir_all, File, OpenOptions};
  15. use std::io::Write;
  16. use std::io::{self, BufRead};
  17. use std::path::{Path, PathBuf};
  18. use std::process::Stdio;
  19. pub const VERSION: &str = env!("CARGO_PKG_VERSION");
  20. pub const ANCHOR_VERSION: &str = anchor_cli::VERSION;
  21. pub const WORLD_PROGRAM: &str = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n";
  22. #[derive(Debug, Subcommand)]
  23. pub enum BoltCommand {
  24. #[clap(about = "Create a new component")]
  25. Component(ComponentCommand),
  26. #[clap(about = "Create a new system")]
  27. System(SystemCommand),
  28. // Include all existing commands from anchor_cli::Command
  29. #[clap(flatten)]
  30. Anchor(anchor_cli::Command),
  31. }
  32. #[derive(Debug, Parser)]
  33. pub struct InitCommand {
  34. #[clap(short, long, help = "Workspace name")]
  35. pub workspace_name: String,
  36. }
  37. #[derive(Debug, Parser)]
  38. pub struct ComponentCommand {
  39. pub name: String,
  40. }
  41. #[derive(Debug, Parser)]
  42. pub struct SystemCommand {
  43. pub name: String,
  44. }
  45. #[derive(Debug, Parser)]
  46. #[clap(version = VERSION)]
  47. pub struct Opts {
  48. /// Rebuild the auto-generated types
  49. #[clap(global = true, long, action)]
  50. pub rebuild_types: bool,
  51. #[clap(flatten)]
  52. pub cfg_override: ConfigOverride,
  53. #[clap(subcommand)]
  54. pub command: BoltCommand,
  55. }
  56. pub fn entry(opts: Opts) -> Result<()> {
  57. match opts.command {
  58. BoltCommand::Anchor(command) => match command {
  59. anchor_cli::Command::Init {
  60. name,
  61. javascript,
  62. solidity,
  63. no_install,
  64. no_git,
  65. template,
  66. test_template,
  67. force,
  68. } => init(
  69. &opts.cfg_override,
  70. name,
  71. javascript,
  72. solidity,
  73. no_install,
  74. no_git,
  75. template,
  76. test_template,
  77. force,
  78. ),
  79. anchor_cli::Command::Build {
  80. idl,
  81. no_idl,
  82. idl_ts,
  83. verifiable,
  84. program_name,
  85. solana_version,
  86. docker_image,
  87. bootstrap,
  88. cargo_args,
  89. env,
  90. skip_lint,
  91. no_docs,
  92. arch,
  93. } => build(
  94. &opts.cfg_override,
  95. no_idl,
  96. idl,
  97. idl_ts,
  98. verifiable,
  99. skip_lint,
  100. program_name,
  101. solana_version,
  102. docker_image,
  103. bootstrap,
  104. None,
  105. None,
  106. env,
  107. cargo_args,
  108. no_docs,
  109. arch,
  110. opts.rebuild_types,
  111. ),
  112. _ => {
  113. let opts = anchor_cli::Opts {
  114. cfg_override: opts.cfg_override,
  115. command,
  116. };
  117. anchor_cli::entry(opts)
  118. }
  119. },
  120. BoltCommand::Component(command) => new_component(&opts.cfg_override, command.name),
  121. BoltCommand::System(command) => new_system(&opts.cfg_override, command.name),
  122. }
  123. }
  124. // Bolt Init
  125. #[allow(clippy::too_many_arguments)]
  126. fn init(
  127. cfg_override: &ConfigOverride,
  128. name: String,
  129. javascript: bool,
  130. solidity: bool,
  131. no_install: bool,
  132. no_git: bool,
  133. template: anchor_cli::rust_template::ProgramTemplate,
  134. test_template: anchor_cli::rust_template::TestTemplate,
  135. force: bool,
  136. ) -> Result<()> {
  137. if !force && Config::discover(cfg_override)?.is_some() {
  138. return Err(anyhow!("Workspace already initialized"));
  139. }
  140. // We need to format different cases for the dir and the name
  141. let rust_name = name.to_snake_case();
  142. let project_name = if name == rust_name {
  143. rust_name.clone()
  144. } else {
  145. name.to_kebab_case()
  146. };
  147. // Additional keywords that have not been added to the `syn` crate as reserved words
  148. // https://github.com/dtolnay/syn/pull/1098
  149. let extra_keywords = ["async", "await", "try"];
  150. let component_name = "position";
  151. let system_name = "movement";
  152. // Anchor converts to snake case before writing the program name
  153. if syn::parse_str::<syn::Ident>(&rust_name).is_err()
  154. || extra_keywords.contains(&rust_name.as_str())
  155. {
  156. return Err(anyhow!(
  157. "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.",
  158. ));
  159. }
  160. if force {
  161. fs::create_dir_all(&project_name)?;
  162. } else {
  163. fs::create_dir(&project_name)?;
  164. }
  165. std::env::set_current_dir(&project_name)?;
  166. fs::create_dir_all("app")?;
  167. let mut cfg = Config::default();
  168. let jest = test_template == anchor_cli::rust_template::TestTemplate::Jest;
  169. if jest {
  170. cfg.scripts.insert(
  171. "test".to_owned(),
  172. if javascript {
  173. "yarn run jest"
  174. } else {
  175. "yarn run jest --preset ts-jest"
  176. }
  177. .to_owned(),
  178. );
  179. } else {
  180. cfg.scripts.insert(
  181. "test".to_owned(),
  182. if javascript {
  183. "yarn run mocha -t 1000000 tests/"
  184. } else {
  185. "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
  186. }
  187. .to_owned(),
  188. );
  189. }
  190. let mut localnet = BTreeMap::new();
  191. let program_id = anchor_cli::rust_template::get_or_create_program_id(&rust_name);
  192. localnet.insert(
  193. rust_name,
  194. ProgramDeployment {
  195. address: program_id,
  196. path: None,
  197. idl: None,
  198. },
  199. );
  200. if !solidity {
  201. let component_id = anchor_cli::rust_template::get_or_create_program_id(component_name);
  202. let system_id = anchor_cli::rust_template::get_or_create_program_id(system_name);
  203. localnet.insert(
  204. component_name.to_owned(),
  205. ProgramDeployment {
  206. address: component_id,
  207. path: None,
  208. idl: None,
  209. },
  210. );
  211. localnet.insert(
  212. system_name.to_owned(),
  213. ProgramDeployment {
  214. address: system_id,
  215. path: None,
  216. idl: None,
  217. },
  218. );
  219. cfg.workspace.members.push("programs/*".to_owned());
  220. cfg.workspace
  221. .members
  222. .push("programs-ecs/components/*".to_owned());
  223. cfg.workspace
  224. .members
  225. .push("programs-ecs/systems/*".to_owned());
  226. }
  227. // Setup the test validator to clone Bolt programs from devnet
  228. let validator = Validator {
  229. url: Some("https://rpc.magicblock.app/devnet/".to_owned()),
  230. rpc_port: 8899,
  231. bind_address: "0.0.0.0".to_owned(),
  232. ledger: ".bolt/test-ledger".to_owned(),
  233. account: Some(vec![
  234. // Registry account
  235. anchor_cli::config::AccountEntry {
  236. address: "EHLkWwAT9oebVv9ht3mtqrvHhRVMKrt54tF3MfHTey2K".to_owned(),
  237. filename: "tests/fixtures/registry.json".to_owned(),
  238. },
  239. ]),
  240. ..Default::default()
  241. };
  242. let test_validator = TestValidator {
  243. startup_wait: 5000,
  244. shutdown_wait: 2000,
  245. validator: Some(validator),
  246. genesis: Some(vec![GenesisEntry {
  247. address: WORLD_PROGRAM.to_owned(),
  248. program: "tests/fixtures/world.so".to_owned(),
  249. upgradeable: Some(false),
  250. }]),
  251. ..Default::default()
  252. };
  253. cfg.test_validator = Some(test_validator);
  254. cfg.programs.insert(Cluster::Localnet, localnet);
  255. let toml = cfg.to_string();
  256. fs::write("Anchor.toml", toml)?;
  257. // Initialize .gitignore file
  258. fs::write(".gitignore", rust_template::git_ignore())?;
  259. // Initialize .prettierignore file
  260. fs::write(".prettierignore", rust_template::prettier_ignore())?;
  261. // Remove the default programs if `--force` is passed
  262. if force {
  263. let programs_path = std::env::current_dir()?
  264. .join(if solidity { "solidity" } else { "programs" })
  265. .join(&project_name);
  266. fs::create_dir_all(&programs_path)?;
  267. fs::remove_dir_all(&programs_path)?;
  268. let programs_ecs_path = std::env::current_dir()?
  269. .join("programs-ecs")
  270. .join(&project_name);
  271. fs::create_dir_all(&programs_ecs_path)?;
  272. fs::remove_dir_all(&programs_ecs_path)?;
  273. }
  274. // Build the program.
  275. if solidity {
  276. anchor_cli::solidity_template::create_program(&project_name)?;
  277. } else {
  278. create_system(system_name)?;
  279. create_component(component_name)?;
  280. rust_template::create_program(&project_name, template)?;
  281. // Add the component as a dependency to the system
  282. std::process::Command::new("cargo")
  283. .arg("add")
  284. .arg("--package")
  285. .arg(system_name)
  286. .arg("--path")
  287. .arg(format!("programs-ecs/components/{}", component_name))
  288. .arg("--features")
  289. .arg("cpi")
  290. .stdout(std::process::Stdio::null())
  291. .stderr(std::process::Stdio::null())
  292. .spawn()
  293. .map_err(|e| {
  294. anyhow::format_err!(
  295. "error adding component as dependency to the system: {}",
  296. e.to_string()
  297. )
  298. })?;
  299. }
  300. // Build the test suite.
  301. fs::create_dir_all("tests/fixtures")?;
  302. // Build the migrations directory.
  303. fs::create_dir_all("migrations")?;
  304. // Create the registry account
  305. fs::write(
  306. "tests/fixtures/registry.json",
  307. rust_template::registry_account(),
  308. )?;
  309. // Dump the World program into tests/fixtures/world.so
  310. std::process::Command::new("solana")
  311. .arg("program")
  312. .arg("dump")
  313. .arg("-u")
  314. .arg("d")
  315. .arg(WORLD_PROGRAM)
  316. .arg("tests/fixtures/world.so")
  317. .stdout(Stdio::inherit())
  318. .stderr(Stdio::inherit())
  319. .spawn()
  320. .map_err(|e| anyhow::format_err!("solana program dump failed: {}", e.to_string()))?;
  321. if javascript {
  322. // Build javascript config
  323. let mut package_json = File::create("package.json")?;
  324. package_json.write_all(rust_template::package_json(jest).as_bytes())?;
  325. if jest {
  326. let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
  327. if solidity {
  328. test.write_all(anchor_cli::solidity_template::jest(&project_name).as_bytes())?;
  329. } else {
  330. test.write_all(rust_template::jest(&project_name).as_bytes())?;
  331. }
  332. } else {
  333. let mut test = File::create(format!("tests/{}.js", &project_name))?;
  334. if solidity {
  335. test.write_all(anchor_cli::solidity_template::mocha(&project_name).as_bytes())?;
  336. } else {
  337. test.write_all(rust_template::mocha(&project_name).as_bytes())?;
  338. }
  339. }
  340. let mut deploy = File::create("migrations/deploy.js")?;
  341. deploy.write_all(anchor_cli::rust_template::deploy_script().as_bytes())?;
  342. } else {
  343. // Build typescript config
  344. let mut ts_config = File::create("tsconfig.json")?;
  345. ts_config.write_all(anchor_cli::rust_template::ts_config(jest).as_bytes())?;
  346. let mut ts_package_json = File::create("package.json")?;
  347. ts_package_json.write_all(rust_template::ts_package_json(jest).as_bytes())?;
  348. let mut deploy = File::create("migrations/deploy.ts")?;
  349. deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?;
  350. let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
  351. if solidity {
  352. mocha.write_all(anchor_cli::solidity_template::ts_mocha(&project_name).as_bytes())?;
  353. } else {
  354. mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?;
  355. }
  356. }
  357. if !no_install {
  358. let yarn_result = install_node_modules("yarn")?;
  359. if !yarn_result.status.success() {
  360. println!("Failed yarn install will attempt to npm install");
  361. install_node_modules("npm")?;
  362. }
  363. }
  364. if !no_git {
  365. let git_result = std::process::Command::new("git")
  366. .arg("init")
  367. .stdout(Stdio::inherit())
  368. .stderr(Stdio::inherit())
  369. .output()
  370. .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
  371. if !git_result.status.success() {
  372. eprintln!("Failed to automatically initialize a new git repository");
  373. }
  374. }
  375. println!("{project_name} initialized");
  376. Ok(())
  377. }
  378. #[allow(clippy::too_many_arguments)]
  379. pub fn build(
  380. cfg_override: &ConfigOverride,
  381. no_idl: bool,
  382. idl: Option<String>,
  383. idl_ts: Option<String>,
  384. verifiable: bool,
  385. skip_lint: bool,
  386. program_name: Option<String>,
  387. solana_version: Option<String>,
  388. docker_image: Option<String>,
  389. bootstrap: BootstrapMode,
  390. stdout: Option<File>,
  391. stderr: Option<File>,
  392. env_vars: Vec<String>,
  393. cargo_args: Vec<String>,
  394. no_docs: bool,
  395. arch: ProgramArch,
  396. rebuild_types: bool,
  397. ) -> Result<()> {
  398. let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
  399. let types_path = "crates/types/src";
  400. // If rebuild_types is true and the types directory exists, remove it
  401. if rebuild_types && Path::new(types_path).exists() {
  402. fs::remove_dir_all(
  403. PathBuf::from(types_path)
  404. .parent()
  405. .ok_or_else(|| anyhow::format_err!("Failed to remove types directory"))?,
  406. )?;
  407. }
  408. create_dir_all(types_path)?;
  409. build_dynamic_types(cfg, cfg_override, types_path)?;
  410. // Build the programs
  411. anchor_cli::build(
  412. cfg_override,
  413. no_idl,
  414. idl,
  415. idl_ts,
  416. verifiable,
  417. skip_lint,
  418. program_name,
  419. solana_version,
  420. docker_image,
  421. bootstrap,
  422. stdout,
  423. stderr,
  424. env_vars,
  425. cargo_args,
  426. no_docs,
  427. arch,
  428. )
  429. }
  430. // Install node modules
  431. fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
  432. let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
  433. "cmd"
  434. } else {
  435. cmd
  436. });
  437. if cfg!(target_os = "windows") {
  438. command.arg(format!("/C {} install", cmd));
  439. } else {
  440. command.arg("install");
  441. }
  442. command
  443. .stdout(Stdio::inherit())
  444. .stderr(Stdio::inherit())
  445. .output()
  446. .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
  447. }
  448. // Create a new component from the template
  449. fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
  450. with_workspace(cfg_override, |cfg| {
  451. match cfg.path().parent() {
  452. None => {
  453. println!("Unable to make new component");
  454. }
  455. Some(parent) => {
  456. std::env::set_current_dir(parent)?;
  457. let cluster = cfg.provider.cluster.clone();
  458. let programs = cfg.programs.entry(cluster).or_default();
  459. if programs.contains_key(&name) {
  460. return Err(anyhow!("Program already exists"));
  461. }
  462. programs.insert(
  463. name.clone(),
  464. ProgramDeployment {
  465. address: {
  466. create_component(&name)?;
  467. anchor_cli::rust_template::get_or_create_program_id(&name)
  468. },
  469. path: None,
  470. idl: None,
  471. },
  472. );
  473. let toml = cfg.to_string();
  474. fs::write("Anchor.toml", toml)?;
  475. println!("Created new component: {}", name);
  476. }
  477. };
  478. Ok(())
  479. })
  480. }
  481. // Create a new system from the template
  482. fn new_system(cfg_override: &ConfigOverride, name: String) -> Result<()> {
  483. with_workspace(cfg_override, |cfg| {
  484. match cfg.path().parent() {
  485. None => {
  486. println!("Unable to make new system");
  487. }
  488. Some(parent) => {
  489. std::env::set_current_dir(parent)?;
  490. let cluster = cfg.provider.cluster.clone();
  491. let programs = cfg.programs.entry(cluster).or_default();
  492. if programs.contains_key(&name) {
  493. return Err(anyhow!("Program already exists"));
  494. }
  495. programs.insert(
  496. name.clone(),
  497. anchor_cli::config::ProgramDeployment {
  498. address: {
  499. rust_template::create_system(&name)?;
  500. anchor_cli::rust_template::get_or_create_program_id(&name)
  501. },
  502. path: None,
  503. idl: None,
  504. },
  505. );
  506. let toml = cfg.to_string();
  507. fs::write("Anchor.toml", toml)?;
  508. println!("Created new system: {}", name);
  509. }
  510. };
  511. Ok(())
  512. })
  513. }
  514. // with_workspace ensures the current working directory is always the top level
  515. // workspace directory, i.e., where the `Anchor.toml` file is located, before
  516. // and after the closure invocation.
  517. //
  518. // The closure passed into this function must never change the working directory
  519. // to be outside the workspace. Doing so will have undefined behavior.
  520. fn with_workspace<R>(
  521. cfg_override: &ConfigOverride,
  522. f: impl FnOnce(&mut WithPath<Config>) -> R,
  523. ) -> R {
  524. set_workspace_dir_or_exit();
  525. let mut cfg = Config::discover(cfg_override)
  526. .expect("Previously set the workspace dir")
  527. .expect("Anchor.toml must always exist");
  528. let r = f(&mut cfg);
  529. set_workspace_dir_or_exit();
  530. r
  531. }
  532. fn set_workspace_dir_or_exit() {
  533. let d = match Config::discover(&ConfigOverride::default()) {
  534. Err(err) => {
  535. println!("Workspace configuration error: {err}");
  536. std::process::exit(1);
  537. }
  538. Ok(d) => d,
  539. };
  540. match d {
  541. None => {
  542. println!("Not in anchor workspace.");
  543. std::process::exit(1);
  544. }
  545. Some(cfg) => {
  546. match cfg.path().parent() {
  547. None => {
  548. println!("Unable to make new program");
  549. }
  550. Some(parent) => {
  551. if std::env::set_current_dir(parent).is_err() {
  552. println!("Not in anchor workspace.");
  553. std::process::exit(1);
  554. }
  555. }
  556. };
  557. }
  558. }
  559. }
  560. fn discover_cluster_url(cfg_override: &ConfigOverride) -> Result<String> {
  561. let url = match Config::discover(cfg_override)? {
  562. Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
  563. None => {
  564. if let Some(cluster) = cfg_override.cluster.clone() {
  565. cluster.url().to_string()
  566. } else {
  567. config::get_solana_cfg_url()?
  568. }
  569. }
  570. };
  571. Ok(url)
  572. }
  573. fn cluster_url(cfg: &Config, test_validator: &Option<TestValidator>) -> String {
  574. let is_localnet = cfg.provider.cluster == Cluster::Localnet;
  575. match is_localnet {
  576. // Cluster is Localnet, assume the intent is to use the configuration
  577. // for solana-test-validator
  578. true => test_validator_rpc_url(test_validator),
  579. false => cfg.provider.cluster.url().to_string(),
  580. }
  581. }
  582. // Return the URL that solana-test-validator should be running on given the
  583. // configuration
  584. fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
  585. match test_validator {
  586. Some(TestValidator {
  587. validator: Some(validator),
  588. ..
  589. }) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
  590. _ => "http://127.0.0.1:8899".to_string(),
  591. }
  592. }
  593. fn build_dynamic_types(
  594. cfg: WithPath<Config>,
  595. cfg_override: &ConfigOverride,
  596. types_path: &str,
  597. ) -> Result<()> {
  598. let cur_dir = std::env::current_dir()?;
  599. for p in cfg.get_rust_program_list()? {
  600. process_program_path(&p, cfg_override, types_path)?;
  601. }
  602. let types_path = PathBuf::from(types_path);
  603. let cargo_path = types_path
  604. .parent()
  605. .unwrap_or(&types_path)
  606. .join("Cargo.toml");
  607. if !cargo_path.exists() {
  608. let mut file = File::create(cargo_path)?;
  609. file.write_all(rust_template::types_cargo_toml().as_bytes())?;
  610. }
  611. std::env::set_current_dir(cur_dir)?;
  612. Ok(())
  613. }
  614. fn process_program_path(
  615. program_path: &Path,
  616. cfg_override: &ConfigOverride,
  617. types_path: &str,
  618. ) -> Result<()> {
  619. let lib_rs_path = Path::new(types_path).join("lib.rs");
  620. let file = File::open(program_path.join("src").join("lib.rs"))?;
  621. let lines = io::BufReader::new(file).lines();
  622. let mut contains_dynamic_components = false;
  623. for line in lines.map_while(Result::ok) {
  624. if let Some(component_id) = extract_component_id(&line) {
  625. let file_path = PathBuf::from(format!("{}/component_{}.rs", types_path, component_id));
  626. if !file_path.exists() {
  627. println!("Generating type for Component: {}", component_id);
  628. generate_component_type_file(&file_path, cfg_override, component_id)?;
  629. append_component_to_lib_rs(&lib_rs_path, component_id)?;
  630. }
  631. contains_dynamic_components = true;
  632. }
  633. }
  634. if contains_dynamic_components {
  635. let program_name = program_path.file_name().unwrap().to_str().unwrap();
  636. add_types_crate_dependency(program_name, &types_path.replace("/src", ""))?;
  637. }
  638. Ok(())
  639. }
  640. fn add_types_crate_dependency(program_name: &str, types_path: &str) -> Result<()> {
  641. std::process::Command::new("cargo")
  642. .arg("add")
  643. .arg("--package")
  644. .arg(program_name)
  645. .arg("--path")
  646. .arg(types_path)
  647. .stdout(Stdio::null())
  648. .stderr(Stdio::null())
  649. .spawn()
  650. .map_err(|e| {
  651. anyhow::format_err!(
  652. "error adding types as dependency to the program: {}",
  653. e.to_string()
  654. )
  655. })?;
  656. Ok(())
  657. }
  658. fn extract_component_id(line: &str) -> Option<&str> {
  659. let component_id_marker = "#[component_id(";
  660. line.find(component_id_marker).map(|start| {
  661. let start = start + component_id_marker.len();
  662. let end = line[start..].find(')').unwrap() + start;
  663. line[start..end].trim_matches('"')
  664. })
  665. }
  666. fn fetch_idl_for_component(component_id: &str, url: &str) -> Result<String> {
  667. let output = std::process::Command::new("bolt")
  668. .arg("idl")
  669. .arg("fetch")
  670. .arg(component_id)
  671. .arg("--provider.cluster")
  672. .arg(url)
  673. .stdout(Stdio::piped())
  674. .stderr(Stdio::piped())
  675. .output()?;
  676. if output.status.success() {
  677. let idl_string = String::from_utf8(output.stdout)
  678. .map_err(|e| anyhow!("Failed to decode IDL output as UTF-8: {}", e))?
  679. .to_string();
  680. Ok(idl_string)
  681. } else {
  682. let error_message = String::from_utf8(output.stderr)
  683. .unwrap_or(format!(
  684. "Error trying to dynamically generate the type \
  685. for component {}, unable to fetch the idl. \nEnsure that the idl is available. Specify \
  686. the appropriate cluster using the --provider.cluster option",
  687. component_id
  688. ))
  689. .to_string();
  690. Err(anyhow!("Command failed with error: {}", error_message))
  691. }
  692. }
  693. fn generate_component_type_file(
  694. file_path: &Path,
  695. cfg_override: &ConfigOverride,
  696. component_id: &str,
  697. ) -> Result<()> {
  698. let url = discover_cluster_url(cfg_override)?;
  699. let idl_string = fetch_idl_for_component(component_id, &url)?;
  700. let idl: Idl = serde_json::from_str(&idl_string)?;
  701. let mut file = File::create(file_path)?;
  702. file.write_all(rust_template::component_type(&idl, component_id)?.as_bytes())?;
  703. Ok(())
  704. }
  705. fn append_component_to_lib_rs(lib_rs_path: &Path, component_id: &str) -> Result<()> {
  706. let mut file = OpenOptions::new()
  707. .create(true)
  708. .append(true)
  709. .open(lib_rs_path)?;
  710. file.write_all(rust_template::component_type_import(component_id).as_bytes())?;
  711. Ok(())
  712. }