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::Rng;
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::rng();
83    let suffix: String = (0..32)
84        .map(|_| {
85            let chars = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
86            chars[rng.random_range(0..chars.len())] as char
87        })
88        .collect();
89
90    let fnname = args.fnname;
91    let path = args.path;
92    let mrubyfile = std::fs::canonicalize(&path)?;
93    let fname = file_prefix_of(mrubyfile.as_path()).unwrap();
94
95    let pwd = std::env::current_dir()?;
96    std::env::set_current_dir(std::env::var("TMPDIR").unwrap_or("/tmp".to_string()))?;
97
98    let dirname = format!("work-mrubyedge-{}", suffix);
99    std::fs::create_dir(&dirname)?;
100    std::env::set_current_dir(format!("./work-mrubyedge-{}", &suffix))?;
101    std::fs::create_dir("src")?;
102
103    let code = std::fs::read_to_string(&mrubyfile)?;
104    let out_file = format!("src/{}.mrb", fname);
105
106    if args.verbose {
107        unsafe {
108            let mut context = mruby_compiler2_sys::MRubyCompiler2Context::new();
109            context.dump_bytecode(&code)?;
110        }
111    }
112    unsafe {
113        mruby_compiler2_sys::MRubyCompiler2Context::new()
114            .compile_to_file(&code, out_file.as_ref())?
115    }
116
117    let mut features = Vec::new();
118    if args.no_wasi {
119        features.push("no-wasi");
120    } else {
121        features.push("wasi");
122    }
123    for f in args.features.iter() {
124        features.push(f.as_str());
125    }
126    let mrubyedge_feature = features
127        .iter()
128        .map(|s| format!("\"{}\"", s))
129        .collect::<Vec<String>>()
130        .join(", ");
131
132    if args.debug_mruby_edge {
133        let mruby_edge_crate_path = env::var("MRUBYEDGE_LOCAL_CRATE_PATH").unwrap_or_else(|_| {
134            "/Users/udzura/ghq/github.com/mrubyedge/mrubyedge/mrubyedge".to_string()
135        });
136        let cargo_toml = template::cargo_toml::CargoTomlDebug {
137            mruby_edge_crate_path: &mruby_edge_crate_path,
138            mrubyedge_feature: &mrubyedge_feature,
139        };
140        std::fs::write("Cargo.toml", cargo_toml.render()?)?;
141    } else {
142        let cargo_toml = template::cargo_toml::CargoToml {
143            mrubyedge_version: &args
144                .mruby_edge_version
145                .unwrap_or_else(|| MRUBY_EDGE_DEFAULT_VERSION.to_string()),
146            mrubyedge_feature: &mrubyedge_feature,
147            strip: &args.strip_binary.to_string(),
148        };
149        std::fs::write("Cargo.toml", cargo_toml.render()?)?;
150    }
151
152    let export_rbs_fname = format!("{}.export.rbs", fname);
153    let export_rbs = mrubyfile.parent().unwrap().join(&export_rbs_fname);
154
155    let mut ftypes_imports = Vec::new();
156    let import_rbs_fname = format!("{}.import.rbs", fname);
157    let import_rbs = mrubyfile.parent().unwrap().join(&import_rbs_fname);
158    if import_rbs.exists() {
159        debug_println(
160            args.verbose,
161            &format!(
162                "detected import.rbs: {}",
163                import_rbs.as_path().to_string_lossy()
164            ),
165        );
166        let mut f = File::open(import_rbs)?;
167        let mut s = String::new();
168        f.read_to_string(&mut s)?;
169
170        let (_, parsed) = rbs_parser::parse(&s).unwrap();
171        let parsed: &mut [rbs_parser::FuncDef] = Vec::leak(parsed);
172        for def in parsed.iter() {
173            ftypes_imports.push(template::RustImportFnTemplate {
174                func_name: &def.name,
175                args_decl: def.args_decl(),
176                rettype_decl: def.rettype_decl(),
177                imported_body: def.imported_body(),
178                import_helper_var: def.import_helper_var(),
179            })
180        }
181    }
182
183    let cont = if export_rbs.exists() {
184        debug_println(
185            args.verbose,
186            &format!(
187                "detected export.rbs: {}",
188                export_rbs.as_path().to_string_lossy()
189            ),
190        );
191        let mut f = File::open(export_rbs)?;
192        let mut s = String::new();
193        f.read_to_string(&mut s)?;
194
195        let (_, parsed) = rbs_parser::parse(&s).unwrap();
196        let mut ftypes = vec![];
197        let parsed: &mut [rbs_parser::FuncDef] = Vec::leak(parsed);
198        for def in parsed.iter() {
199            ftypes.push(template::RustFnTemplate {
200                func_name: &def.name,
201                args_decl: def.args_decl(),
202                args_let_vec: def.args_let_vec(),
203                str_args_converter: def.str_args_converter(),
204                rettype_decl: def.rettype_decl(),
205                handle_retval: def.handle_retval(),
206                exported_helper_var: def.exported_helper_var(),
207            })
208        }
209
210        let lib_rs = template::LibRs {
211            file_basename: &fname,
212            ftypes: &ftypes,
213            ftypes_imports: &ftypes_imports,
214        };
215
216        lib_rs.render()?
217    } else {
218        if fnname.is_none() {
219            panic!("--fnname FNNAME should be specified when export.rbs does not exist")
220        }
221        let fnname = fnname.unwrap();
222
223        let ftypes = vec![template::RustFnTemplate {
224            func_name: &fnname,
225            args_decl: "",
226            args_let_vec: "vec![]",
227            str_args_converter: "",
228            rettype_decl: "-> ()",
229            handle_retval: "()",
230            exported_helper_var: "",
231        }];
232
233        let lib_rs = template::LibRs {
234            file_basename: &fname,
235            ftypes: &ftypes,
236            ftypes_imports: &ftypes_imports,
237        };
238
239        lib_rs.render()?
240    };
241    debug_println(args.verbose, "[debug] will generate main.rs:");
242    debug_println(args.verbose, &cont);
243    std::fs::write("src/lib.rs", cont)?;
244
245    let target = if args.no_wasi {
246        "wasm32-unknown-unknown"
247    } else {
248        "wasm32-wasip1"
249    };
250
251    sh_do(
252        &format!("cargo build --target {} --release", target),
253        args.verbose,
254    )?;
255
256    let output_path = if let Some(out_path) = &args.out_path {
257        std::fs::canonicalize(out_path).unwrap_or_else(|_| pwd.join(out_path))
258    } else {
259        pwd.join(format!("{}.wasm", &fname))
260    };
261
262    let from = format!("./target/{}/release/mywasm.wasm", target);
263    let to = output_path.to_str().expect("Invalid output path");
264    rename(from, to)?;
265    if args.skip_cleanup {
266        println!(
267            "debug: working directory for compile wasm is remained in {}",
268            std::env::current_dir()?.as_os_str().to_str().unwrap()
269        );
270    } else {
271        std::env::set_current_dir("..")?;
272        sh_do(&format!("rm -rf {}", &dirname), args.verbose)?;
273    }
274
275    std::env::set_current_dir(pwd)?;
276
277    println!(
278        "[ok] wasm file is generated: {}",
279        &output_path.to_string_lossy()
280    );
281
282    Ok(())
283}