lib.rs 24 KB

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