|
1 | 1 | mod rust_template;
|
2 | 2 |
|
3 | 3 | use crate::rust_template::{create_component, create_system};
|
| 4 | +use anchor_cli::config; |
4 | 5 | use anchor_cli::config::{
|
5 | 6 | BootstrapMode, Config, ConfigOverride, GenesisEntry, ProgramArch, ProgramDeployment,
|
6 | 7 | TestValidator, Validator, WithPath,
|
7 | 8 | };
|
8 | 9 | use anchor_client::Cluster;
|
| 10 | +use anchor_syn::idl::types::Idl; |
9 | 11 | use anyhow::{anyhow, Result};
|
10 | 12 | use clap::{Parser, Subcommand};
|
11 | 13 | use heck::{ToKebabCase, ToSnakeCase};
|
12 | 14 | use std::collections::BTreeMap;
|
13 |
| -use std::fs::{self, File}; |
| 15 | +use std::fs::{self, create_dir_all, File, OpenOptions}; |
14 | 16 | use std::io::Write;
|
| 17 | +use std::io::{self, BufRead}; |
| 18 | +use std::path::{Path, PathBuf}; |
15 | 19 | use std::process::Stdio;
|
16 | 20 |
|
17 | 21 | pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
@@ -49,6 +53,9 @@ pub struct SystemCommand {
|
49 | 53 | #[derive(Debug, Parser)]
|
50 | 54 | #[clap(version = VERSION)]
|
51 | 55 | pub struct Opts {
|
| 56 | + /// Rebuild the auto-generated types |
| 57 | + #[clap(global = true, long, action)] |
| 58 | + pub rebuild_types: bool, |
52 | 59 | #[clap(flatten)]
|
53 | 60 | pub cfg_override: ConfigOverride,
|
54 | 61 | #[clap(subcommand)]
|
@@ -105,6 +112,7 @@ pub fn entry(opts: Opts) -> Result<()> {
|
105 | 112 | cargo_args,
|
106 | 113 | no_docs,
|
107 | 114 | arch,
|
| 115 | + opts.rebuild_types, |
108 | 116 | ),
|
109 | 117 | _ => {
|
110 | 118 | let opts = anchor_cli::Opts {
|
@@ -415,7 +423,23 @@ pub fn build(
|
415 | 423 | cargo_args: Vec<String>,
|
416 | 424 | no_docs: bool,
|
417 | 425 | arch: ProgramArch,
|
| 426 | + rebuild_types: bool, |
418 | 427 | ) -> Result<()> {
|
| 428 | + let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); |
| 429 | + let types_path = "crates/types/src"; |
| 430 | + |
| 431 | + // If rebuild_types is true and the types directory exists, remove it |
| 432 | + if rebuild_types && Path::new(types_path).exists() { |
| 433 | + fs::remove_dir_all( |
| 434 | + PathBuf::from(types_path) |
| 435 | + .parent() |
| 436 | + .ok_or_else(|| anyhow::format_err!("Failed to remove types directory"))?, |
| 437 | + )?; |
| 438 | + } |
| 439 | + create_dir_all(types_path)?; |
| 440 | + build_dynamic_types(cfg, cfg_override, types_path)?; |
| 441 | + |
| 442 | + // Build the programs |
419 | 443 | anchor_cli::build(
|
420 | 444 | cfg_override,
|
421 | 445 | idl,
|
@@ -472,9 +496,9 @@ fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
|
472 | 496 |
|
473 | 497 | programs.insert(
|
474 | 498 | name.clone(),
|
475 |
| - anchor_cli::config::ProgramDeployment { |
| 499 | + ProgramDeployment { |
476 | 500 | address: {
|
477 |
| - rust_template::create_component(&name)?; |
| 501 | + create_component(&name)?; |
478 | 502 | anchor_cli::rust_template::get_or_create_program_id(&name)
|
479 | 503 | },
|
480 | 504 | path: None,
|
@@ -581,3 +605,168 @@ fn set_workspace_dir_or_exit() {
|
581 | 605 | }
|
582 | 606 | }
|
583 | 607 | }
|
| 608 | + |
| 609 | +fn discover_cluster_url(cfg_override: &ConfigOverride) -> Result<String> { |
| 610 | + let url = match Config::discover(cfg_override)? { |
| 611 | + Some(cfg) => cluster_url(&cfg, &cfg.test_validator), |
| 612 | + None => { |
| 613 | + if let Some(cluster) = cfg_override.cluster.clone() { |
| 614 | + cluster.url().to_string() |
| 615 | + } else { |
| 616 | + config::get_solana_cfg_url()? |
| 617 | + } |
| 618 | + } |
| 619 | + }; |
| 620 | + Ok(url) |
| 621 | +} |
| 622 | + |
| 623 | +fn cluster_url(cfg: &Config, test_validator: &Option<TestValidator>) -> String { |
| 624 | + let is_localnet = cfg.provider.cluster == Cluster::Localnet; |
| 625 | + match is_localnet { |
| 626 | + // Cluster is Localnet, assume the intent is to use the configuration |
| 627 | + // for solana-test-validator |
| 628 | + true => test_validator_rpc_url(test_validator), |
| 629 | + false => cfg.provider.cluster.url().to_string(), |
| 630 | + } |
| 631 | +} |
| 632 | + |
| 633 | +// Return the URL that solana-test-validator should be running on given the |
| 634 | +// configuration |
| 635 | +fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String { |
| 636 | + match test_validator { |
| 637 | + Some(TestValidator { |
| 638 | + validator: Some(validator), |
| 639 | + .. |
| 640 | + }) => format!("http://{}:{}", validator.bind_address, validator.rpc_port), |
| 641 | + _ => "http://127.0.0.1:8899".to_string(), |
| 642 | + } |
| 643 | +} |
| 644 | + |
| 645 | +fn build_dynamic_types( |
| 646 | + cfg: WithPath<Config>, |
| 647 | + cfg_override: &ConfigOverride, |
| 648 | + types_path: &str, |
| 649 | +) -> Result<()> { |
| 650 | + let cur_dir = std::env::current_dir()?; |
| 651 | + for p in cfg.get_rust_program_list()? { |
| 652 | + process_program_path(&p, cfg_override, types_path)?; |
| 653 | + } |
| 654 | + let types_path = PathBuf::from(types_path); |
| 655 | + let cargo_path = types_path |
| 656 | + .parent() |
| 657 | + .unwrap_or(&types_path) |
| 658 | + .join("Cargo.toml"); |
| 659 | + if !cargo_path.exists() { |
| 660 | + let mut file = File::create(cargo_path)?; |
| 661 | + file.write_all(rust_template::types_cargo_toml().as_bytes())?; |
| 662 | + } |
| 663 | + std::env::set_current_dir(cur_dir)?; |
| 664 | + Ok(()) |
| 665 | +} |
| 666 | + |
| 667 | +fn process_program_path( |
| 668 | + program_path: &Path, |
| 669 | + cfg_override: &ConfigOverride, |
| 670 | + types_path: &str, |
| 671 | +) -> Result<()> { |
| 672 | + let lib_rs_path = Path::new(types_path).join("lib.rs"); |
| 673 | + let file = File::open(program_path.join("src").join("lib.rs"))?; |
| 674 | + let lines = io::BufReader::new(file).lines(); |
| 675 | + let mut contains_dynamic_components = false; |
| 676 | + for line in lines.map_while(Result::ok) { |
| 677 | + if let Some(component_id) = extract_component_id(&line) { |
| 678 | + let file_path = PathBuf::from(format!("{}/component_{}.rs", types_path, component_id)); |
| 679 | + if !file_path.exists() { |
| 680 | + println!("Generating type for Component: {}", component_id); |
| 681 | + generate_component_type_file(&file_path, cfg_override, component_id)?; |
| 682 | + append_component_to_lib_rs(&lib_rs_path, component_id)?; |
| 683 | + } |
| 684 | + contains_dynamic_components = true; |
| 685 | + } |
| 686 | + } |
| 687 | + if contains_dynamic_components { |
| 688 | + let program_name = program_path.file_name().unwrap().to_str().unwrap(); |
| 689 | + add_types_crate_dependency(program_name, &types_path.replace("/src", ""))?; |
| 690 | + } |
| 691 | + |
| 692 | + Ok(()) |
| 693 | +} |
| 694 | + |
| 695 | +fn add_types_crate_dependency(program_name: &str, types_path: &str) -> Result<()> { |
| 696 | + std::process::Command::new("cargo") |
| 697 | + .arg("add") |
| 698 | + .arg("--package") |
| 699 | + .arg(program_name) |
| 700 | + .arg("--path") |
| 701 | + .arg(types_path) |
| 702 | + .stdout(Stdio::null()) |
| 703 | + .stderr(Stdio::null()) |
| 704 | + .spawn() |
| 705 | + .map_err(|e| { |
| 706 | + anyhow::format_err!( |
| 707 | + "error adding types as dependency to the program: {}", |
| 708 | + e.to_string() |
| 709 | + ) |
| 710 | + })?; |
| 711 | + Ok(()) |
| 712 | +} |
| 713 | + |
| 714 | +fn extract_component_id(line: &str) -> Option<&str> { |
| 715 | + let component_id_marker = "#[component_id("; |
| 716 | + line.find(component_id_marker).map(|start| { |
| 717 | + let start = start + component_id_marker.len(); |
| 718 | + let end = line[start..].find(')').unwrap() + start; |
| 719 | + line[start..end].trim_matches('"') |
| 720 | + }) |
| 721 | +} |
| 722 | + |
| 723 | +fn fetch_idl_for_component(component_id: &str, url: &str) -> Result<String> { |
| 724 | + let output = std::process::Command::new("bolt") |
| 725 | + .arg("idl") |
| 726 | + .arg("fetch") |
| 727 | + .arg(component_id) |
| 728 | + .arg("--provider.cluster") |
| 729 | + .arg(url) |
| 730 | + .stdout(Stdio::piped()) |
| 731 | + .stderr(Stdio::piped()) |
| 732 | + .output()?; |
| 733 | + |
| 734 | + if output.status.success() { |
| 735 | + let idl_string = String::from_utf8(output.stdout) |
| 736 | + .map_err(|e| anyhow!("Failed to decode IDL output as UTF-8: {}", e))? |
| 737 | + .to_string(); |
| 738 | + Ok(idl_string) |
| 739 | + } else { |
| 740 | + let error_message = String::from_utf8(output.stderr) |
| 741 | + .unwrap_or(format!( |
| 742 | + "Error trying to dynamically generate the type \ |
| 743 | + for component {}, unable to fetch the idl. \nEnsure that the idl is available. Specify \ |
| 744 | + the appropriate cluster using the --provider.cluster option", |
| 745 | + component_id |
| 746 | + )) |
| 747 | + .to_string(); |
| 748 | + Err(anyhow!("Command failed with error: {}", error_message)) |
| 749 | + } |
| 750 | +} |
| 751 | + |
| 752 | +fn generate_component_type_file( |
| 753 | + file_path: &Path, |
| 754 | + cfg_override: &ConfigOverride, |
| 755 | + component_id: &str, |
| 756 | +) -> Result<()> { |
| 757 | + let url = discover_cluster_url(cfg_override)?; |
| 758 | + let idl_string = fetch_idl_for_component(component_id, &url)?; |
| 759 | + let idl: Idl = serde_json::from_str(&idl_string)?; |
| 760 | + let mut file = File::create(file_path)?; |
| 761 | + file.write_all(rust_template::component_type(&idl, component_id)?.as_bytes())?; |
| 762 | + Ok(()) |
| 763 | +} |
| 764 | + |
| 765 | +fn append_component_to_lib_rs(lib_rs_path: &Path, component_id: &str) -> Result<()> { |
| 766 | + let mut file = OpenOptions::new() |
| 767 | + .create(true) |
| 768 | + .append(true) |
| 769 | + .open(lib_rs_path)?; |
| 770 | + file.write_all(rust_template::component_type_import(component_id).as_bytes())?; |
| 771 | + Ok(()) |
| 772 | +} |
0 commit comments