1use anyhow::{bail, Context, Result};
2use clap::{Args, Parser, Subcommand};
3use clap_cargo_extra::ClapCargo;
4use heck::ToKebabCase;
5use regex::Regex;
6use std::{
7 collections::HashMap,
8 fs::{read, OpenOptions},
10 io::Write,
11 path::{Path, PathBuf},
12};
13use syn::File;
14use witgen_macro_helper::{parse_crate_as_file, Resolver, Wit};
15
16#[derive(Parser, Debug)]
17#[clap(
18 author = "Benjamin Coenen <benjamin.coenen@hotmail.com>, Willem Wyndham <willem@ahalabs.dev>"
19)]
20pub struct App {
21 #[clap(subcommand)]
22 pub command: Command,
23}
24
25#[derive(Debug, Subcommand)]
26pub enum Command {
27 #[clap(alias = "gen")]
29 Generate(Witgen),
30}
31
32#[derive(Debug, Args)]
33pub struct Witgen {
34 #[clap(long, short = 'i')]
36 pub input: Option<PathBuf>,
37
38 #[clap(long, short = 'd', default_value = ".")]
43 pub input_dir: PathBuf,
44
45 #[clap(long, short = 'o', default_value = "index.wit")]
47 pub output: PathBuf,
48
49 #[clap(long, short = 'b')]
51 pub prefix_file: Vec<PathBuf>,
52
53 #[clap(long, short = 'a')]
56 pub prefix_string: Vec<String>,
57
58 #[clap(long)]
60 pub stdout: bool,
61
62 #[clap(long)]
64 pub skip_resolve: bool,
65
66 #[clap(long)]
68 pub skip_prologue: bool,
69
70 #[clap(flatten)]
71 pub cargo: ClapCargo,
72}
73
74impl Witgen {
75 pub fn from_path(path: &Path) -> Self {
76 Self {
77 input: None,
78 input_dir: path.to_path_buf(),
79 output: PathBuf::from("index.wit"),
80 prefix_file: vec![],
81 prefix_string: vec![],
82 stdout: false,
83 cargo: ClapCargo::default(),
84 skip_resolve: false,
85 skip_prologue: true,
86 }
87 }
88
89 pub fn gen_from_path(path: &Path) -> Result<String> {
90 let witgen = Witgen::from_path(path);
91 witgen.generate_str(witgen.read_input()?)
92 }
93
94 #[allow(dead_code)]
96 pub fn gen_static_from_path(path: &Path) -> Result<String> {
97 let witgen = Witgen::from_path(path);
98 witgen.resolve(&witgen.generate_str(witgen.read_input()?)?)
99 }
100
101 pub fn read_input(&self) -> Result<File> {
102 let input = self
104 .input
105 .as_ref()
106 .map_or_else(|| self.input_dir.join("src/lib.rs"), |i| i.clone());
107
108 if !input.exists() {
109 bail!("input {:?} doesn't exist", input);
110 }
111 parse_crate_as_file(&input)
112 }
113
114 pub fn generate_str(&self, file: File) -> Result<String> {
115 let wit: Wit = file.into();
116 let mut wit_str = if self.skip_prologue {
117 String::new()
118 } else {
119 format!("// auto-generated file by witgen (https://github.com/bnjjj/witgen), please do not edit yourself, you can generate a new one thanks to cargo witgen generate command. (cargo-witgen v{}) \n\n", env!("CARGO_PKG_VERSION"))
120 };
121 if !self.prefix_string.is_empty() {
122 wit_str.push_str(&self.prefix_string.join("\n"));
123 wit_str.push('\n');
124 }
125 for path in &self.prefix_file {
126 let prefix_file = String::from_utf8(read(path)?)?;
127 wit_str.push_str(&prefix_file);
128 wit_str.push('\n');
129 }
130 wit_str.push_str(&wit.to_string());
131 Ok(wit_str)
132 }
133
134 pub fn write_output(&self, wit_str: &str) -> Result<()> {
135 if self.stdout {
136 println!("{wit_str}");
137 } else {
138 write_file(&self.output, wit_str)?;
139 }
140 Ok(())
141 }
142
143 pub fn resolve_wit(&self, wit_str: &str) -> Result<HashMap<String, String>> {
144 let mut resolver = WitResolver::new(&self.cargo);
145 let _ = resolver.parse_wit_interface(
146 self.output.to_str().expect("failed to decode output"),
147 wit_str,
148 )?;
149 Ok(resolver.wit_generated)
150 }
151
152 pub fn run(&self) -> Result<()> {
153 let input = self.read_input()?;
154 let mut wit_str = self.generate_str(input)?;
155 if !self.skip_resolve {
156 wit_str = self.resolve(&wit_str)?;
157 }
158 self.write_output(&wit_str)
159 }
160
161 pub fn resolve(&self, wit_str: &str) -> Result<String> {
162 let dep_wit = self
163 .resolve_wit(wit_str)?
164 .into_values()
165 .collect::<Vec<String>>()
166 .join("\n");
167
168 let re = Regex::new(r"^use .+\n").unwrap();
170 let mut res = re.replace_all(wit_str, "").to_string();
171 res.push_str(&dep_wit);
172 Ok(res)
173 }
174}
175
176struct WitResolver<'a> {
177 cargo: &'a ClapCargo,
178 wit_generated: HashMap<String, String>,
179}
180
181impl<'a> WitResolver<'a> {
182 fn new(cargo: &'a ClapCargo) -> Self {
183 Self {
184 cargo,
185 wit_generated: Default::default(),
186 }
187 }
188}
189
190impl Resolver for WitResolver<'_> {
191 fn resolve_name(&mut self, name: &str) -> Result<String> {
192 let package = self
193 .cargo
194 .find_package(name)?
195 .or_else(|| {
196 self.cargo
197 .find_package(&name.to_kebab_case())
198 .unwrap_or(None)
199 })
200 .map_or_else(|| bail!("Failed to find {name}"), Ok)?;
201
202 let manifest_dir = package.manifest_path.as_std_path().parent().map_or_else(
203 || bail!("failed to find parent of {}", package.manifest_path),
204 Ok,
205 )?;
206
207 let res = Witgen::gen_from_path(manifest_dir)?;
208 self.wit_generated.insert(name.to_string(), res.clone());
209 Ok(res)
210 }
211}
212
213impl Command {
214 pub fn run(&self) -> Result<()> {
215 match self {
216 Command::Generate(witgen) => witgen.run()?,
217 };
218 Ok(())
219 }
220}
221
222impl App {
223 #[allow(dead_code)]
224 pub fn run(&self) -> Result<()> {
225 self.command.run()
226 }
227}
228
229fn write_file(path: &Path, contents: &str) -> Result<()> {
230 let mut file = OpenOptions::new()
231 .write(true)
232 .truncate(true)
233 .create(true)
234 .open(path)
235 .expect("cannot create file to generate wit");
236 file.write_all(contents.as_bytes())
237 .context("cannot write to file")?;
238 file.flush().context("cannot flush file")?;
239 Ok(())
240}