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