shank_cli/
lib.rs

1use std::{
2    fs::{self, File},
3    io::Write,
4    path::{Path, PathBuf},
5};
6
7use anyhow::{anyhow, format_err, Result};
8use clap::Parser;
9use log::{debug, info};
10use shank_idl::{extract_idl, manifest::Manifest, ParseIdlOpts};
11
12pub const VERSION: &str = env!("CARGO_PKG_VERSION");
13
14#[derive(Debug, Parser)]
15#[clap(version = VERSION)]
16pub struct Opts {
17    #[clap(subcommand)]
18    pub command: Command,
19}
20
21#[derive(Debug, Parser)]
22pub enum Command {
23    Idl {
24        /// Output directory for the IDL JSON.
25        #[clap(short, long, default_value = "idl")]
26        out_dir: String,
27
28        /// Output filename for the IDL JSON [default: <program crate name>.json,
29        /// e.g. "my_program.json"]
30        #[clap(long)]
31        out_filename: Option<String>,
32
33        /// Directory of program crate for which to generate the IDL.
34        #[clap(short = 'r', long)]
35        crate_root: Option<String>,
36
37        /// Manually specify and override the address in the IDL
38        #[clap(short = 'p', long)]
39        program_id: Option<String>,
40    },
41}
42
43pub fn entry(opts: Opts) -> Result<()> {
44    match opts.command {
45        Command::Idl {
46            out_dir,
47            out_filename,
48            crate_root,
49            program_id,
50        } => idl(out_dir, out_filename, crate_root, program_id),
51    }
52}
53
54pub fn try_resolve_path(p: Option<String>, label: &str) -> Result<PathBuf> {
55    let p = match p {
56        Some(crate_root) => Ok(Path::new(&crate_root).to_path_buf()),
57        None => {
58            debug!("No {} provided, assuming in current dir", label);
59            std::env::current_dir()
60        }
61    }?;
62
63    let p = if p.is_absolute() {
64        Ok(p)
65    } else {
66        debug!("{} is relative, resolving from current dir", label);
67        std::env::current_dir().map(|x| x.join(p))
68    }?;
69
70    Ok(p)
71}
72
73pub fn idl(
74    out_dir: String,
75    out_filename: Option<String>,
76    crate_root: Option<String>,
77    program_id: Option<String>,
78) -> Result<()> {
79    // Resolve input and output directories
80    let crate_root = try_resolve_path(crate_root, "crate_root")?;
81    let out_dir = try_resolve_path(Some(out_dir), "out_dir")?;
82    fs::create_dir_all(&out_dir).map_err(|err| {
83        format_err!(
84            "Unable to create out_dir ({}), {}",
85            &out_dir.display(),
86            err
87        )
88    })?;
89
90    // Resolve info about lib for which we generate IDL
91    let cargo_toml = crate_root.join("Cargo.toml");
92    if !cargo_toml.exists() {
93        return Err(anyhow!(
94            "Did not find Cargo.toml at the path: {}",
95            crate_root.display()
96        ));
97    }
98    let manifest = Manifest::from_path(&cargo_toml)?;
99    let lib_rel_path = manifest
100        .lib_rel_path()
101        .ok_or(anyhow!("Program needs to be a lib"))?;
102
103    let lib_full_path_str = crate_root.join(lib_rel_path);
104    let lib_full_path =
105        lib_full_path_str.to_str().ok_or(anyhow!("Invalid Path"))?;
106
107    // Extract IDL and convert to JSON
108    let opts = ParseIdlOpts {
109        program_address_override: program_id,
110        ..ParseIdlOpts::default()
111    };
112    let idl = extract_idl(lib_full_path, opts)?
113        .ok_or(anyhow!("No IDL could be extracted"))?;
114    let idl_json = idl.try_into_json()?;
115
116    // Write to JSON file
117    let out_filename = if let Some(out_filename) = out_filename {
118        out_filename
119    } else {
120        format!("{}.json", manifest.lib_name()?)
121    };
122    let idl_json_path = out_dir.join(out_filename);
123    let mut idl_json_file = File::create(&idl_json_path)?;
124    info!("Writing IDL to {}", &idl_json_path.display());
125
126    idl_json_file.write_all(idl_json.as_bytes())?;
127
128    Ok(())
129}