mrubyedge-cli 1.1.12

mruby/edge cli endpoint - run, compile to wasm, etc.
Documentation
extern crate mruby_compiler2_sys;
extern crate rand;

use clap::Args;
use std::{
    env,
    fs::{File, rename},
    io::Read,
    path::{Path, PathBuf},
    process::Command,
};

use askama::Template;
use rand::Rng;

use crate::rbs_parser;
use crate::template;

const MRUBY_EDGE_DEFAULT_VERSION: &str = ">= 1";

#[derive(Debug, Clone, Args)]
pub struct WasmArgs {
    #[arg(short = 'f', long)]
    fnname: Option<String>,
    #[arg(short = 'm', long)]
    mruby_edge_version: Option<String>,
    #[arg(short = 'F', long)]
    features: Vec<String>,
    #[arg(short = 'W', long)]
    no_wasi: bool,
    #[arg(short = 'o', long)]
    out_path: Option<PathBuf>,
    #[arg(long)]
    skip_cleanup: bool,
    #[arg(long)]
    debug_mruby_edge: bool,
    #[arg(long)]
    verbose: bool,
    #[arg(long)]
    strip_binary: bool,
    path: PathBuf,
}

fn sh_do(sharg: &str, debug: bool) -> Result<(), Box<dyn std::error::Error>> {
    println!("running: `{}`", sharg);
    let out = Command::new("/bin/sh").args(["-c", sharg]).output()?;
    if debug && !out.stdout.is_empty() {
        println!(
            "stdout:\n{}",
            String::from_utf8_lossy(&out.stdout).to_string().trim()
        );
    }
    if debug && !out.stderr.is_empty() {
        println!(
            "stderr:\n{}",
            String::from_utf8_lossy(&out.stderr).to_string().trim()
        );
    }
    if !out.status.success() {
        println!("{:?}", out.status);
        panic!("failed to execute command");
    }

    Ok(())
}

fn file_prefix_of(file: &Path) -> Option<String> {
    file.file_name()?
        .to_str()?
        .split('.')
        .next()
        .map(|s| s.to_string())
}

fn debug_println(debug: bool, msg: &str) {
    if debug {
        eprintln!("{}", msg);
    }
}

