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