lib.rs 24 KB

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