anchor_lang_idl/
build.rs

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
14/// A trait that types must implement in order to include the type in the IDL definition.
15///
16/// This trait is automatically implemented for Anchor all types that use the `AnchorSerialize`
17/// proc macro. Note that manually implementing the `AnchorSerialize` trait does **NOT** have the
18/// same effect.
19///
20/// Types that don't implement this trait will cause a compile error during the IDL generation.
21///
22/// The default implementation of the trait allows the program to compile but the type does **NOT**
23/// get included in the IDL.
24pub trait IdlBuild {
25    /// Create an IDL type definition for the type.
26    ///
27    /// The type is only included in the IDL if this method returns `Some`.
28    fn create_type() -> Option<IdlTypeDef> {
29        None
30    }
31
32    /// Insert all types that are included in the current type definition to the given map.
33    fn insert_types(_types: &mut BTreeMap<String, IdlTypeDef>) {}
34
35    /// Get the full module path of the type.
36    ///
37    /// The full path will be used in the case of a conflicting type definition, e.g. when there
38    /// are multiple structs with the same name.
39    ///
40    /// The default implementation covers most cases.
41    fn get_full_path() -> String {
42        std::any::type_name::<Self>().into()
43    }
44}
45
46/// IDL builder using builder pattern.
47///
48/// # Example
49///
50/// ```ignore
51/// let idl = IdlBuilder::new().program_path(path).skip_lint(true).build()?;
52/// ```
53#[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    /// Create a new [`IdlBuilder`] instance.
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Set the program path (default: current directory)
69    pub fn program_path(mut self, program_path: PathBuf) -> Self {
70        self.program_path.replace(program_path);
71        self
72    }
73
74    /// Set whether to include account resolution information in the IDL (default: true).
75    pub fn resolution(mut self, resolution: bool) -> Self {
76        self.resolution.replace(resolution);
77        self
78    }
79    /// Set whether to skip linting (default: false).
80    pub fn skip_lint(mut self, skip_lint: bool) -> Self {
81        self.skip_lint.replace(skip_lint);
82        self
83    }
84
85    /// Set whether to skip generating docs in the IDL (default: false).
86    pub fn no_docs(mut self, no_docs: bool) -> Self {
87        self.no_docs.replace(no_docs);
88        self
89    }
90
91    /// Set the `cargo` args that will get passed to the underlying `cargo` command when building
92    /// IDLs (default: empty).
93    pub fn cargo_args(mut self, cargo_args: Vec<String>) -> Self {
94        self.cargo_args.replace(cargo_args);
95        self
96    }
97
98    /// Build the IDL with the current configuration.
99    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/// Generate IDL via compilation.
118#[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
133/// Build IDL.
134fn build(
135    program_path: &Path,
136    resolution: bool,
137    skip_lint: bool,
138    no_docs: bool,
139    cargo_args: &[String],
140) -> Result<Idl> {
141    // `nightly` toolchain is currently required for building the IDL.
142    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
286/// Install the given toolchain if it's not already installed.
287fn 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
303/// Convert paths to name if there are no conflicts.
304fn 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            // Replace path with name
314            let replaced_idl = acc.replace(path, &format!(r#""{name}""#));
315
316            // Check whether there is a conflict
317            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
330/// Alphabetically sort fields for consistency.
331fn 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
341/// Verify IDL is valid.
342fn verify(idl: &Idl) -> Result<()> {
343    // Check full path accounts
344    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    // Check empty discriminators
357    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    // Check potential discriminator collisions
373    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    // Disallow all zero account discriminators
396    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    // Disallow account discriminators that can conflict with the `zero` constraint.
408    //
409    // Problematic scenario:
410    //
411    // 1. Account 1's discriminator starts with 0 (but not all 0s, since that's disallowed)
412    // 2. Account 2's discriminator is a 1-byte custom discriminator
413    // 3. Account 2 gets initialized using the `zero` constraint.
414    //
415    // In this case, it's possible to pass an already initialized Account 1 to a place that expects
416    // non-initialized Account 2, because the first byte of Account 1 is also 0, which is what the
417    // `zero` constraint checks.
418    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}