lib/
lib.rs

1mod cabal;
2mod cargo;
3#[macro_use]
4mod errors;
5mod flake;
6mod hsbindgen;
7
8use cargo::{get_crate_type, CrateType};
9use clap::{arg, Parser, Subcommand};
10use errors::Error;
11use std::{fs, path::Path};
12
13/// A tool that helps you to turn in one command a Rust crate into a Haskell Cabal library
14#[derive(Parser)]
15#[command(version)]
16struct Args {
17    #[command(subcommand)]
18    cabal: Wrapper,
19}
20
21#[derive(Subcommand)]
22enum Wrapper {
23    #[command(subcommand)]
24    Cabal(Commands),
25}
26
27#[derive(Subcommand)]
28enum Commands {
29    /// Initialize the project by generating custom Cabal files
30    Init {
31        /// Generate a haskell.nix / naersk based flake.nix
32        #[arg(long)]
33        enable_nix: bool,
34        /// Run a clean before generating files
35        #[arg(long)]
36        overwrite: bool,
37    },
38    /// Remove files generated by cargo-cabal, except flake.nix
39    Clean,
40}
41
42// TODO: rather use https://crates.io/crates/cargo_metadata?!
43struct CargoMetadata {
44    root: cargo::Root,
45    version: String,
46    name: String,
47    module: String,
48}
49
50/// Parse Cargo.toml file content ...
51fn parse_cargo_toml() -> Result<CargoMetadata, Error> {
52    let cargo = fs::read_to_string("Cargo.toml").or(Err(Error::NoCargoToml))?;
53    let root: cargo::Root = toml::from_str(&cargo).or(Err(Error::WrongCargoToml))?;
54    let package = root.clone().package.ok_or(Error::NotCargoPackage)?;
55    let version = package.version.unwrap_or_else(|| "0.1.0.0".to_owned());
56    let name = package.name.ok_or(Error::NoCargoNameField)?;
57    let module = name
58        .split(&['-', '_'])
59        .map(|s| format!("{}{}", &s[..1].to_uppercase(), &s[1..]))
60        .collect::<Vec<String>>()
61        .join("");
62    Ok(CargoMetadata {
63        root,
64        version,
65        name,
66        module,
67    })
68}
69
70/// Parse `cargo-cabal` CLI arguments
71pub fn parse_cli_args(args: Vec<String>) -> Result<(), Error> {
72    let metadata = parse_cargo_toml()?;
73    match Args::parse_from(args).cabal {
74        Wrapper::Cabal(command) => match command {
75            Commands::Init { .. } => cmd_init(command, metadata),
76            Commands::Clean => cmd_clean(&metadata.name),
77        },
78    }
79}
80
81/// Initialize the project by generating custom Cabal files
82fn cmd_init(args: Commands, metadata: CargoMetadata) -> Result<(), Error> {
83    let Commands::Init {
84        enable_nix,
85        overwrite,
86    } = args else { unreachable!() };
87    let CargoMetadata {
88        root,
89        version,
90        name,
91        module,
92    } = metadata;
93
94    // `cargo cabal init --overwrite` == `cargo cabal clean && cargo cabal init`
95    if overwrite {
96        cmd_clean(&name)?;
97    }
98
99    // Check that project have a `crate-type` target ...
100    let crate_type = get_crate_type(root).ok_or(Error::NoCargoLibTarget)?;
101
102    // Check that `cargo cabal init` have not been already run ...
103    let cabal = format!("{name}.cabal");
104    (!(Path::new(&cabal).exists()
105        || Path::new(".hsbindgen").exists()
106        || Path::new("hsbindgen.toml").exists()
107        || Path::new("Setup.hs").exists()
108        || Path::new("Setup.lhs").exists()))
109    .then_some(())
110    .ok_or_else(|| Error::CabalFilesExist(name.to_owned()))?;
111    // ... and that no existing file would conflict ...
112    if crate_type == CrateType::DynLib {
113        (!Path::new("build.rs").exists())
114            .then_some(())
115            .ok_or(Error::BuildFileExist)?;
116    }
117    if enable_nix {
118        (!Path::new("flake.rs").exists())
119            .then_some(())
120            .ok_or(Error::FlakeFileExist)?;
121    }
122
123    // Generate wanted files from templates ... starting by a `.cabal` ...
124    fs::write(
125        cabal.clone(),
126        cabal::generate(&name, &module, &version, enable_nix),
127    )
128    .or(Err(Error::FailedToWriteFile(cabal)))?;
129
130    // `hsbindgen.toml` is a config file readed by `#[hs_bindgen]` proc macro ...
131    fs::write("hsbindgen.toml", hsbindgen::generate(&module))
132        .map_err(|_| Error::FailedToWriteFile("hsbindgen.toml".to_owned()))?;
133
134    // If `crate-type = [ "cdylib" ]` then a custom `build.rs` is needed ...
135    if crate_type == CrateType::DynLib {
136        fs::write("build.rs", include_str!("build.rs"))
137            .map_err(|_| Error::FailedToWriteFile("build.rs".to_owned()))?;
138    }
139
140    // `--enable-nix` CLI option generate a `flake.nix` rather than a `Setup.lhs`
141    if enable_nix {
142        fs::write("flake.nix", flake::generate(&name))
143            .map_err(|_| Error::FailedToWriteFile("flake.nix".to_owned()))?;
144    } else {
145        fs::write("Setup.lhs", include_str!("Setup.lhs"))
146            .map_err(|_| Error::FailedToWriteFile("Setup.lhs".to_owned()))?;
147    }
148
149    println!(
150        "\
151Cabal files generated!
152**********************
153You should now be able to compile your library with `cabal build` and should
154add `hs-bindgen` to your crate dependencies list and decorate the Rust function
155you want to expose with `#[hs_bindgen]` attribute macro."
156    );
157
158    Ok(())
159}
160
161/// Remove files generated by cargo-cabal, except flake.nix
162fn cmd_clean(name: &str) -> Result<(), Error> {
163    let _ = fs::remove_file(format!("{name}.cabal"));
164    let _ = fs::remove_file(".hsbindgen");
165    let _ = fs::remove_file("hsbindgen.toml");
166    let _ = fs::remove_file("Setup.hs");
167    let _ = fs::remove_file("Setup.lhs");
168    Ok(())
169}