1use anyhow::{Context, bail};
2use cargo_metadata::{DependencyKind, Package, PackageId, camino::*, semver::VersionReq};
3use convert_case::{Case, Casing};
4use std::{
5 collections::{HashMap, HashSet},
6 env, fs,
7 path::{Path, PathBuf},
8 process::{Command, ExitStatus},
9};
10
11const META_PATH: &[&str] = &["sails_idl_meta", "ProgramMeta"];
12const ICON_BUILD: &str = "🔨";
13const ICON_DONE: &str = "✅";
14const ICON_ERROR: &str = "❌";
15
16pub struct CrateIdlGenerator {
17 manifest_path: Utf8PathBuf,
18 target_dir: Option<Utf8PathBuf>,
19 deps_level: usize,
20 program_name: Option<String>,
21}
22
23impl CrateIdlGenerator {
24 pub fn new(
25 manifest_path: Option<PathBuf>,
26 target_dir: Option<PathBuf>,
27 deps_level: Option<usize>,
28 program_name: Option<String>,
29 ) -> Self {
30 Self {
31 manifest_path: Utf8PathBuf::from_path_buf(
32 manifest_path.unwrap_or_else(|| env::current_dir().unwrap().join("Cargo.toml")),
33 )
34 .unwrap(),
35 target_dir: target_dir
36 .and_then(|p| p.canonicalize().ok())
37 .map(Utf8PathBuf::from_path_buf)
38 .and_then(|t| t.ok()),
39 deps_level: deps_level.unwrap_or(1),
40 program_name,
41 }
42 }
43
44 pub fn generate(self) -> anyhow::Result<()> {
45 println!("...reading metadata: {}", &self.manifest_path);
46 let metadata = cargo_metadata::MetadataCommand::new()
48 .manifest_path(&self.manifest_path)
49 .exec()?;
50
51 let sails_packages = metadata
53 .packages
54 .iter()
55 .filter(|&p| p.name == "sails-rs")
56 .collect::<Vec<_>>();
57 if sails_packages.is_empty() {
58 bail!("`sails-rs` package not found");
59 }
60
61 let target_dir = self
62 .target_dir
63 .as_ref()
64 .unwrap_or(&metadata.target_directory);
65
66 let package_list = get_package_list(&metadata, self.deps_level)?;
67 println!(
68 "...looking for Program implementation in {} package(s)",
69 package_list.len()
70 );
71 for program_package in package_list {
72 let idl_gen = PackageIdlGenerator::new(
73 program_package,
74 &sails_packages,
75 target_dir,
76 &metadata.workspace_root,
77 self.program_name.clone(),
78 );
79 match get_program_struct_path_from_doc(program_package, target_dir) {
80 Ok(program_struct_path) => {
81 println!("...found Program implementation: {program_struct_path}");
82 let file_path = idl_gen.try_generate_for_package(&program_struct_path)?;
83 println!("{ICON_DONE} Generated IDL: {file_path}");
84
85 return Ok(());
86 }
87 Err(err) => {
88 println!("{ICON_ERROR} no Program implementation found: {err}");
89 }
90 }
91 }
92 Err(anyhow::anyhow!("no Program implementation found"))
93 }
94}
95
96struct PackageIdlGenerator<'a> {
97 program_package: &'a Package,
98 sails_packages: &'a Vec<&'a Package>,
99 target_dir: &'a Utf8Path,
100 workspace_root: &'a Utf8Path,
101 program_name: Option<String>,
102}
103
104impl<'a> PackageIdlGenerator<'a> {
105 fn new(
106 program_package: &'a Package,
107 sails_packages: &'a Vec<&'a Package>,
108 target_dir: &'a Utf8Path,
109 workspace_root: &'a Utf8Path,
110 program_name: Option<String>,
111 ) -> Self {
112 Self {
113 program_package,
114 sails_packages,
115 target_dir,
116 workspace_root,
117 program_name,
118 }
119 }
120
121 fn try_generate_for_package(&self, program_struct_path: &str) -> anyhow::Result<Utf8PathBuf> {
122 let sails_dep = self
124 .program_package
125 .dependencies
126 .iter()
127 .find(|p| p.name == "sails-rs" && p.kind == DependencyKind::Normal)
128 .context("failed to find `sails-rs` dependency")?;
129 let sails_features = &sails_dep.features;
131 println!(
132 "...found `sails-rs` dep with features: {:?}",
133 sails_features
134 );
135 let sails_package = self
137 .sails_packages
138 .iter()
139 .find(|p| sails_dep.req == VersionReq::STAR || sails_dep.req.matches(&p.version))
140 .context(format!(
141 "failed to find `sails-rs` package with matching version {}",
142 &sails_dep.req
143 ))?;
144
145 let crate_name = &get_idl_gen_crate_name(self.program_package);
146 let crate_dir = &self.target_dir.join(crate_name);
147 let src_dir = crate_dir.join("src");
148 fs::create_dir_all(&src_dir)?;
149
150 let gen_manifest_path = crate_dir.join("Cargo.toml");
151 write_file(
152 &gen_manifest_path,
153 gen_cargo_toml(self.program_package, sails_package, sails_features),
154 )?;
155
156 let out_file = self
157 .target_dir
158 .join(format!("{}.idl", &self.program_package.name));
159 let program_name = self
160 .program_name
161 .clone()
162 .unwrap_or_else(|| self.program_package.name.to_case(Case::Pascal));
163 let main_rs_path = src_dir.join("main.rs");
164 write_file(
165 main_rs_path,
166 gen_main_rs(program_struct_path, &program_name, &out_file),
167 )?;
168
169 let from_lock = &self.workspace_root.join("Cargo.lock");
170 let to_lock = &crate_dir.join("Cargo.lock");
171 drop(fs::copy(from_lock, to_lock));
172
173 println!("{ICON_BUILD} cargo run --bin {crate_name}");
174 let res = cargo_run_bin(&gen_manifest_path, crate_name, self.target_dir);
175
176 fs::remove_dir_all(crate_dir)?;
177
178 match res {
179 Ok(exit_status) if exit_status.success() => Ok(out_file),
180 Ok(exit_status) => Err(anyhow::anyhow!("Exit status: {}", exit_status)),
181 Err(err) => Err(err),
182 }
183 }
184}
185
186fn get_package_list(
188 metadata: &cargo_metadata::Metadata,
189 deps_level: usize,
190) -> Result<Vec<&Package>, anyhow::Error> {
191 let resolve = metadata
192 .resolve
193 .as_ref()
194 .context("failed to get resolve from metadata")?;
195 let root_package_id = resolve
196 .root
197 .as_ref()
198 .context("failed to find root package")?;
199 let node_map = resolve
200 .nodes
201 .iter()
202 .map(|n| (&n.id, n))
203 .collect::<HashMap<_, _>>();
204 let package_map = metadata
205 .packages
206 .iter()
207 .map(|p| (&p.id, p))
208 .collect::<HashMap<_, _>>();
209
210 let mut deps_set: HashSet<&PackageId> = HashSet::new();
211 deps_set.insert(root_package_id);
212
213 let mut deps = vec![root_package_id];
214 for _ in 0..deps_level {
215 deps = deps
216 .iter()
217 .filter_map(|id| node_map.get(id))
218 .flat_map(|&n| &n.dependencies)
219 .filter(|&id| metadata.workspace_members.contains(id))
220 .collect();
221 if deps.is_empty() {
222 break;
223 }
224 deps_set.extend(deps.iter());
225 }
226 let package_list: Vec<&Package> = deps_set
227 .iter()
228 .filter_map(|id| package_map.get(id))
229 .copied()
230 .collect();
231 Ok(package_list)
232}
233
234fn get_program_struct_path_from_doc(
235 program_package: &Package,
236 target_dir: &Utf8Path,
237) -> anyhow::Result<String> {
238 let program_package_file_name = program_package.name.to_lowercase().replace('-', "_");
239 println!(
240 "...running doc generation for `{}`",
241 program_package.manifest_path
242 );
243 _ = cargo_doc(&program_package.manifest_path, target_dir)?;
245 let docs_path = target_dir
247 .join("doc")
248 .join(format!("{}.json", &program_package_file_name));
249 println!("...reading doc: {docs_path}");
250 let json_string = std::fs::read_to_string(docs_path)?;
251 let doc_crate: rustdoc_types::Crate = serde_json::from_str(&json_string)?;
252
253 let program_meta_id = doc_crate
255 .paths
256 .iter()
257 .find_map(|(id, summary)| (summary.path == META_PATH).then_some(id))
258 .context("failed to find `sails_rs::meta::ProgramMeta` definition in dependencies")?;
259 let program_struct_path = doc_crate
261 .index
262 .values()
263 .find_map(|idx| try_get_trait_implementation_path(idx, program_meta_id))
264 .context("failed to find `sails_rs::meta::ProgramMeta` implementation")?;
265 let program_struct = doc_crate
266 .paths
267 .get(&program_struct_path.id)
268 .context("failed to get Program struct by id")?;
269 let program_struct_path = program_struct.path.join("::");
270 Ok(program_struct_path)
271}
272
273fn try_get_trait_implementation_path(
274 idx: &rustdoc_types::Item,
275 program_meta_id: &rustdoc_types::Id,
276) -> Option<rustdoc_types::Path> {
277 if let rustdoc_types::ItemEnum::Impl(item) = &idx.inner
278 && let Some(tp) = &item.trait_
279 && &tp.id == program_meta_id
280 && let rustdoc_types::Type::ResolvedPath(path) = &item.for_
281 {
282 return Some(path.clone());
283 }
284 None
285}
286
287fn get_idl_gen_crate_name(program_package: &Package) -> String {
288 format!("{}-idl-gen", program_package.name)
289}
290
291fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> anyhow::Result<()> {
292 let path = path.as_ref();
293 fs::write(path, contents.as_ref())
294 .with_context(|| format!("failed to write `{}`", path.display()))
295}
296
297fn cargo_doc(
298 manifest_path: &cargo_metadata::camino::Utf8Path,
299 target_dir: &cargo_metadata::camino::Utf8Path,
300) -> anyhow::Result<ExitStatus> {
301 let cargo_path = std::env::var("CARGO").unwrap_or("cargo".into());
302
303 let mut cmd = Command::new(cargo_path);
304 cmd.env("RUSTC_BOOTSTRAP", "1")
305 .env(
306 "RUSTDOCFLAGS",
307 "-Z unstable-options --output-format=json --cap-lints=allow",
308 )
309 .env("__GEAR_WASM_BUILDER_NO_BUILD", "1")
310 .stdout(std::process::Stdio::null()) .arg("doc")
312 .arg("--manifest-path")
313 .arg(manifest_path.as_str())
314 .arg("--target-dir")
315 .arg(target_dir.as_str())
316 .arg("--no-deps")
317 .arg("--quiet");
318
319 cmd.status()
320 .context("failed to execute `cargo doc` command")
321}
322
323fn cargo_run_bin(
324 manifest_path: &cargo_metadata::camino::Utf8Path,
325 bin_name: &str,
326 target_dir: &cargo_metadata::camino::Utf8Path,
327) -> anyhow::Result<ExitStatus> {
328 let cargo_path = std::env::var("CARGO").unwrap_or("cargo".into());
329
330 let mut cmd = Command::new(cargo_path);
331 cmd.env("CARGO_TARGET_DIR", target_dir)
332 .env("__GEAR_WASM_BUILDER_NO_BUILD", "1")
333 .stdout(std::process::Stdio::null()) .arg("run")
335 .arg("--manifest-path")
336 .arg(manifest_path.as_str())
337 .arg("--bin")
338 .arg(bin_name);
339 cmd.status().context("failed to execute `cargo` command")
340}
341
342fn gen_cargo_toml(
343 program_package: &Package,
344 sails_package: &Package,
345 sails_features: &[String],
346) -> String {
347 let mut manifest = toml_edit::DocumentMut::new();
348 manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new());
349 manifest["package"]["name"] = toml_edit::value(get_idl_gen_crate_name(program_package));
350 manifest["package"]["version"] = toml_edit::value("0.1.0");
351 manifest["package"]["edition"] = toml_edit::value(program_package.edition.as_str());
352
353 let mut dep_table = toml_edit::Table::default();
354 let mut package_table = toml_edit::InlineTable::new();
355 let manifest_dir = program_package.manifest_path.parent().unwrap();
356 package_table.insert("path", manifest_dir.as_str().into());
357 dep_table[&program_package.name] = toml_edit::value(package_table);
358
359 let sails_dep = sails_dep_v2(sails_package, sails_features);
360 dep_table[&sails_package.name] = toml_edit::value(sails_dep);
361
362 manifest["dependencies"] = toml_edit::Item::Table(dep_table);
363
364 let mut bin = toml_edit::Table::new();
365 bin["name"] = toml_edit::value(get_idl_gen_crate_name(program_package));
366 bin["path"] = toml_edit::value("src/main.rs");
367 manifest["bin"]
368 .or_insert(toml_edit::Item::ArrayOfTables(
369 toml_edit::ArrayOfTables::new(),
370 ))
371 .as_array_of_tables_mut()
372 .expect("bin is an array of tables")
373 .push(bin);
374
375 manifest["workspace"] = toml_edit::Item::Table(toml_edit::Table::new());
376
377 manifest.to_string()
378}
379
380fn sails_dep_v2(sails_package: &Package, sails_features: &[String]) -> toml_edit::InlineTable {
381 let mut features = toml_edit::Array::default();
382 features.push("idl-gen");
383 features.push("std");
384 features.extend(sails_features);
385 let mut sails_table = toml_edit::InlineTable::new();
386 let manifest_dir = sails_package.manifest_path.parent().unwrap();
387 sails_table.insert("package", sails_package.name.as_str().into());
388 sails_table.insert("path", manifest_dir.as_str().into());
389 sails_table.insert("features", features.into());
390 sails_table
391}
392
393fn gen_main_rs(
394 program_struct_path: &str,
395 program_name: &str,
396 out_file: &cargo_metadata::camino::Utf8Path,
397) -> String {
398 format!(
399 "
400fn main() {{
401 sails_rs::generate_idl_to_file::<{}>(
402 Some(r\"{}\"),
403 std::path::PathBuf::from(r\"{}\")
404 )
405 .unwrap();
406}}",
407 program_struct_path,
408 program_name,
409 out_file.as_str(),
410 )
411}