Is there a mechanism to filter out code if a linked library is absent?

To learn rust, and because games are supposed to be fun, I'm currently working on yet another rogue like, following tutorial there and there.

I started it with tcod-rs, using https://github.com/tomassedovic/tcod-rs which provides rust bindings for libtcod.
But I soon figured out that it was depending on installing sdl2 on computers, and it annoyed me.

So I got rid of tcod-rs, replaced it with piston, and implemented my own version of the field of view calculation. Yes, I know, there are plenty of field of vision calculators out there, but where is the fun in that :smiley:
I'm in the learning phase, not the being efficient phase!

I still keep tcod-rs as a dev-dependency here for benching and test purposes: I wanted to compare my algorithm to tcod-rs to make sure it performs okay.

Now, if I build the executable on a device without sdl2, it works fine, but if I run the benchmark or the tests, it will fail with a nice exit code: 1\n--- stderr\nPackage sdl2 was not found in the pkg-config which is expected, as can be seen in this github action result.

So now, I'm wondering if there is a way to detect at compile time if sdl2 is installed or not, and if not avoid these tests altogether, and process with the rest?

Don't know how good of an idea it may be, but know that you can:

  1. Use something akin to the following build.rs script:

    fn try_main ()
      -> Result<(), Box<dyn ::std::error::Error>>
    {
        if  ::std::process::Command::new("pkg-config")
                .args(&["--libs", "--cflags", "sdl2"])
                .status()?
                .success()
        {
            println!("cargo:rustc-cfg=sdl2");
        }
        println!("cargo:rerun-if-changed=build.rs");
        Ok(())
    }
    
    fn main ()
    {
        if let Err(some_err) = try_main() {
            println!("cargo:warning={}", format_args!(
                "Failed to run `pkg-config`: {}", some_err,
            ));
        }
    }
    
  2. The above script will automagically enable cfg(sdl2) when available, so you can
    #[cfg(sdl2)] annotate the items that depend on it. That coupled with a "good enough" link-time optimization, may avoid linker errors.

That being said, the error you linked comes from "pre-compiling" a dependency, for whom a missing sdl2 is a fatal error. This means it is impossible for you to depend on that crate if you are missing sdl2.

You thus then have two options:

  • Fork that crate to have a missing sdl2 be non fatal (in which case it could showcase a completely empty API, by doing #![cfg(sdl2)], for instance).

  • Wrap your main cargo invocation within a helper script, like I showcased as cargo smart in this other post, so as to be able to express [dependencies.cfg(sdl2)] in a way that actually works.

4 Likes

Thanks for the answer! And now I'm learning about build.rs, which is quite neat.

For this specific case, I probably will not go the extra mile to do it, because it's really for these tests case, and I guess if I'm confident enough with the algo, I could just trash the benchmark anyway.

Fun fact, the answer you showcased with cargo smart was in answer to the friend of mine who pushed me to learn rust :stuck_out_tongue:

2 Likes

You could also hide those tests behind a feature flag. It's manual but does remove the dependency in the normal case.

2 Likes

I'm having a hard time figuring out how: when I look at the feature doc, it doesn't seem to indicate a way to have dev-features. Using [dev-features] in the Cargo.toml file actually throws a error: failed to parse manifest.

Optional also does not seem to be valid for dependencies.

I guess I could make tcod an optional non dev dependency, and then create the feature tcod that would be part of the default? But I somehow really want tcod to not be a dependency of the non test debug and release build.

I see what you mean. I didn't know that dev dependencies weren't allowed to be optional.

1 Like

I just discovered it myself as I was trying you suggestion :smile: