cli: Automatically decide IDL generation method (#2578)

This commit is contained in:
acheron 2023-07-22 16:39:05 +02:00 committed by GitHub
parent c548c85dff
commit 4604fbea9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 134 deletions

View File

@ -1209,28 +1209,28 @@ fn build_cwd_verifiable(
Ok(_) => { Ok(_) => {
// Build the idl. // Build the idl.
println!("Extracting the IDL"); println!("Extracting the IDL");
if let Ok(Some(idl)) = extract_idl(cfg, "src/lib.rs", skip_lint, no_docs) { let idl = generate_idl(cfg, skip_lint, no_docs)?;
// Write out the JSON file. // Write out the JSON file.
println!("Writing the IDL file"); println!("Writing the IDL file");
let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name)); let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name));
write_idl(&idl, OutFile::File(out_file))?; write_idl(&idl, OutFile::File(out_file))?;
// Write out the TypeScript type. // Write out the TypeScript type.
println!("Writing the .ts file"); println!("Writing the .ts file");
let ts_file = workspace_dir.join(format!("target/types/{}.ts", idl.name)); let ts_file = workspace_dir.join(format!("target/types/{}.ts", idl.name));
fs::write(&ts_file, rust_template::idl_ts(&idl)?)?; fs::write(&ts_file, rust_template::idl_ts(&idl)?)?;
// Copy out the TypeScript type. // Copy out the TypeScript type.
if !&cfg.workspace.types.is_empty() { if !&cfg.workspace.types.is_empty() {
fs::copy( fs::copy(
ts_file, ts_file,
workspace_dir workspace_dir
.join(&cfg.workspace.types) .join(&cfg.workspace.types)
.join(idl.name) .join(idl.name)
.with_extension("ts"), .with_extension("ts"),
)?; )?;
}
} }
println!("Build success"); println!("Build success");
} }
} }
@ -1502,34 +1502,33 @@ fn _build_rust_cwd(
std::process::exit(exit.status.code().unwrap_or(1)); std::process::exit(exit.status.code().unwrap_or(1));
} }
// Always assume idl is located at src/lib.rs. // Generate IDL
if let Some(idl) = extract_idl(cfg, "src/lib.rs", skip_lint, no_docs)? { let idl = generate_idl(cfg, skip_lint, no_docs)?;
// JSON out path. // JSON out path.
let out = match idl_out { let out = match idl_out {
None => PathBuf::from(".").join(&idl.name).with_extension("json"), None => PathBuf::from(".").join(&idl.name).with_extension("json"),
Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")), Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")),
}; };
// TS out path. // TS out path.
let ts_out = match idl_ts_out { let ts_out = match idl_ts_out {
None => PathBuf::from(".").join(&idl.name).with_extension("ts"), None => PathBuf::from(".").join(&idl.name).with_extension("ts"),
Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("ts")), Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("ts")),
}; };
// Write out the JSON file. // Write out the JSON file.
write_idl(&idl, OutFile::File(out))?; write_idl(&idl, OutFile::File(out))?;
// Write out the TypeScript type. // Write out the TypeScript type.
fs::write(&ts_out, rust_template::idl_ts(&idl)?)?; fs::write(&ts_out, rust_template::idl_ts(&idl)?)?;
// Copy out the TypeScript type. // Copy out the TypeScript type.
let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml"); let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
if !&cfg.workspace.types.is_empty() { if !&cfg.workspace.types.is_empty() {
fs::copy( fs::copy(
&ts_out, &ts_out,
cfg_parent cfg_parent
.join(&cfg.workspace.types) .join(&cfg.workspace.types)
.join(&idl.name) .join(&idl.name)
.with_extension("ts"), .with_extension("ts"),
)?; )?;
}
} }
Ok(()) Ok(())
@ -1671,13 +1670,12 @@ fn verify(
} }
// Verify IDL (only if it's not a buffer account). // Verify IDL (only if it's not a buffer account).
if let Some(local_idl) = extract_idl(&cfg, "src/lib.rs", true, false)? { let local_idl = generate_idl(&cfg, true, false)?;
if bin_ver.state != BinVerificationState::Buffer { if bin_ver.state != BinVerificationState::Buffer {
let deployed_idl = fetch_idl(cfg_override, program_id)?; let deployed_idl = fetch_idl(cfg_override, program_id)?;
if local_idl != deployed_idl { if local_idl != deployed_idl {
println!("Error: IDLs don't match"); println!("Error: IDLs don't match");
std::process::exit(1); std::process::exit(1);
}
} }
} }
@ -1805,62 +1803,6 @@ pub enum BinVerificationState {
}, },
} }
// Fetches an IDL for the given program_id.
fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result<Idl> {
let url = match Config::discover(cfg_override)? {
Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
None => {
// If the command is not run inside a workspace,
// cluster_url will be used from default solana config
// provider.cluster option can be used to override this
if let Some(cluster) = cfg_override.cluster.clone() {
cluster.url().to_string()
} else {
config::get_solana_cfg_url()?
}
}
};
let client = create_client(url);
let mut account = client.get_account(&idl_addr)?;
if account.executable {
let idl_addr = IdlAccount::address(&idl_addr);
account = client.get_account(&idl_addr)?;
}
// Cut off account discriminator.
let mut d: &[u8] = &account.data[8..];
let idl_account: IdlAccount = AnchorDeserialize::deserialize(&mut d)?;
let compressed_len: usize = idl_account.data_len.try_into().unwrap();
let compressed_bytes = &account.data[44..44 + compressed_len];
let mut z = ZlibDecoder::new(compressed_bytes);
let mut s = Vec::new();
z.read_to_end(&mut s)?;
serde_json::from_slice(&s[..]).map_err(Into::into)
}
fn extract_idl(
cfg: &WithPath<Config>,
file: &str,
skip_lint: bool,
no_docs: bool,
) -> Result<Option<Idl>> {
let file = shellexpand::tilde(file);
let manifest_from_path = std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap());
let cargo = Manifest::discover_from_path(manifest_from_path)?
.ok_or_else(|| anyhow!("Cargo.toml not found"))?;
anchor_syn::idl::parse::file::parse(
&*file,
cargo.version(),
cfg.features.seeds,
no_docs,
!(cfg.features.skip_lint || skip_lint),
)
}
fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
match subcmd { match subcmd {
IdlCommand::Init { IdlCommand::Init {
@ -1907,6 +1849,43 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
} }
} }
/// Fetch an IDL for the given program id.
fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result<Idl> {
let url = match Config::discover(cfg_override)? {
Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
None => {
// If the command is not run inside a workspace,
// cluster_url will be used from default solana config
// provider.cluster option can be used to override this
if let Some(cluster) = cfg_override.cluster.clone() {
cluster.url().to_string()
} else {
config::get_solana_cfg_url()?
}
}
};
let client = create_client(url);
let mut account = client.get_account(&idl_addr)?;
if account.executable {
let idl_addr = IdlAccount::address(&idl_addr);
account = client.get_account(&idl_addr)?;
}
// Cut off account discriminator.
let mut d: &[u8] = &account.data[8..];
let idl_account: IdlAccount = AnchorDeserialize::deserialize(&mut d)?;
let compressed_len: usize = idl_account.data_len.try_into().unwrap();
let compressed_bytes = &account.data[44..44 + compressed_len];
let mut z = ZlibDecoder::new(compressed_bytes);
let mut s = Vec::new();
z.read_to_end(&mut s)?;
serde_json::from_slice(&s[..]).map_err(Into::into)
}
fn get_idl_account(client: &RpcClient, idl_address: &Pubkey) -> Result<IdlAccount> { fn get_idl_account(client: &RpcClient, idl_address: &Pubkey) -> Result<IdlAccount> {
let account = client.get_account(idl_address)?; let account = client.get_account(idl_address)?;
let mut data: &[u8] = &account.data; let mut data: &[u8] = &account.data;
@ -2233,7 +2212,18 @@ fn idl_parse(
no_docs: bool, no_docs: bool,
) -> Result<()> { ) -> Result<()> {
let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
let idl = extract_idl(&cfg, &file, true, no_docs)?.ok_or_else(|| anyhow!("IDL not parsed"))?; let file = shellexpand::tilde(&file);
let manifest_path = std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap());
let manifest = Manifest::discover_from_path(manifest_path)?
.ok_or_else(|| anyhow!("Cargo.toml not found"))?;
let idl = generate_idl_parse(
&*file,
manifest.version(),
cfg.features.seeds,
no_docs,
!cfg.features.skip_lint,
)?;
let out = match out { let out = match out {
None => OutFile::Stdout, None => OutFile::Stdout,
Some(out) => OutFile::File(PathBuf::from(out)), Some(out) => OutFile::File(PathBuf::from(out)),
@ -2249,6 +2239,64 @@ fn idl_parse(
} }
fn idl_build(out: Option<String>, out_ts: Option<String>, no_docs: bool) -> Result<()> { fn idl_build(out: Option<String>, out_ts: Option<String>, no_docs: bool) -> Result<()> {
let idls = generate_idl_build(no_docs)?;
if idls.len() == 1 {
let idl = &idls[0];
let out = match out {
None => OutFile::Stdout,
Some(path) => OutFile::File(PathBuf::from(path)),
};
write_idl(idl, out)?;
if let Some(path) = out_ts {
fs::write(path, rust_template::idl_ts(idl)?)?;
}
} else {
println!("{}", serde_json::to_string_pretty(&idls)?);
};
Ok(())
}
/// Generate IDL with method decided by whether manifest file has `idl-build` feature or not.
fn generate_idl(cfg: &WithPath<Config>, skip_lint: bool, no_docs: bool) -> Result<Idl> {
let manifest = Manifest::discover()?.ok_or_else(|| anyhow!("Cargo.toml not found"))?;
// Check whether the manifest has `idl-build` feature
let is_idl_build = manifest
.features
.iter()
.any(|(feature, _)| feature == "idl-build");
if is_idl_build {
generate_idl_build(no_docs)?
.into_iter()
.next()
.ok_or_else(|| anyhow!("Could not build IDL"))
} else {
generate_idl_parse(
"src/lib.rs",
manifest.version(),
cfg.features.seeds,
no_docs,
!(cfg.features.skip_lint || skip_lint),
)
}
}
/// Generate IDL with the parsing method(default).
fn generate_idl_parse(
path: impl AsRef<Path>,
version: String,
seeds_feature: bool,
no_docs: bool,
safety_checks: bool,
) -> Result<Idl> {
anchor_syn::idl::parse::file::parse(path, version, seeds_feature, no_docs, safety_checks)
.and_then(|maybe_idl| maybe_idl.ok_or_else(|| anyhow!("Failed to parse IDL")))
}
/// Generate IDL with the build method.
fn generate_idl_build(no_docs: bool) -> Result<Vec<Idl>> {
let no_docs = if no_docs { "TRUE" } else { "FALSE" }; let no_docs = if no_docs { "TRUE" } else { "FALSE" };
let cfg = Config::discover(&ConfigOverride::default())?.expect("Not in workspace."); let cfg = Config::discover(&ConfigOverride::default())?.expect("Not in workspace.");
@ -2264,8 +2312,8 @@ fn idl_build(out: Option<String>, out_ts: Option<String>, no_docs: bool) -> Resu
"--show-output", "--show-output",
"--quiet", "--quiet",
]) ])
.env("ANCHOR_IDL_GEN_NO_DOCS", no_docs) .env("ANCHOR_IDL_BUILD_NO_DOCS", no_docs)
.env("ANCHOR_IDL_GEN_SEEDS_FEATURE", seeds_feature) .env("ANCHOR_IDL_BUILD_SEEDS_FEATURE", seeds_feature)
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.output() .output()
.map_err(|e| anyhow::format_err!("{}", e.to_string()))?; .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
@ -2282,7 +2330,7 @@ fn idl_build(out: Option<String>, out_ts: Option<String>, no_docs: bool) -> Resu
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct IdlGenEventPrint { struct IdlBuildEventPrint {
event: IdlEvent, event: IdlEvent,
defined_types: Vec<IdlTypeDefinition>, defined_types: Vec<IdlTypeDefinition>,
} }
@ -2360,7 +2408,7 @@ fn idl_build(out: Option<String>, out_ts: Option<String>, no_docs: bool) -> Resu
} }
State::EventLines(lines) => { State::EventLines(lines) => {
if line == "---- IDL end event ----" { if line == "---- IDL end event ----" {
let event: IdlGenEventPrint = serde_json::from_str(&lines.join("\n"))?; let event: IdlBuildEventPrint = serde_json::from_str(&lines.join("\n"))?;
events.push(event.event); events.push(event.event);
defined_types.extend( defined_types.extend(
event event
@ -2421,22 +2469,7 @@ fn idl_build(out: Option<String>, out_ts: Option<String>, no_docs: bool) -> Resu
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if idls.len() == 1 { Ok(idls)
let idl = &idls[0];
let out = match out {
None => OutFile::Stdout,
Some(path) => OutFile::File(PathBuf::from(path)),
};
write_idl(idl, out)?;
if let Some(path) = out_ts {
fs::write(path, rust_template::idl_ts(idl)?)?;
}
} else {
println!("{}", serde_json::to_string_pretty(&idls)?);
};
Ok(())
} }
fn idl_fetch(cfg_override: &ConfigOverride, address: Pubkey, out: Option<String>) -> Result<()> { fn idl_fetch(cfg_override: &ConfigOverride, address: Pubkey, out: Option<String>) -> Result<()> {

View File

@ -15,14 +15,14 @@ fn get_module_paths() -> (TokenStream, TokenStream) {
#[inline(always)] #[inline(always)]
pub fn get_no_docs() -> bool { pub fn get_no_docs() -> bool {
std::option_env!("ANCHOR_IDL_GEN_NO_DOCS") std::option_env!("ANCHOR_IDL_BUILD_NO_DOCS")
.map(|val| val == "TRUE") .map(|val| val == "TRUE")
.unwrap_or(false) .unwrap_or(false)
} }
#[inline(always)] #[inline(always)]
pub fn get_seeds_feature() -> bool { pub fn get_seeds_feature() -> bool {
std::option_env!("ANCHOR_IDL_GEN_SEEDS_FEATURE") std::option_env!("ANCHOR_IDL_BUILD_SEEDS_FEATURE")
.map(|val| val == "TRUE") .map(|val| val == "TRUE")
.unwrap_or(false) .unwrap_or(false)
} }

View File

@ -21,13 +21,13 @@ const ERROR_CODE_OFFSET: u32 = 6000;
// Parse an entire interface file. // Parse an entire interface file.
pub fn parse( pub fn parse(
filename: impl AsRef<Path>, path: impl AsRef<Path>,
version: String, version: String,
seeds_feature: bool, seeds_feature: bool,
no_docs: bool, no_docs: bool,
safety_checks: bool, safety_checks: bool,
) -> Result<Option<Idl>> { ) -> Result<Option<Idl>> {
let ctx = CrateContext::parse(filename)?; let ctx = CrateContext::parse(path)?;
if safety_checks { if safety_checks {
ctx.safety_checks()?; ctx.safety_checks()?;
} }

View File

@ -2,6 +2,9 @@
set -x set -x
set -e set -e
# Run anchor test
anchor test --skip-lint
idls_dir=idls idls_dir=idls
tmp_dir=$(mktemp -d) tmp_dir=$(mktemp -d)

9
tests/idl/tests/idl.ts Normal file
View File

@ -0,0 +1,9 @@
import * as anchor from "@coral-xyz/anchor";
import { IDL } from "../target/types/idl";
describe(IDL.name, () => {
anchor.setProvider(anchor.AnchorProvider.env());
it("Builds", () => {});
});