mrubyedge_cli/subcommands/
wasm.rs

1extern crate mruby_compiler2_sys;
2extern crate rand;
3
4use clap::Args;
5use std::{
6    fs::{File, rename},
7    io::Read,
8    path::{Path, PathBuf},
9    process::Command,
10};
11
12use askama::Template;
13use rand::distributions::{Alphanumeric, DistString};
14
15use crate::rbs_parser;
16use crate::template;
17
18const MRUBY_EDGE_DEFAULT_VERSION: &str = ">= 1";
19
20#[derive(Debug, Clone, Args)]
21pub struct WasmArgs {
22    #[arg(short = 'f', long)]
23    fnname: Option<String>,
24    #[arg(short = 'm', long)]
25    mruby_edge_version: Option<String>,
26    #[arg(short = 'F', long)]
27    features: Vec<String>,
28    #[arg(short = 'W', long)]
29    no_wasi: bool,
30    #[arg(short = 'o', long)]
31    out_path: Option<PathBuf>,
32    #[arg(long)]
33    skip_cleanup: bool,
34    #[arg(long)]
35    debug_mruby_edge: bool,
36    #[arg(long)]
37    verbose: bool,
38    #[arg(long)]
39    strip_binary: bool,
40    path: PathBuf,
41}
42
43fn sh_do(sharg: &str, debug: bool) -> Result<(), Box<dyn std::error::Error>> {
44    println!("running: `{}`", sharg);
45    let out = Command::new("/bin/sh").args(["-c", sharg]).output()?;
46    if debug && !out.stdout.is_empty() {
47        println!(
48            "stdout:\n{}",
49            String::from_utf8_lossy(&out.stdout).to_string().trim()
50        );
51    }
52    if debug && !out.stderr.is_empty() {
53        println!(
54            "stderr:\n{}",
55            String::from_utf8_lossy(&out.stderr).to_string().trim()
56        );
57    }
58    if !out.status.success() {
59        println!("{:?}", out.status);
60        panic!("failed to execute command");
61    }
62
63    Ok(())
64}
65
66fn file_prefix_of(file: &Path) -> Option<String> {
67    file.file_name()?
68        .to_str()?
69        .split('.')
70        .next()
71        .map(|s| s.to_string())
72}
73
74fn debug_println(debug: bool, msg: &str) {
75    if debug {
76        eprintln!("{}", msg);
77    }
78}
79
80pub fn execute(args: WasmArgs) -> Result<(), Box<dyn std::error::Error>> {
81    let mut rng = rand::thread_rng();
82    let suffix = Alphanumeric.sample_string(&mut rng, 32);
83
84    let fnname = args.fnname;
85    let path = args.path;
86    let mrubyfile = std::fs::canonicalize(&path)?;
87    let fname = file_prefix_of(mrubyfile.as_path()).unwrap();
88
89    let pwd = std::env::current_dir()?;
90    std::env::set_current_dir(std::env::var("TMPDIR").unwrap_or("/tmp".to_string()))?;
91
92    let dirname = format!("work-mrubyedge-{}", suffix);
93    std::fs::create_dir(&dirname)?;
94    std::env::set_current_dir(format!("./work-mrubyedge-{}", &suffix))?;
95    std::fs::create_dir("src")?;
96
97    let code = std::fs::read_to_string(&mrubyfile)?;
98    let out_file = format!("src/{}.mrb", fname);
99
100    if args.verbose {
101        unsafe {
102            let mut context = mruby_compiler2_sys::MRubyCompiler2Context::new();
103            context.dump_bytecode(&code)?;
104        }
105    }
106    unsafe {
107        mruby_compiler2_sys::MRubyCompiler2Context::new()
108            .compile_to_file(&code, out_file.as_ref())?
109    }
110
111    let mut features = Vec::new();
112    if args.no_wasi {
113        features.push("no-wasi");
114    } else {
115        features.push("wasi");
116    }
117    for f in args.features.iter() {
118        features.push(f.as_str());
119    }
120    let mrubyedge_feature = features
121        .iter()
122        .map(|s| format!("\"{}\"", s))
123        .collect::<Vec<String>>()
124        .join(", ");
125
126    if args.debug_mruby_edge {
127        let cargo_toml = template::cargo_toml::CargoTomlDebug {
128            mruby_edge_crate_path: "/Users/udzura/ghq/github.com/udzura/mrubyedge/mrubyedge",
129            mrubyedge_feature: &mrubyedge_feature,
130        };
131        std::fs::write("Cargo.toml", cargo_toml.render()?)?;
132    } else {
133        let cargo_toml = template::cargo_toml::CargoToml {
134            mrubyedge_version: &args
135                .mruby_edge_version
136                .unwrap_or_else(|| MRUBY_EDGE_DEFAULT_VERSION.to_string()),
137            mrubyedge_feature: &mrubyedge_feature,
138            strip: &args.strip_binary.to_string(),
139        };
140        std::fs::write("Cargo.toml", cargo_toml.render()?)?;
141    }
142
143    let export_rbs_fname = format!("{}.export.rbs", fname);
144    let export_rbs = mrubyfile.parent().unwrap().join(&export_rbs_fname);
145
146    let mut ftypes_imports = Vec::new();
147    let import_rbs_fname = format!("{}.import.rbs", fname);
148    let import_rbs = mrubyfile.parent().unwrap().join(&import_rbs_fname);
149    if import_rbs.exists() {
150        debug_println(
151            args.verbose,
152            &format!(
153                "detected import.rbs: {}",
154                import_rbs.as_path().to_string_lossy()
155            ),
156        );
157        let mut f = File::open(import_rbs)?;
158        let mut s = String::new();
159        f.read_to_string(&mut s)?;
160
161        let (_, parsed) = rbs_parser::parse(&s).unwrap();
162        let parsed: &mut [rbs_parser::FuncDef] = Vec::leak(parsed);
163        for def in parsed.iter() {
164            ftypes_imports.push(template::RustImportFnTemplate {
165                func_name: &def.name,
166                args_decl: def.args_decl(),
167                rettype_decl: def.rettype_decl(),
168                imported_body: def.imported_body(),
169                import_helper_var: def.import_helper_var(),
170            })
171        }
172    }
173
174    let cont = if export_rbs.exists() {
175        debug_println(
176            args.verbose,
177            &format!(
178                "detected export.rbs: {}",
179                export_rbs.as_path().to_string_lossy()
180            ),
181        );
182        let mut f = File::open(export_rbs)?;
183        let mut s = String::new();
184        f.read_to_string(&mut s)?;
185
186        let (_, parsed) = rbs_parser::parse(&s).unwrap();
187        let mut ftypes = vec![];
188        let parsed: &mut [rbs_parser::FuncDef] = Vec::leak(parsed);
189        for def in parsed.iter() {
190            ftypes.push(template::RustFnTemplate {
191                func_name: &def.name,
192                args_decl: def.args_decl(),
193                args_let_vec: def.args_let_vec(),
194                str_args_converter: def.str_args_converter(),
195                rettype_decl: def.rettype_decl(),
196                handle_retval: def.handle_retval(),
197                exported_helper_var: def.exported_helper_var(),
198            })
199        }
200
201        let lib_rs = template::LibRs {
202            file_basename: &fname,
203            ftypes: &ftypes,
204            ftypes_imports: &ftypes_imports,
205        };
206
207        lib_rs.render()?
208    } else {
209        if fnname.is_none() {
210            panic!("--fnname FNNAME should be specified when export.rbs does not exist")
211        }
212        let fnname = fnname.unwrap();
213
214        let ftypes = vec![template::RustFnTemplate {
215            func_name: &fnname,
216            args_decl: "",
217            args_let_vec: "vec![]",
218            str_args_converter: "",
219            rettype_decl: "-> ()",
220            handle_retval: "()",
221            exported_helper_var: "",
222        }];
223
224        let lib_rs = template::LibRs {
225            file_basename: &fname,
226            ftypes: &ftypes,
227            ftypes_imports: &ftypes_imports,
228        };
229
230        lib_rs.render()?
231    };
232    debug_println(args.verbose, "[debug] will generate main.rs:");
233    debug_println(args.verbose, &cont);
234    std::fs::write("src/lib.rs", cont)?;
235
236    let target = if args.no_wasi {
237        "wasm32-unknown-unknown"
238    } else {
239        "wasm32-wasip1"
240    };
241
242    sh_do(
243        &format!("cargo build --target {} --release", target),
244        args.verbose,
245    )?;
246
247    let output_path = if let Some(out_path) = &args.out_path {
248        std::fs::canonicalize(out_path).unwrap_or_else(|_| pwd.join(out_path))
249    } else {
250        pwd.join(format!("{}.wasm", &fname))
251    };
252
253    let from = format!("./target/{}/release/mywasm.wasm", target);
254    let to = output_path.to_str().expect("Invalid output path");
255    rename(from, to)?;
256    if args.skip_cleanup {
257        println!(
258            "debug: working directory for compile wasm is remained in {}",
259            std::env::current_dir()?.as_os_str().to_str().unwrap()
260        );
261    } else {
262        std::env::set_current_dir("..")?;
263        sh_do(&format!("rm -rf {}", &dirname), args.verbose)?;
264    }
265
266    std::env::set_current_dir(pwd)?;
267
268    println!(
269        "[ok] wasm file is generated: {}",
270        &output_path.to_string_lossy()
271    );
272
273    Ok(())
274}