Proper -sys crate setup for a "split" C library?

I'm in the process of writing bindings to Pugl, a graphics library that supports OpenGL and Cairo backends. Pugl's maintainer requests that distributors provide a separate package for each backend to keep dependencies to a minimum, which seems reasonable. However, this seems to be an awkward fit for the structure of crates; I'm not sure how I could accommodate Pugl's preferred distribution approach without duplicating my build code and/or forcing users to build Pugl repeatedly.

Pugl is only provided as source, and its build script (which uses Waf) builds a platform-specific core library and two backend libraries, one for each backend, provided that OpenGL and/or Cairo development libraries are present on the host. The way it does this is involved enough that I'd rather use it directly than try to copy it with cc or something.

Here are the possible strategies I see for setting up crates around this library, none of which seem ideal:

  • Separate pugl-sys, pugl-cairo-sys, and pugl-gl-sys crates, each mapping onto one of the three libraries that Pugl builds. Users would depend on either pugl-cairo-sys or pugl-gl-sys, which in turn would each depend on pugl-sys. This allows for keeping all the dependencies separate (i.e. pugl-cairo-sys could bring in cairo-rs and the others could leave it out). It also allows the bindings for the core library and both backend libraries to be maintained in one place each. However, it would force users to build Pugl twice when installing the crates and would require duplicating the same build.rs across all three crates.
  • Two crates, pugl-cairo-sys and pugl-gl-sys. This allows users to run the build once and get the whole library as they want it, and keep dependencies for each backend separate. However, it would require duplicating the bindings for the core library across both crates, as well as the build.rs.
  • One crate, pugl-sys. This would link the core with both backends and bring in dependencies for both OpenGL and Cairo, going against the wishes of the Pugl maintainer and adding bloat and complexity to projects depending on pugl-sys. However, it would be the easiest to maintain and install, allowing for one build.rs, one set of bindings, and one build process.

The situation gets further complicated when you consider building Rust-friendly Cairo and OpenGL frontends to the -sys crates.

What should I do here? Is there a better solution I haven't considered, or a way around the problems I've brought up? Linux distros often provide some way to do a "split package" or the like for cases like these, where one build script and one set of sources can be used to generate multiple packages. Cargo doesn't seem to support that kind of approach, but I'm still new to Rust, so maybe there's something I'm missing.

You could create a single crate that uses cargo features to pick either OpenGL, Cairo or both backends.

Oh thanks, that seems like a good solution! I missed that feature in the Cargo docs; reading over the description it seems powerful enough to handle this case elegantly. Technically, users do need to require at least one in order for the library to be useful so they're not exactly optional per se, but that can be comfortably explained in the documentation I think.

The main thing with crate features is that they must be additive — adding a feature must not make things stop compiling. This is because if you have crates A and B that both depend on C, but only B enabled a feature of C, then both A and B get C with the feature enabled.

Requiring that you pick at least one is fine. You can do this:

#[cfg(all(not(feature = "cairo"), not(feature = "opengl")))]
compile_error!("Either feature \"cairo\" or \"opengl\" must be enabled for this crate.");

This is actually one of the examples for the compile_error! macro.

You could even choose to make the api be the same regardless of backend to allow crates that don't care about the backend to work with either. Of course you can still have parts of the api be backend specific, and there are some techniques to mark which items require specific features, see e.g. the tokio docs.

2 Likes

Thanks for the further advice! The crate is nearly ready to release now and I've been able to get everything I wanted by using features as you recommended. The API is basically the same regardless of backend; including a backend just gives you a function you can pass in during init that later determines what kind of drawing context you'll get. I also added a "plain" feature that can be specified in case you don't want any of the backends (like if you want to implement your own).

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.