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#[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 Init {
31 #[arg(long)]
33 enable_nix: bool,
34 #[arg(long)]
36 overwrite: bool,
37 },
38 Clean,
40}
41
42struct CargoMetadata {
44 root: cargo::Root,
45 version: String,
46 name: String,
47 module: String,
48}
49
50fn 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
70pub 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
81fn 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 if overwrite {
96 cmd_clean(&name)?;
97 }
98
99 let crate_type = get_crate_type(root).ok_or(Error::NoCargoLibTarget)?;
101
102 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 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 fs::write(
125 cabal.clone(),
126 cabal::generate(&name, &module, &version, enable_nix),
127 )
128 .or(Err(Error::FailedToWriteFile(cabal)))?;
129
130 fs::write("hsbindgen.toml", hsbindgen::generate(&module))
132 .map_err(|_| Error::FailedToWriteFile("hsbindgen.toml".to_owned()))?;
133
134 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 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
161fn 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}