lib.rs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  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. let mut cfg = Config::default();
  167. let jest = test_template == anchor_cli::rust_template::TestTemplate::Jest;
  168. if jest {
  169. cfg.scripts.insert(
  170. "test".to_owned(),
  171. if javascript {
  172. "yarn run jest"
  173. } else {
  174. "yarn run jest --preset ts-jest"
  175. }
  176. .to_owned(),
  177. );
  178. } else {
  179. cfg.scripts.insert(
  180. "test".to_owned(),
  181. if javascript {
  182. "yarn run mocha -t 1000000 tests/"
  183. } else {
  184. "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
  185. }
  186. .to_owned(),
  187. );
  188. }
  189. let mut localnet = BTreeMap::new();
  190. let program_id = anchor_cli::rust_template::get_or_create_program_id(&rust_name);
  191. localnet.insert(
  192. rust_name,
  193. ProgramDeployment {
  194. address: program_id,
  195. path: None,
  196. idl: None,
  197. },
  198. );
  199. if !solidity {
  200. let component_id = anchor_cli::rust_template::get_or_create_program_id(component_name);
  201. let system_id = anchor_cli::rust_template::get_or_create_program_id(system_name);
  202. localnet.insert(
  203. component_name.to_owned(),
  204. ProgramDeployment {
  205. address: component_id,
  206. path: None,
  207. idl: None,
  208. },
  209. );
  210. localnet.insert(
  211. system_name.to_owned(),
  212. ProgramDeployment {
  213. address: system_id,
  214. path: None,
  215. idl: None,
  216. },
  217. );
  218. cfg.workspace.members.push("programs/*".to_owned());
  219. cfg.workspace
  220. .members
  221. .push("programs-ecs/components/*".to_owned());
  222. cfg.workspace
  223. .members
  224. .push("programs-ecs/systems/*".to_owned());
  225. }
  226. // Setup the test validator to clone Bolt programs from devnet
  227. let validator = Validator {
  228. url: Some("https://rpc.magicblock.app/devnet/".to_owned()),
  229. rpc_port: 8899,
  230. bind_address: "0.0.0.0".to_owned(),
  231. ledger: ".bolt/test-ledger".to_owned(),
  232. account: Some(vec![
  233. // Registry account
  234. anchor_cli::config::AccountEntry {
  235. address: "EHLkWwAT9oebVv9ht3mtqrvHhRVMKrt54tF3MfHTey2K".to_owned(),
  236. filename: "tests/fixtures/registry.json".to_owned(),
  237. },
  238. ]),
  239. ..Default::default()
  240. };
  241. let test_validator = TestValidator {
  242. startup_wait: 5000,
  243. shutdown_wait: 2000,
  244. validator: Some(validator),
  245. genesis: Some(vec![GenesisEntry {
  246. address: WORLD_PROGRAM.to_owned(),
  247. program: "tests/fixtures/world.so".to_owned(),
  248. upgradeable: Some(false),
  249. }]),
  250. ..Default::default()
  251. };
  252. cfg.test_validator = Some(test_validator);
  253. cfg.programs.insert(Cluster::Localnet, localnet);
  254. let toml = cfg.to_string();
  255. fs::write("Anchor.toml", toml)?;
  256. // Initialize .gitignore file
  257. fs::write(".gitignore", rust_template::git_ignore())?;
  258. // Initialize .prettierignore file
  259. fs::write(".prettierignore", rust_template::prettier_ignore())?;
  260. // Remove the default programs if `--force` is passed
  261. if force {
  262. let programs_path = std::env::current_dir()?
  263. .join(if solidity { "solidity" } else { "programs" })
  264. .join(&project_name);
  265. fs::create_dir_all(&programs_path)?;
  266. fs::remove_dir_all(&programs_path)?;
  267. let programs_ecs_path = std::env::current_dir()?
  268. .join("programs-ecs")
  269. .join(&project_name);
  270. fs::create_dir_all(&programs_ecs_path)?;
  271. fs::remove_dir_all(&programs_ecs_path)?;
  272. }
  273. // Build the program.
  274. if solidity {
  275. anchor_cli::solidity_template::create_program(&project_name)?;
  276. } else {
  277. create_system(system_name)?;
  278. create_component(component_name)?;
  279. rust_template::create_program(&project_name, template)?;
  280. // Add the component as a dependency to the system
  281. std::process::Command::new("cargo")
  282. .arg("add")
  283. .arg("--package")
  284. .arg(system_name)
  285. .arg("--path")
  286. .arg(format!("programs-ecs/components/{}", component_name))
  287. .arg("--features")
  288. .arg("cpi")
  289. .stdout(std::process::Stdio::null())
  290. .stderr(std::process::Stdio::null())
  291. .spawn()
  292. .map_err(|e| {
  293. anyhow::format_err!(
  294. "error adding component as dependency to the system: {}",
  295. e.to_string()
  296. )
  297. })?;
  298. }
  299. // Build the test suite.
  300. fs::create_dir_all("tests/fixtures")?;
  301. // Build the migrations directory.
  302. fs::create_dir_all("migrations")?;
  303. // Create the registry account
  304. fs::write(
  305. "tests/fixtures/registry.json",
  306. rust_template::registry_account(),
  307. )?;
  308. // Dump the World program into tests/fixtures/world.so
  309. std::process::Command::new("solana")
  310. .arg("program")
  311. .arg("dump")
  312. .arg("-u")
  313. .arg("d")
  314. .arg(WORLD_PROGRAM)
  315. .arg("tests/fixtures/world.so")
  316. .stdout(Stdio::inherit())
  317. .stderr(Stdio::inherit())
  318. .spawn()
  319. .map_err(|e| anyhow::format_err!("solana program dump failed: {}", e.to_string()))?;
  320. if javascript {
  321. // Build javascript config
  322. let mut package_json = File::create("package.json")?;
  323. package_json.write_all(rust_template::package_json(jest).as_bytes())?;
  324. if jest {
  325. let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
  326. if solidity {
  327. test.write_all(anchor_cli::solidity_template::jest(&project_name).as_bytes())?;
  328. } else {
  329. test.write_all(rust_template::jest(&project_name).as_bytes())?;
  330. }
  331. } else {
  332. let mut test = File::create(format!("tests/{}.js", &project_name))?;
  333. if solidity {
  334. test.write_all(anchor_cli::solidity_template::mocha(&project_name).as_bytes())?;
  335. } else {
  336. test.write_all(rust_template::mocha(&project_name).as_bytes())?;
  337. }
  338. }
  339. let mut deploy = File::create("migrations/deploy.js")?;
  340. deploy.write_all(anchor_cli::rust_template::deploy_script().as_bytes())?;
  341. } else {
  342. // Build typescript config
  343. let mut ts_config = File::create("tsconfig.json")?;
  344. ts_config.write_all(anchor_cli::rust_template::ts_config(jest).as_bytes())?;
  345. let mut ts_package_json = File::create("package.json")?;
  346. ts_package_json.write_all(rust_template::ts_package_json(jest).as_bytes())?;
  347. let mut deploy = File::create("migrations/deploy.ts")?;
  348. deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?;
  349. let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
  350. if solidity {
  351. mocha.write_all(anchor_cli::solidity_template::ts_mocha(&project_name).as_bytes())?;
  352. } else {
  353. mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?;
  354. }
  355. }
  356. if !no_install {
  357. let yarn_result = install_node_modules("yarn")?;
  358. if !yarn_result.status.success() {
  359. println!("Failed yarn install will attempt to npm install");
  360. install_node_modules("npm")?;
  361. }
  362. }
  363. if !no_git {
  364. let git_result = std::process::Command::new("git")
  365. .arg("init")
  366. .stdout(Stdio::inherit())
  367. .stderr(Stdio::inherit())
  368. .output()
  369. .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
  370. if !git_result.status.success() {
  371. eprintln!("Failed to automatically initialize a new git repository");
  372. }
  373. }
  374. println!("{project_name} initialized");
  375. Ok(())
  376. }
  377. #[allow(clippy::too_many_arguments)]
  378. pub fn build(
  379. cfg_override: &ConfigOverride,
  380. no_idl: bool,
  381. idl: Option<String>,
  382. idl_ts: Option<String>,
  383. verifiable: bool,
  384. skip_lint: bool,
  385. program_name: Option<String>,
  386. solana_version: Option<String>,
  387. docker_image: Option<String>,
  388. bootstrap: BootstrapMode,
  389. stdout: Option<File>,
  390. stderr: Option<File>,
  391. env_vars: Vec<String>,
  392. cargo_args: Vec<String>,
  393. no_docs: bool,
  394. arch: ProgramArch,
  395. rebuild_types: bool,
  396. ) -> Result<()> {
  397. let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
  398. let types_path = "crates/types/src";
  399. // If rebuild_types is true and the types directory exists, remove it
  400. if rebuild_types && Path::new(types_path).exists() {
  401. fs::remove_dir_all(
  402. PathBuf::from(types_path)
  403. .parent()
  404. .ok_or_else(|| anyhow::format_err!("Failed to remove types directory"))?,
  405. )?;
  406. }
  407. create_dir_all(types_path)?;
  408. build_dynamic_types(cfg, cfg_override, types_path)?;
  409. // Build the programs
  410. anchor_cli::build(
  411. cfg_override,
  412. no_idl,
  413. idl,
  414. idl_ts,
  415. verifiable,
  416. skip_lint,
  417. program_name,
  418. solana_version,
  419. docker_image,
  420. bootstrap,
  421. stdout,
  422. stderr,
  423. env_vars,
  424. cargo_args,
  425. no_docs,
  426. arch,
  427. )
  428. }
  429. // Install node modules
  430. fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
  431. let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
  432. "cmd"
  433. } else {
  434. cmd
  435. });
  436. if cfg!(target_os = "windows") {
  437. command.arg(format!("/C {} install", cmd));
  438. } else {
  439. command.arg("install");
  440. }
  441. command
  442. .stdout(Stdio::inherit())
  443. .stderr(Stdio::inherit())
  444. .output()
  445. .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
  446. }
  447. // Create a new component from the template
  448. fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
  449. with_workspace(cfg_override, |cfg| {
  450. match cfg.path().parent() {
  451. None => {
  452. println!("Unable to make new component");
  453. }
  454. Some(parent) => {
  455. std::env::set_current_dir(parent)?;
  456. let cluster = cfg.provider.cluster.clone();
  457. let programs = cfg.programs.entry(cluster).or_default();
  458. if programs.contains_key(&name) {
  459. return Err(anyhow!("Program already exists"));
  460. }
  461. programs.insert(
  462. name.clone(),
  463. ProgramDeployment {
  464. address: {
  465. create_component(&name)?;
  466. anchor_cli::rust_template::get_or_create_program_id(&name)
  467. },
  468. path: None,
  469. idl: None,
  470. },
  471. );
  472. let toml = cfg.to_string();
  473. fs::write("Anchor.toml", toml)?;
  474. println!("Created new component: {}", name);
  475. }
  476. };
  477. Ok(())
  478. })
  479. }
  480. // Create a new system from the template
  481. fn new_system(cfg_override: &ConfigOverride, name: String) -> Result<()> {
  482. with_workspace(cfg_override, |cfg| {
  483. match cfg.path().parent() {
  484. None => {
  485. println!("Unable to make new system");
  486. }
  487. Some(parent) => {
  488. std::env::set_current_dir(parent)?;
  489. let cluster = cfg.provider.cluster.clone();
  490. let programs = cfg.programs.entry(cluster).or_default();
  491. if programs.contains_key(&name) {
  492. return Err(anyhow!("Program already exists"));
  493. }
  494. programs.insert(
  495. name.clone(),
  496. anchor_cli::config::ProgramDeployment {
  497. address: {
  498. rust_template::create_system(&name)?;
  499. anchor_cli::rust_template::get_or_create_program_id(&name)
  500. },
  501. path: None,
  502. idl: None,
  503. },
  504. );
  505. let toml = cfg.to_string();
  506. fs::write("Anchor.toml", toml)?;
  507. println!("Created new system: {}", name);
  508. }
  509. };
  510. Ok(())
  511. })
  512. }
  513. // with_workspace ensures the current working directory is always the top level
  514. // workspace directory, i.e., where the `Anchor.toml` file is located, before
  515. // and after the closure invocation.
  516. //
  517. // The closure passed into this function must never change the working directory
  518. // to be outside the workspace. Doing so will have undefined behavior.
  519. fn with_workspace<R>(
  520. cfg_override: &ConfigOverride,
  521. f: impl FnOnce(&mut WithPath<Config>) -> R,
  522. ) -> R {
  523. set_workspace_dir_or_exit();
  524. let mut cfg = Config::discover(cfg_override)
  525. .expect("Previously set the workspace dir")
  526. .expect("Anchor.toml must always exist");
  527. let r = f(&mut cfg);
  528. set_workspace_dir_or_exit();
  529. r
  530. }
  531. fn set_workspace_dir_or_exit() {
  532. let d = match Config::discover(&ConfigOverride::default()) {
  533. Err(err) => {
  534. println!("Workspace configuration error: {err}");
  535. std::process::exit(1);
  536. }
  537. Ok(d) => d,
  538. };
  539. match d {
  540. None => {
  541. println!("Not in anchor workspace.");
  542. std::process::exit(1);
  543. }
  544. Some(cfg) => {
  545. match cfg.path().parent() {
  546. None => {
  547. println!("Unable to make new program");
  548. }
  549. Some(parent) => {
  550. if std::env::set_current_dir(parent).is_err() {
  551. println!("Not in anchor workspace.");
  552. std::process::exit(1);
  553. }
  554. }
  555. };
  556. }
  557. }
  558. }
  559. fn discover_cluster_url(cfg_override: &ConfigOverride) -> Result<String> {
  560. let url = match Config::discover(cfg_override)? {
  561. Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
  562. None => {
  563. if let Some(cluster) = cfg_override.cluster.clone() {
  564. cluster.url().to_string()
  565. } else {
  566. config::get_solana_cfg_url()?
  567. }
  568. }
  569. };
  570. Ok(url)
  571. }
  572. fn cluster_url(cfg: &Config, test_validator: &Option<TestValidator>) -> String {
  573. let is_localnet = cfg.provider.cluster == Cluster::Localnet;
  574. match is_localnet {
  575. // Cluster is Localnet, assume the intent is to use the configuration
  576. // for solana-test-validator
  577. true => test_validator_rpc_url(test_validator),
  578. false => cfg.provider.cluster.url().to_string(),
  579. }
  580. }
  581. // Return the URL that solana-test-validator should be running on given the
  582. // configuration
  583. fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
  584. match test_validator {
  585. Some(TestValidator {
  586. validator: Some(validator),
  587. ..
  588. }) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
  589. _ => "http://127.0.0.1:8899".to_string(),
  590. }
  591. }
  592. fn build_dynamic_types(
  593. cfg: WithPath<Config>,
  594. cfg_override: &ConfigOverride,
  595. types_path: &str,
  596. ) -> Result<()> {
  597. let cur_dir = std::env::current_dir()?;
  598. for p in cfg.get_rust_program_list()? {
  599. process_program_path(&p, cfg_override, types_path)?;
  600. }
  601. let types_path = PathBuf::from(types_path);
  602. let cargo_path = types_path
  603. .parent()
  604. .unwrap_or(&types_path)
  605. .join("Cargo.toml");
  606. if !cargo_path.exists() {
  607. let mut file = File::create(cargo_path)?;
  608. file.write_all(rust_template::types_cargo_toml().as_bytes())?;
  609. }
  610. std::env::set_current_dir(cur_dir)?;
  611. Ok(())
  612. }
  613. fn process_program_path(
  614. program_path: &Path,
  615. cfg_override: &ConfigOverride,
  616. types_path: &str,
  617. ) -> Result<()> {
  618. let lib_rs_path = Path::new(types_path).join("lib.rs");
  619. let file = File::open(program_path.join("src").join("lib.rs"))?;
  620. let lines = io::BufReader::new(file).lines();
  621. let mut contains_dynamic_components = false;
  622. for line in lines.map_while(Result::ok) {
  623. if let Some(component_id) = extract_component_id(&line) {
  624. let file_path = PathBuf::from(format!("{}/component_{}.rs", types_path, component_id));
  625. if !file_path.exists() {
  626. println!("Generating type for Component: {}", component_id);
  627. generate_component_type_file(&file_path, cfg_override, component_id)?;
  628. append_component_to_lib_rs(&lib_rs_path, component_id)?;
  629. }
  630. contains_dynamic_components = true;
  631. }
  632. }
  633. if contains_dynamic_components {
  634. let program_name = program_path.file_name().unwrap().to_str().unwrap();
  635. add_types_crate_dependency(program_name, &types_path.replace("/src", ""))?;
  636. }
  637. Ok(())
  638. }
  639. fn add_types_crate_dependency(program_name: &str, types_path: &str) -> Result<()> {
  640. std::process::Command::new("cargo")
  641. .arg("add")
  642. .arg("--package")
  643. .arg(program_name)
  644. .arg("--path")
  645. .arg(types_path)
  646. .stdout(Stdio::null())
  647. .stderr(Stdio::null())
  648. .spawn()
  649. .map_err(|e| {
  650. anyhow::format_err!(
  651. "error adding types as dependency to the program: {}",
  652. e.to_string()
  653. )
  654. })?;
  655. Ok(())
  656. }
  657. fn extract_component_id(line: &str) -> Option<&str> {
  658. let component_id_marker = "#[component_id(";
  659. line.find(component_id_marker).map(|start| {
  660. let start = start + component_id_marker.len();
  661. let end = line[start..].find(')').unwrap() + start;
  662. line[start..end].trim_matches('"')
  663. })
  664. }
  665. fn fetch_idl_for_component(component_id: &str, url: &str) -> Result<String> {
  666. let output = std::process::Command::new("bolt")
  667. .arg("idl")
  668. .arg("fetch")
  669. .arg(component_id)
  670. .arg("--provider.cluster")
  671. .arg(url)
  672. .stdout(Stdio::piped())
  673. .stderr(Stdio::piped())
  674. .output()?;
  675. if output.status.success() {
  676. let idl_string = String::from_utf8(output.stdout)
  677. .map_err(|e| anyhow!("Failed to decode IDL output as UTF-8: {}", e))?
  678. .to_string();
  679. Ok(idl_string)
  680. } else {
  681. let error_message = String::from_utf8(output.stderr)
  682. .unwrap_or(format!(
  683. "Error trying to dynamically generate the type \
  684. for component {}, unable to fetch the idl. \nEnsure that the idl is available. Specify \
  685. the appropriate cluster using the --provider.cluster option",
  686. component_id
  687. ))
  688. .to_string();
  689. Err(anyhow!("Command failed with error: {}", error_message))
  690. }
  691. }
  692. fn generate_component_type_file(
  693. file_path: &Path,
  694. cfg_override: &ConfigOverride,
  695. component_id: &str,
  696. ) -> Result<()> {
  697. let url = discover_cluster_url(cfg_override)?;
  698. let idl_string = fetch_idl_for_component(component_id, &url)?;
  699. let idl: Idl = serde_json::from_str(&idl_string)?;
  700. let mut file = File::create(file_path)?;
  701. file.write_all(rust_template::component_type(&idl, component_id)?.as_bytes())?;
  702. Ok(())
  703. }
  704. fn append_component_to_lib_rs(lib_rs_path: &Path, component_id: &str) -> Result<()> {
  705. let mut file = OpenOptions::new()
  706. .create(true)
  707. .append(true)
  708. .open(lib_rs_path)?;
  709. file.write_all(rust_template::component_type_import(component_id).as_bytes())?;
  710. Ok(())
  711. }