Multiple binary versions using Cargo

I am wanting to generate multiple binary versions of a single source file.

I have (roughly)

#[cfg(foo)]
let a = do_foo();
#[cfg(bar)]
let a = do_bar();

I want to be able to build mybin-foo and mybin-bar, using a single invocation of Cargo. So I am looking for something in Cargo.toml that goes like

[[bin]]
name="mybin-foo"
path="src/main.rs"
config="foo"

[[bin]]
name="mybin-bar"
path="src/main.rs"
config="bar"

A one-line pointer to the relevant doc would be fine, I have spent some time looking but not found how to do this.

I would use a trait for this, and put them in separate files.

// src/bin/mybin-foo
use mybin::Main;
fn main() {
    Foo::main();
}

impl Main for Foo {
    fn make_a() -> i32 {
        1
    }
}

// src/bin/mybin-bar
use mybin::Main;
fn main() {
    Bar::main();
}

impl Main for Bar {
    fn make_a() -> i32 {
        2
    }
}

// src/lib.rs
pub trait Main {
    fn main() {
        // all your shared code
        let a = Self::make_a();
    }

    fn make_a() -> i32;
}

AFAIK that doesn't exist.

What you could do is use a feature flag, and then compile the binary 2 times, each with the corresponding feature flag.

One snag there is that you would need to define what happens if both features are provided at the same time.

Having spent several tens of minutes goggling the problem, I am unfortunately not surprised by your assessment. (I had posted in the hope that someone would surprise me, but confirmation is helpful as well.)

I can set up the multiple compilation (not the first time I've decided to make 'make' a driver of Cargo), was hoping there'd be a solution that didn't look like pulling a car with a horse!

Thanks for this. I was hoping not to have a file-per-case; there are a couple of variables, so I'd end up with several files. I should probably have mentioned in the first place: it's about comparing performance options, so 3 algo's for each of 3 aspects makes for a lot of source files. They could be autogenerated I guess, so that might reduce the manual code.

You could change behavior at runtime by examining args_os().next() (argument 0, usually the path to the executable), and then symlink or hard link the "alternative binaries" to the one true executable.[1]

Or make it command argument or environment based and write a tiny shell script wrapper.


  1. This seems less common that it used to be, but it's how rustup installed components work, for example. ↩︎

1 Like

With that many options, I'd do the same thing but use command-line arguments or environment variables to pick which one to use. But if you're just comparing performance and don't actually need them to coexist, you can conditionally compile them with the env! macro, and in a shell script use something like:

PERF_A=1 PERF_B=3 PERF_C=2 cargo install --path . --root temp/perf-1-3-2

Features could also work but they're clunky to use with things that have more than 2 options.

Thanks for looking and suggesting! This is part of a 'learn me a Rust' project --- knowing that something isn't possible is useful!

If it ends up as shellscripts, I'll probably just use make/bash/perl/rust-include to bruteforce generate the sources, then 'rustc yadayada" the binaries.

I am interested in a 'pure Rust ecosystem' solution; I've had a response that seems to confirm my suspicion that there is none.

Thanks for your followup!

My thinking as a grizzled old C programmer...

In a makefile, one would do sort-of...

bin-a-a-a : source.c
gcc foo.c -o bin-a-a-a -D asdf=a -D qwer=a -Dzxcv=a

bin-a-a-b : source.c
gcc foo.c -o bin-a-a-b : source.c

...

bin-c-c-c : source.c
gcc foo.c -o bin-c-c-c -D asdf=c -D qwer=c -Dzxcv=c

and:

targets : bin-a-a-a bin-a-a-b ....

So then, 'make targets' would get me my 27 (or whatever) executables. One source, one makefile. Ok, yes, 3 * 3 * 3 targets, and a rollup target to bind them all.

Key thing is, I'm looking for a Cargo-native solution for this. I'm getting from you that this isn't an option, that's fine, that's something I want to learn!

In general, if you compare Cargo directly to make, you may be disappointed. Cargo largely does a very specific job only: for a given build configuration, it produces Rust binaries or libraries. Build scripts can customize what happens on input, but there are no post-build steps. Therefore, there are a lot of jobs that cannot be done with only Cargo.

So for your case where you want multiple builds with different features, you may find the best option is in fact to use make, or whatever tool you like, to sequence that. If you'd like to do it with “pure Rust”, check out the xtask pattern: you can have Cargo build and run your own custom tooling with a single command.

Also, for the step of actually giving the binaries distinct names rather than overwriting each other in target/, you can use cargo install to ask Cargo to copy the built binary to a particular directory, which might simplify the work — or might complicate it due to cargo install being a bit inflexible about the output path.

4 Likes

I use cargo test with different feature flags, it seems it does distinct builds (generating multiple executable files) so after everything is built the tests can be re-run testing various features without further compilation/linking.

My script:

cargo test %1 --quiet --release -- --nocapture --test-threads 1
cargo test %1 --quiet --release --features unsafe-optim -- --nocapture --test-threads 1

Thanks to all who replied. The initial question ("can I create multiple binary versions using different '#[cfg(...)] options purely in Cargo") has been answered (in the negative).

I've gone with a makefile based solution that runs 'cargo rustc ..." (per kpreid):

target.perf.basic/release/enumerate : src/enumerate/main.rs
cargo rustc --release --bin enumerate --target-dir target.perf.basic -- --cfg DISPATCHER="BASIC"

target.perf.basic-k/release/enumerate : src/enumerate/main.rs
cargo rustc --release --bin enumerate --target-dir target.perf.basic-k -- --cfg DISPATCHER="BASIC" --cfg ENUMERATOR="KEY"

target.perf.threaded/release/enumerate : src/enumerate/main.rs
cargo rustc --release --bin enumerate --target-dir target.perf.threaded -- --cfg DISPATCHER="THREADED"

... etc,

I confess that I'm disappointed with this, in that
a) it seems as if Cargo should/could handle this, but does not, and
b) the doc isn't at all clear about that (OTOH, ok, it doesn't support it, why should it be?).

Agreed, too, that this is something I could try to contribute further on. I'm trying to figure out what the initial "gripping point" would be: I'd need to understand a lot more about Cargo/ecosystem in order to make a useful change.