pub fn execute(args: WasmArgs) -> Result<(), Box<dyn std::error::Error>> {
    let mut rng = rand::rng();
    let suffix: String = (0..32)
        .map(|_| {
            let chars = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            chars[rng.random_range(0..chars.len())] as char
        })
        .collect();

    let fnname = args.fnname;
    let path = args.path;
    let mrubyfile = std::fs::canonicalize(&path)?;
    let fname = file_prefix_of(mrubyfile.as_path()).unwrap();

    let pwd = std::env::current_dir()?;
    std::env::set_current_dir(std::env::var("TMPDIR").unwrap_or("/tmp".to_string()))?;

    let dirname = format!("work-mrubyedge-{}", suffix);
    std::fs::create_dir(&dirname)?;
    std::env::set_current_dir(format!("./work-mrubyedge-{}", &suffix))?;
    std::fs::create_dir("src")?;

    let code = std::fs::read_to_string(&mrubyfile)?;
    let out_file = format!("src/{}.mrb", fname);

    if args.verbose {
        unsafe {
            let mut context = mruby_compiler2_sys::MRubyCompiler2Context::new();
            context.dump_bytecode(&code)?;
        }
    }
    unsafe {
        mruby_compiler2_sys::MRubyCompiler2Context::new()
            .compile_to_file(&code, out_file.as_ref())?
    }

    let mut features = Vec::new();
    if args.no_wasi {
        features.push("no-wasi");
    } else {
        features.push("wasi");
    }
    for f in args.features.iter() {
        features.push(f.as_str());
    }
    let mrubyedge_feature = features
        .iter()
        .map(|s| format!("\"{}\"", s))
        .collect::<Vec<String>>()
        .join(", ");

    if args.debug_mruby_edge {
        let mruby_edge_crate_path = env::var("MRUBYEDGE_LOCAL_CRATE_PATH").unwrap_or_else(|_| {
            "/Users/udzura/ghq/github.com/mrubyedge/mrubyedge/mrubyedge".to_string()
        });
        let cargo_toml = template::cargo_toml::CargoTomlDebug {
            mruby_edge_crate_path: &mruby_edge_crate_path,
            mrubyedge_feature: &mrubyedge_feature,
        };
        std::fs::write("Cargo.toml", cargo_toml.render()?)?;
    } else {
        let cargo_toml = template::cargo_toml::CargoToml {
            mrubyedge_version: &args
                .mruby_edge_version
                .unwrap_or_else(|| MRUBY_EDGE_DEFAULT_VERSION.to_string()),
            mrubyedge_feature: &mrubyedge_feature,
            strip: &args.strip_binary.to_string(),
        };
        std::fs::write("Cargo.toml", cargo_toml.render()?)?;
    }

    let export_rbs_fname = format!("{}.export.rbs", fname);
    let export_rbs = mrubyfile.parent().unwrap().join(&export_rbs_fname);

    let mut ftypes_imports = Vec::new();
    let import_rbs_fname = format!("{}.import.rbs", fname);
    let import_rbs = mrubyfile.parent().unwrap().join(&import_rbs_fname);
    if import_rbs.exists() {
        debug_println(
            args.verbose,
            &format!(
                "detected import.rbs: {}",
                import_rbs.as_path().to_string_lossy()
            ),
        );
        let mut f = File::open(import_rbs)?;
        let mut s = String::new();
        f.read_to_string(&mut s)?;

        let (_, parsed) = rbs_parser::parse(&s).unwrap();
        let parsed: &mut [rbs_parser::FuncDef] = Vec::leak(parsed);
        for def in parsed.iter() {
            ftypes_imports.push(template::RustImportFnTemplate {
                func_name: &def.name,
                args_decl: def.args_decl(),
                rettype_decl: def.rettype_decl(),
                imported_body: def.imported_body(),
                import_helper_var: def.import_helper_var(),
            })
        }
    }

    let cont = if export_rbs.exists() {
        debug_println(
            args.verbose,
            &format!(
                "detected export.rbs: {}",
                export_rbs.as_path().to_string_lossy()
            ),
        );
        let mut f = File::open(export_rbs)?;
        let mut s = String::new();
        f.read_to_string(&mut s)?;

        let (_, parsed) = rbs_parser::parse(&s).unwrap();
        let mut ftypes = vec![];
        let parsed: &mut [rbs_parser::FuncDef] = Vec::leak(parsed);
        for def in parsed.iter() {
            ftypes.push(template::RustFnTemplate {
                func_name: &def.name,
                args_decl: def.args_decl(),
                args_let_vec: def.args_let_vec(),
                str_args_converter: def.str_args_converter(),
                rettype_decl: def.rettype_decl(),
                handle_retval: def.handle_retval(),
                exported_helper_var: def.exported_helper_var(),
            })
        }

        let lib_rs = template::LibRs {
            file_basename: &fname,
            ftypes: &ftypes,
            ftypes_imports: &ftypes_imports,
        };

        lib_rs.render()?
    } else {
        if fnname.is_none() {
            panic!("--fnname FNNAME should be specified when export.rbs does not exist")
        }
        let fnname = fnname.unwrap();

        let ftypes = vec![template::RustFnTemplate {
            func_name: &fnname,
            args_decl: "",
            args_let_vec: "vec![]",
            str_args_converter: "",
            rettype_decl: "-> ()",
            handle_retval: "()",
            exported_helper_var: "",
        }];

        let lib_rs = template::LibRs {
            file_basename: &fname,
            ftypes: &ftypes,
            ftypes_imports: &ftypes_imports,
        };

        lib_rs.render()?
    };
    debug_println(args.verbose, "[debug] will generate main.rs:");
    debug_println(args.verbose, &cont);
    std::fs::write("src/lib.rs", cont)?;

    let target = if args.no_wasi {
        "wasm32-unknown-unknown"
    } else {
        "wasm32-wasip1"
    };

    sh_do(
        &format!("cargo build --target {} --release", target),
        args.verbose,
    )?;

    let output_path = if let Some(out_path) = &args.out_path {
        std::fs::canonicalize(out_path).unwrap_or_else(|_| pwd.join(out_path))
    } else {
        pwd.join(format!("{}.wasm", &fname))
    };

    let from = format!("./target/{}/release/mywasm.wasm", target);
    let to = output_path.to_str().expect("Invalid output path");
    rename(from, to)?;
    if args.skip_cleanup {
        println!(
            "debug: working directory for compile wasm is remained in {}",
            std::env::current_dir()?.as_os_str().to_str().unwrap()
        );
    } else {
        std::env::set_current_dir("..")?;
        sh_do(&format!("rm -rf {}", &dirname), args.verbose)?;
    }

    std::env::set_current_dir(pwd)?;

    println!(
        "[ok] wasm file is generated: {}",
        &output_path.to_string_lossy()
    );

    Ok(())
}