sails_cli/
idlgen.rs

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        // get metadata with deps
38        let metadata = cargo_metadata::MetadataCommand::new()
39            .manifest_path(&self.manifest_path)
40            .exec()?;
41
42        // find `sails-rs` packages (any version )
43        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        // find `sails-rs` dependency
112        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        // find `sails-rs` package matches dep version
119        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
161/// Get list of packages from the root package and its dependencies
162fn 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    // run `cargo doc`
219    _ = cargo_doc(&program_package.manifest_path, target_dir)?;
220    // read doc
221    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    // find `sails_rs::meta::ProgramMeta` path id
229    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    // find struct implementing `sails_rs::meta::ProgramMeta`
235    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()) // Don't pollute output
286        .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()) // Don't pollute output
309        .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}