Skip to main content

mrubyedge_cli/subcommands/
wasm.rs

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