cabal_foreign_library/
lib.rs

1//! A library for Cargo [build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html)
2//! to build and link a Cabal [foreign library](https://cabal.readthedocs.io/en/3.4/cabal-package.html#foreign-libraries)
3//! to Rust crates. The crate calls out to Cabal and GHC; all necesssary Haskell dependencies must
4//! be installed and managed separately.
5//!
6//! Everything is a work-in-progress!
7//!
8//! # Example
9//!
10//! For a basic usage example, see [`examples/basic`](https://github.com/mirryi/cabal-foreign-library/tree/master/examples/basic).
11
12mod error;
13mod util;
14
15use std::process::Command;
16use std::{fs, str};
17
18use camino::{Utf8Path, Utf8PathBuf};
19use regex::Regex;
20
21use util::{out_dir, package, CommandStdoutExt, DYLIB_EXT};
22
23pub use bindgen;
24pub use error::*;
25
26/// A builder for a Cabal library.
27#[derive(Debug)]
28pub struct Build {
29    cabal: Utf8PathBuf,
30    ghc_pkg: Utf8PathBuf,
31    rts_version: RTSVersion,
32}
33
34/// A handler for a library built by Cabal.
35#[derive(Debug)]
36pub struct Lib<'b> {
37    build: &'b Build,
38    path: Utf8PathBuf,
39    hs_deps: Vec<HSDep>,
40}
41
42#[derive(Debug, Clone, Copy)]
43enum HSDep {
44    Ghc,
45    Base,
46}
47
48/// Builder for the Rust bindings for the Haskell library.
49pub type BindgenBuilder = bindgen::Builder;
50
51/// Alias for Result.
52pub type Result<T> = std::result::Result<T, Error>;
53
54/// The version of the Haskell runtime library.
55#[derive(Debug, Clone, Copy)]
56pub enum RTSVersion {
57    NonThreaded,
58    NonThreadedL,
59    NonThreadedDebug,
60    Threaded,
61    ThreadedL,
62    ThreadedDebug,
63}
64
65impl RTSVersion {
66    fn default() -> Self {
67        Self::NonThreaded
68    }
69}
70
71impl Build {
72    /// Construct a new instance with a default configuration.
73    pub fn new() -> Result<Self> {
74        let cabal = util::which("cabal").map_err(Error::CabalError)?;
75        let ghc_pkg = util::which("ghc-pkg").map_err(Error::GHCPkgError)?;
76        Ok(Self {
77            cabal,
78            ghc_pkg,
79            rts_version: RTSVersion::default(),
80        })
81    }
82
83    /// Set the `cabal` binary.
84    ///
85    /// By default, `PATH` is searched for the `cabal` binary.
86    pub fn use_cabal(&mut self, path: impl AsRef<Utf8Path>) -> &mut Self {
87        self.cabal = path.as_ref().to_path_buf();
88        self
89    }
90
91    /// Set the `ghc-pkg` binary.
92    ///
93    /// By default, `PATH` is searched for the `ghc-pkg` binary.
94    pub fn use_ghc_pkg(&mut self, path: impl AsRef<Utf8Path>) -> &mut Self {
95        self.ghc_pkg = path.as_ref().to_path_buf();
96        self
97    }
98
99    /// Set the version of the GHC RTS.
100    ///
101    /// By default, [`RTSVersion::NonThreaded`] is used.
102    pub fn use_rts(&mut self, rts_version: RTSVersion) -> &mut Self {
103        self.rts_version = rts_version;
104        self
105    }
106
107    /// Build the foreign library with cabal. The resulting [`Lib`] handler can be used to link and
108    /// generate bindings.
109    pub fn build(&mut self) -> Result<Lib> {
110        // build
111        let status = self
112            .cabal_cmd("build")
113            .status()
114            .map_err(|err| Error::BuildError(Some(err)))?;
115        if !status.success() {
116            return Err(Error::BuildError(None));
117        }
118
119        // find the dylib file
120        let path = self
121            .cabal_cmd("list-bin")
122            .arg(util::package())
123            .stdout_trim()
124            .map(Utf8PathBuf::from)
125            .map_err(|err| Error::BuildError(Some(err)))?;
126
127        Ok(Lib {
128            build: self,
129            path,
130            // TODO somehow pull necessary dependencies from cabal? or just link all system
131            // dependencies?
132            hs_deps: vec![HSDep::Ghc, HSDep::Base],
133        })
134    }
135
136    fn cabal_cmd(&self, cmd: &str) -> Command {
137        let mut cabal = Command::new(&self.cabal);
138        cabal.args([cmd, "--builddir", &out_dir()]);
139        cabal
140    }
141
142    fn ghc_pkg_cmd(&self, cmd: &str) -> Command {
143        let mut ghc_pkg = Command::new(&self.ghc_pkg);
144        ghc_pkg.args([cmd]);
145        ghc_pkg
146    }
147}
148
149impl<'b> Lib<'b> {
150    /// Link the crate to the dynamic library.
151    ///
152    /// If `rpath` is true, the runpath of the resulting executable is modified to include the
153    /// directory of the compiled foreign library.
154    pub fn link(&self, rpath: bool) -> Result<()> {
155        let dir = self.path.parent().unwrap();
156        println!("cargo:rustc-link-search=native={}", dir);
157        println!("cargo:rustc-link-lib=dylib={}", &package());
158
159        if rpath {
160            println!("cargo:rustc-link-arg=-Wl,-rpath,{}", dir);
161        }
162
163        Ok(())
164    }
165
166    /// Return a [`bindgen::Builder`] for generating the Rust bindings of the dynamic library. The
167    /// builder is already configued for the correct header and includes files; additional
168    /// configuration may be performed as necessary.
169    ///
170    /// See documentation for [`bindgen::Builder`].
171    pub fn bindings(&self) -> Result<bindgen::Builder> {
172        // find GHC RTS headers to be included
173        let rts_headers = self
174            .build
175            .ghc_pkg_cmd("field")
176            .args(["rts", "include-dirs", "--simple-output"])
177            .stdout_trim()
178            .map(Utf8PathBuf::from)
179            .map_err(InvocationError::IoError)
180            .map_err(Error::GHCPkgError)?;
181
182        // find the stub file
183        let stub = self
184            .path
185            .parent()
186            .unwrap()
187            .join(format!("{}-tmp", package()))
188            .join("Lib_stub.h");
189
190        // invoke bindgen
191        let builder = bindgen::Builder::default()
192            .clang_args(["-isystem", rts_headers.as_str()])
193            .header(stub)
194            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
195
196        Ok(builder)
197    }
198
199    /// Link the crate to the Haskell system libraries, which are discovered via `ghc-pkg`.
200    ///
201    /// If `rpath` is true, the runpath of the resulting executable is modified to include the
202    /// directory of the system libraries.
203    pub fn link_system(&self, rpath: bool) -> Result<()> {
204        // retrieve dynamic libraries directory.
205        let ghc_lib_dir = self
206            .build
207            .ghc_pkg_cmd("field")
208            .args(["rts", "dynamic-library-dirs", "--simple-output"])
209            .stdout_trim()
210            .map_err(InvocationError::IoError)
211            .map_err(Error::GHCPkgError)?;
212        let ghc_lib_dir = fs::canonicalize(ghc_lib_dir).unwrap();
213
214        // regexes to match the necessary system dependencies.
215        let version_regex = Regex::new(r"((\d+)\.)+?(\d+)").unwrap();
216
217        let non_rts_prefixes = self
218            .hs_deps
219            .iter()
220            .map(HSDep::prefix)
221            .collect::<Vec<_>>()
222            .join("|");
223        let non_rts_regex = Regex::new(&format!(
224            r"^lib({prefix})-({version})-ghc({version})\.{ext}$",
225            prefix = non_rts_prefixes,
226            version = version_regex,
227            ext = DYLIB_EXT
228        ))
229        .unwrap();
230
231        let rts_suffix = self.build.rts_version.suffix();
232        let rts_regex = Regex::new(&format!(
233            r"^libHSrts-({version})({suffix})-ghc({version})\.{ext}$",
234            version = version_regex,
235            suffix = rts_suffix,
236            ext = DYLIB_EXT
237        ))
238        .unwrap();
239
240        // link matching library files
241        println!("cargo:rustc-link-search=native={}", ghc_lib_dir.display());
242        for entry in fs::read_dir(&ghc_lib_dir).unwrap() {
243            let entry = entry.unwrap();
244
245            if let Some(i) = entry.file_name().to_str() {
246                if non_rts_regex.is_match(i) || rts_regex.is_match(i) {
247                    // get rid of lib from the file name
248                    let temp = i.split_at(3).1;
249                    // get rid of the .so from the file name
250                    let trimmed = temp.split_at(temp.len() - DYLIB_EXT.len() - 1).0;
251
252                    println!("cargo:rustc-link-lib=dylib={}", trimmed);
253                }
254            }
255        }
256
257        if rpath {
258            println!("cargo:rustc-link-arg=-Wl,-rpath,{}", ghc_lib_dir.display());
259        }
260
261        // TODO error if failed to find some libraries
262        Ok(())
263    }
264}
265
266impl RTSVersion {
267    fn suffix(&self) -> &str {
268        match self {
269            RTSVersion::NonThreaded => "",
270            RTSVersion::NonThreadedL => "_l",
271            RTSVersion::NonThreadedDebug => "_debug",
272            RTSVersion::Threaded => "_thr",
273            RTSVersion::ThreadedL => "_thr_l",
274            RTSVersion::ThreadedDebug => "_thr_debug",
275        }
276    }
277}
278
279impl HSDep {
280    fn prefix(&self) -> &str {
281        match self {
282            HSDep::Ghc => "HSghc",
283            HSDep::Base => "HSbase",
284        }
285    }
286}