mrubyedge_cli/subcommands/
wasm.rs1extern 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}