dodo-bindgen-cli 2.0.0

A CLI tool for generating TypeScript bindings for libraries built with the `dodo_bindgen` crate
use std::fs::{self, File, create_dir_all};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;

use dodo_bindgen_codegen::{FfiLib, TsModule};

mod cargo;
mod interface;
mod meta_reader;

use cargo::Cargo;

pub type Result<T> = std::result::Result<T, ()>;

fn write_atomic(
    destination: &Path,
    fill: impl FnOnce(&mut File) -> io::Result<()>,
) -> io::Result<()> {
    let mut builder = tempfile::Builder::new();
    #[cfg(unix)]
    builder.permissions(std::os::unix::fs::PermissionsExt::from_mode(0o666));
    let mut file = builder.tempfile_in(destination.parent().unwrap_or(Path::new(".")))?;
    fill(file.as_file_mut())?;
    file.persist(destination)?;
    Ok(())
}

fn main() -> Result<()> {
    let args: interface::Cli = clap::Parser::parse();

    let metadata = Cargo::get_metadata();

    let file_name;
    let out_path;

    if let Some(output) = &args.output {
        file_name = output
            .file_name()
            .expect("invalid output file name")
            .to_str()
            .expect("unknown utf8 character on file name")
            .to_string();
        let mut _out_path = output.clone();
        _out_path.pop();
        out_path = _out_path;
    } else {
        file_name = format!("lib{}.ts", metadata.lib_name);
        out_path = metadata.pkg_path.join("lib");
    }
    create_dir_all(&out_path).expect("failed to create output dir");

    let targets = if args.targets.is_empty() {
        vec![None]
    } else {
        args.targets
            .iter()
            .map(|target| Some(target.as_str()))
            .collect()
    };

    let mut items = None;
    for target in targets {
        let dylib_path = Cargo::build(&metadata.pkg_name, args.profile(), target);
        if items.is_none() {
            items = Some(meta_reader::read(&dylib_path));
        }

        let target = target
            .map(str::to_string)
            .unwrap_or_else(Cargo::host_target);
        let extension = dylib_path
            .extension()
            .and_then(|extension| extension.to_str())
            .expect("library artifact has no extension");
        let destination = out_path.join(FfiLib::file_name(&metadata.lib_name, &target, extension));
        write_atomic(&destination, |file| {
            io::copy(&mut File::open(&dylib_path)?, file)?;
            file.set_permissions(fs::metadata(&dylib_path)?.permissions())
        })
        .unwrap_or_else(|err| {
            panic!(
                "failed to copy `{}` to `{}`: {err}",
                dylib_path.display(),
                destination.display()
            )
        });
        println!("copied {}", destination.display());
    }
    let items = items.expect("no library was built");

    let opts = args.to_codegen_opts(file_name.to_string(), metadata.lib_name.clone());
    let module = TsModule::new(items, &opts);

    let module_path = out_path.join(file_name);
    let mut written = vec![module_path.clone()];
    if args.inline {
        let module = module.generate_single(&opts);
        write_atomic(&module_path, |file| file.write_all(module.as_bytes()))
            .expect("failed to write module file");
    } else {
        let (module, type_defs) = module.generate_multiple(&opts, "rust_type.ts");

        write_atomic(&module_path, |file| file.write_all(module.as_bytes()))
            .expect("failed to write module file");

        let typedefs_path = out_path.join("rust_type.ts");
        write_atomic(&typedefs_path, |file| file.write_all(type_defs.as_bytes()))
            .expect("failed to write typedefs file");
        written.push(typedefs_path);
    }

    match Command::new("deno")
        .args(["fmt", "--quiet"])
        .args(&written)
        .current_dir(&out_path)
        .status()
    {
        Ok(status) if status.success() => {},
        Ok(status) => {
            eprintln!("warning: `deno fmt` exited with {status}, output left unformatted")
        },
        Err(err) => eprintln!("warning: failed to run `deno fmt`: {err}, output left unformatted"),
    }

    println!("{} ready", metadata.pkg_name);

    Ok(())
}