mod cabal;
mod cargo;
#[macro_use]
mod errors;
mod flake;
mod hsbindgen;
use cargo::{get_crate_type, CrateType};
use clap::{arg, Parser, Subcommand};
use errors::Error;
use std::{fs, path::Path};
#[derive(Parser)]
#[command(version)]
struct Args {
#[command(subcommand)]
cabal: Wrapper,
}
#[derive(Subcommand)]
enum Wrapper {
#[command(subcommand)]
Cabal(Commands),
}
#[derive(Subcommand)]
enum Commands {
Init {
#[arg(long)]
enable_nix: bool,
#[arg(long)]
overwrite: bool,
},
Clean,
}
struct CargoMetadata {
root: cargo::Root,
version: String,
name: String,
module: String,
}
fn parse_cargo_toml() -> Result<CargoMetadata, Error> {
let cargo = fs::read_to_string("Cargo.toml").or(Err(Error::NoCargoToml))?;
let root: cargo::Root = toml::from_str(&cargo).or(Err(Error::WrongCargoToml))?;
let package = root.clone().package.ok_or(Error::NotCargoPackage)?;
let version = package.version.unwrap_or_else(|| "0.1.0.0".to_owned());
let name = package.name.ok_or(Error::NoCargoNameField)?;
let module = name
.split(&['-', '_'])
.map(|s| format!("{}{}", &s[..1].to_uppercase(), &s[1..]))
.collect::<Vec<String>>()
.join("");
Ok(CargoMetadata {
root,
version,
name,
module,
})
}
pub fn parse_cli_args(args: Vec<String>) -> Result<(), Error> {
let metadata = parse_cargo_toml()?;
match Args::parse_from(args).cabal {
Wrapper::Cabal(command) => match command {
Commands::Init { .. } => cmd_init(command, metadata),
Commands::Clean => cmd_clean(&metadata.name),
},
}
}
fn cmd_init(args: Commands, metadata: CargoMetadata) -> Result<(), Error> {
let Commands::Init {
enable_nix,
overwrite,
} = args else { unreachable!() };
let CargoMetadata {
root,
version,
name,
module,
} = metadata;
if overwrite {
cmd_clean(&name)?;
}
let crate_type = get_crate_type(root).ok_or(Error::NoCargoLibTarget)?;
let cabal = format!("{name}.cabal");
(!(Path::new(&cabal).exists()
|| Path::new(".hsbindgen").exists()
|| Path::new("hsbindgen.toml").exists()
|| Path::new("Setup.hs").exists()
|| Path::new("Setup.lhs").exists()))
.then_some(())
.ok_or_else(|| Error::CabalFilesExist(name.to_owned()))?;
if crate_type == CrateType::DynLib {
(!Path::new("build.rs").exists())
.then_some(())
.ok_or(Error::BuildFileExist)?;
}
if enable_nix {
(!Path::new("flake.rs").exists())
.then_some(())
.ok_or(Error::FlakeFileExist)?;
}
fs::write(
cabal.clone(),
cabal::generate(&name, &module, &version, enable_nix),
)
.or(Err(Error::FailedToWriteFile(cabal)))?;
fs::write("hsbindgen.toml", hsbindgen::generate(&module))
.map_err(|_| Error::FailedToWriteFile("hsbindgen.toml".to_owned()))?;
if crate_type == CrateType::DynLib {
fs::write("build.rs", include_str!("build.rs"))
.map_err(|_| Error::FailedToWriteFile("build.rs".to_owned()))?;
}
if enable_nix {
fs::write("flake.nix", flake::generate(&name))
.map_err(|_| Error::FailedToWriteFile("flake.nix".to_owned()))?;
} else {
fs::write("Setup.lhs", include_str!("Setup.lhs"))
.map_err(|_| Error::FailedToWriteFile("Setup.lhs".to_owned()))?;
}
println!(
"\
Cabal files generated!
**********************
You should now be able to compile your library with `cabal build` and should
add `hs-bindgen` to your crate dependencies list and decorate the Rust function
you want to expose with `#[hs_bindgen]` attribute macro."
);
Ok(())
}
fn cmd_clean(name: &str) -> Result<(), Error> {
let _ = fs::remove_file(format!("{name}.cabal"));
let _ = fs::remove_file(".hsbindgen");
let _ = fs::remove_file("hsbindgen.toml");
let _ = fs::remove_file("Setup.hs");
let _ = fs::remove_file("Setup.lhs");
Ok(())
}