1use std::{
2 collections::BTreeMap,
3 env, mem,
4 path::{Path, PathBuf},
5 process::{Command, Stdio},
6};
7
8use anyhow::{anyhow, Result};
9use regex::Regex;
10use serde::Deserialize;
11
12use crate::types::{Idl, IdlEvent, IdlTypeDef};
13
14pub trait IdlBuild {
25 fn create_type() -> Option<IdlTypeDef> {
29 None
30 }
31
32 fn insert_types(_types: &mut BTreeMap<String, IdlTypeDef>) {}
34
35 fn get_full_path() -> String {
42 std::any::type_name::<Self>().into()
43 }
44}
45
46#[derive(Default)]
54pub struct IdlBuilder {
55 program_path: Option<PathBuf>,
56 resolution: Option<bool>,
57 skip_lint: Option<bool>,
58 no_docs: Option<bool>,
59 cargo_args: Option<Vec<String>>,
60}
61
62impl IdlBuilder {
63 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn program_path(mut self, program_path: PathBuf) -> Self {
70 self.program_path.replace(program_path);
71 self
72 }
73
74 pub fn resolution(mut self, resolution: bool) -> Self {
76 self.resolution.replace(resolution);
77 self
78 }
79 pub fn skip_lint(mut self, skip_lint: bool) -> Self {
81 self.skip_lint.replace(skip_lint);
82 self
83 }
84
85 pub fn no_docs(mut self, no_docs: bool) -> Self {
87 self.no_docs.replace(no_docs);
88 self
89 }
90
91 pub fn cargo_args(mut self, cargo_args: Vec<String>) -> Self {
94 self.cargo_args.replace(cargo_args);
95 self
96 }
97
98 pub fn build(self) -> Result<Idl> {
100 let idl = build(
101 &self
102 .program_path
103 .unwrap_or_else(|| std::env::current_dir().expect("Failed to get program path")),
104 self.resolution.unwrap_or(true),
105 self.skip_lint.unwrap_or_default(),
106 self.no_docs.unwrap_or_default(),
107 &self.cargo_args.unwrap_or_default(),
108 )
109 .map(convert_module_paths)
110 .map(sort)?;
111 verify(&idl)?;
112
113 Ok(idl)
114 }
115}
116
117#[deprecated(since = "0.1.2", note = "Use `IdlBuilder` instead")]
119pub fn build_idl(
120 program_path: impl AsRef<Path>,
121 resolution: bool,
122 skip_lint: bool,
123 no_docs: bool,
124) -> Result<Idl> {
125 IdlBuilder::new()
126 .program_path(program_path.as_ref().into())
127 .resolution(resolution)
128 .skip_lint(skip_lint)
129 .no_docs(no_docs)
130 .build()
131}
132
133fn build(
135 program_path: &Path,
136 resolution: bool,
137 skip_lint: bool,
138 no_docs: bool,
139 cargo_args: &[String],
140) -> Result<Idl> {
141 let toolchain = std::env::var("RUSTUP_TOOLCHAIN")
143 .map(|toolchain| format!("+{}", toolchain))
144 .unwrap_or_else(|_| "+nightly".to_string());
145
146 install_toolchain_if_needed(&toolchain)?;
147 let output = Command::new("cargo")
148 .args([
149 &toolchain,
150 "test",
151 "__anchor_private_print_idl",
152 "--features",
153 "idl-build",
154 ])
155 .args(cargo_args)
156 .args(["--", "--show-output", "--quiet"])
157 .env(
158 "ANCHOR_IDL_BUILD_NO_DOCS",
159 if no_docs { "TRUE" } else { "FALSE" },
160 )
161 .env(
162 "ANCHOR_IDL_BUILD_RESOLUTION",
163 if resolution { "TRUE" } else { "FALSE" },
164 )
165 .env(
166 "ANCHOR_IDL_BUILD_SKIP_LINT",
167 if skip_lint { "TRUE" } else { "FALSE" },
168 )
169 .env("ANCHOR_IDL_BUILD_PROGRAM_PATH", program_path)
170 .env("RUSTFLAGS", "--cfg procmacro2_semver_exempt -A warnings")
171 .current_dir(program_path)
172 .stderr(Stdio::inherit())
173 .output()?;
174
175 let stdout = String::from_utf8_lossy(&output.stdout);
176 if env::var("ANCHOR_LOG").is_ok() {
177 eprintln!("{}", stdout);
178 }
179
180 if !output.status.success() {
181 return Err(anyhow!(
182 "Building IDL failed. Run `ANCHOR_LOG=true anchor idl build` to see the logs."
183 ));
184 }
185
186 enum State {
187 Pass,
188 Address,
189 Constants(Vec<String>),
190 Events(Vec<String>),
191 Errors(Vec<String>),
192 Program(Vec<String>),
193 }
194
195 let mut address = String::new();
196 let mut events = vec![];
197 let mut error_codes = vec![];
198 let mut constants = vec![];
199 let mut types = BTreeMap::new();
200 let mut idl: Option<Idl> = None;
201
202 let mut state = State::Pass;
203 for line in stdout.lines() {
204 match &mut state {
205 State::Pass => match line {
206 "--- IDL begin address ---" => state = State::Address,
207 "--- IDL begin const ---" => state = State::Constants(vec![]),
208 "--- IDL begin event ---" => state = State::Events(vec![]),
209 "--- IDL begin errors ---" => state = State::Errors(vec![]),
210 "--- IDL begin program ---" => state = State::Program(vec![]),
211 _ => {
212 if line.starts_with("test result: ok")
213 && !line.starts_with("test result: ok. 0 passed; 0 failed; 0")
214 {
215 if let Some(idl) = idl.as_mut() {
216 idl.address = mem::take(&mut address);
217 idl.constants = mem::take(&mut constants);
218 idl.events = mem::take(&mut events);
219 idl.errors = mem::take(&mut error_codes);
220 idl.types = {
221 let prog_ty = mem::take(&mut idl.types);
222 let mut types = mem::take(&mut types);
223 types.extend(prog_ty.into_iter().map(|ty| (ty.name.clone(), ty)));
224 types.into_values().collect()
225 };
226 }
227 }
228 }
229 },
230 State::Address => {
231 address = line.replace(|c: char| !c.is_alphanumeric(), "");
232 state = State::Pass;
233 continue;
234 }
235 State::Constants(lines) => {
236 if line == "--- IDL end const ---" {
237 let constant = serde_json::from_str(&lines.join("\n"))?;
238 constants.push(constant);
239 state = State::Pass;
240 continue;
241 }
242
243 lines.push(line.to_owned());
244 }
245 State::Events(lines) => {
246 if line == "--- IDL end event ---" {
247 #[derive(Deserialize)]
248 struct IdlBuildEventPrint {
249 event: IdlEvent,
250 types: Vec<IdlTypeDef>,
251 }
252
253 let event = serde_json::from_str::<IdlBuildEventPrint>(&lines.join("\n"))?;
254 events.push(event.event);
255 types.extend(event.types.into_iter().map(|ty| (ty.name.clone(), ty)));
256 state = State::Pass;
257 continue;
258 }
259
260 lines.push(line.to_owned());
261 }
262 State::Errors(lines) => {
263 if line == "--- IDL end errors ---" {
264 error_codes = serde_json::from_str(&lines.join("\n"))?;
265 state = State::Pass;
266 continue;
267 }
268
269 lines.push(line.to_owned());
270 }
271 State::Program(lines) => {
272 if line == "--- IDL end program ---" {
273 idl = Some(serde_json::from_str(&lines.join("\n"))?);
274 state = State::Pass;
275 continue;
276 }
277
278 lines.push(line.to_owned());
279 }
280 }
281 }
282
283 idl.ok_or_else(|| anyhow!("IDL doesn't exist"))
284}
285
286fn install_toolchain_if_needed(toolchain: &str) -> Result<()> {
288 let is_installed = Command::new("cargo")
289 .arg(toolchain)
290 .output()?
291 .status
292 .success();
293 if !is_installed {
294 Command::new("rustup")
295 .args(["toolchain", "install", toolchain.trim_start_matches('+')])
296 .spawn()?
297 .wait()?;
298 }
299
300 Ok(())
301}
302
303fn convert_module_paths(idl: Idl) -> Idl {
305 let idl = serde_json::to_string(&idl).unwrap();
306 let idl = Regex::new(r#""(\w+::)+(\w+)""#)
307 .unwrap()
308 .captures_iter(&idl.clone())
309 .fold(idl, |acc, cur| {
310 let path = cur.get(0).unwrap().as_str();
311 let name = cur.get(2).unwrap().as_str();
312
313 let replaced_idl = acc.replace(path, &format!(r#""{name}""#));
315
316 let has_conflict = Regex::new(&format!(r#""(\w+::)+{name}""#))
318 .unwrap()
319 .is_match(&replaced_idl);
320 if has_conflict {
321 acc
322 } else {
323 replaced_idl
324 }
325 });
326
327 serde_json::from_str(&idl).expect("Invalid IDL")
328}
329
330fn sort(mut idl: Idl) -> Idl {
332 idl.accounts.sort_by(|a, b| a.name.cmp(&b.name));
333 idl.constants.sort_by(|a, b| a.name.cmp(&b.name));
334 idl.events.sort_by(|a, b| a.name.cmp(&b.name));
335 idl.instructions.sort_by(|a, b| a.name.cmp(&b.name));
336 idl.types.sort_by(|a, b| a.name.cmp(&b.name));
337
338 idl
339}
340
341fn verify(idl: &Idl) -> Result<()> {
343 if let Some(account) = idl
345 .accounts
346 .iter()
347 .find(|account| account.name.contains("::"))
348 {
349 return Err(anyhow!(
350 "Conflicting accounts names are not allowed.\nProgram: `{}`\nAccount: `{}`",
351 idl.metadata.name,
352 account.name
353 ));
354 }
355
356 macro_rules! check_empty_discriminators {
358 ($field:ident) => {
359 if let Some(item) = idl.$field.iter().find(|it| it.discriminator.is_empty()) {
360 return Err(anyhow!(
361 "Empty discriminators are not allowed for {}: `{}`",
362 stringify!($field),
363 item.name
364 ));
365 }
366 };
367 }
368 check_empty_discriminators!(accounts);
369 check_empty_discriminators!(events);
370 check_empty_discriminators!(instructions);
371
372 macro_rules! check_discriminator_collision {
374 ($field:ident) => {
375 if let Some((outer, inner)) = idl.$field.iter().find_map(|outer| {
376 idl.$field
377 .iter()
378 .filter(|inner| inner.name != outer.name)
379 .find(|inner| outer.discriminator.starts_with(&inner.discriminator))
380 .map(|inner| (outer, inner))
381 }) {
382 return Err(anyhow!(
383 "Ambiguous discriminators for {} `{}` and `{}`",
384 stringify!($field),
385 outer.name,
386 inner.name
387 ));
388 }
389 };
390 }
391 check_discriminator_collision!(accounts);
392 check_discriminator_collision!(events);
393 check_discriminator_collision!(instructions);
394
395 if let Some(account) = idl
397 .accounts
398 .iter()
399 .find(|acc| acc.discriminator.iter().all(|b| *b == 0))
400 {
401 return Err(anyhow!(
402 "All zero account discriminators are not allowed (account: `{}`)",
403 account.name
404 ));
405 }
406
407 for account in &idl.accounts {
419 let zero_count = account
420 .discriminator
421 .iter()
422 .take_while(|b| **b == 0)
423 .count();
424 if let Some(account2) = idl
425 .accounts
426 .iter()
427 .find(|acc| acc.discriminator.len() <= zero_count)
428 {
429 return Err(anyhow!(
430 "Accounts may allow substitution when used with the `zero` constraint: `{}` `{}`",
431 account.name,
432 account2.name
433 ));
434 }
435 }
436
437 Ok(())
438}