Conditional compilation for Rust build.rs script? - rust

The Rust language supports conditional compilation using attributes like #[cfg(test)].
Rust also supports build scripts using a build.rs file to run code as part of the build process to prepare for compilation.
I would like to use conditional compilation in Rust code to conditionally compile depending on whether we're compiling for a build script, similar to how that is possible for test builds.
Imagine the following:
#[cfg(build)]
fn main() {
// Part of build script
}
#[cfg(not(build))]
fn main() {
// Not part of build script, probably regular build
}
This does not work, because build is not a valid identifier here.
Is it possible to do this using a different attribute, or could some other trick be used to achieve something similar?
For some context on this issue:
My goal is to generate shell completion scripts through clap at compile time. I've quite a comprehensive App definition across multiple files in the application. I'd like to use this in build.rs by including these parts using the include!(...) macro (as suggested by clap), so I don't have to define App a second time. This pulls some dependencies with it, which I'd like to exclude when used by the build.rs file as they aren't needed in that case. This is what I'm trying to make available in my build.rs script.

You can just put the build code in build.rs (or presumably have build.rs declare a mod xyz to pull in another file).
I wonder if the question you are trying to ask is whether you can reference the same code from build.rs and main.rs, and if so can that code tell if it's being called by one or the other. It seems you could switch on an environment variable set when using build.rs (using something like option_env, but possibly a nicer way might be to enable a feature in the main code from within build.rs.
(Have a read of the documentation for build scripts if you haven't already.)

This is what works for me:
fn main() {
if std::env::var("PROFILE").unwrap() == "debug" {
// Here I do something that is needed for tests
// only, not for 'release'
}
}

Related

Is it possible to have example-specific build.rs file?

In my library I have few examples -- (normally) each one is represented by a single x<N>.rs file living in examples directory.
One example uses a .proto file -- this file needs to be compiled during build (of said example) and it's generated output is used by example itself.
I've tried this in my Cargo.toml:
[[example]]
name = "x1"
path = "examples/x1/main.rs"
build = "examples/x1/build.rs"
but build key gets ignored when I run cargo build --example x1
Is it possible to have example-specific build.rs file?
If not -- what is the correct way to deal with this situation?
Edit: I ended up processing that .proto file in crate's build.rs (even though it is not required to build that crate) and using artefacts in the example like this:
pub mod my_proto {
include!(concat!(env!("OUT_DIR"), "/my_proto.rs"));
}
This is not possible. This issue explains why, but in a nutshell build scripts are used for whole crate. So you could move your example into separate crate.

How to specify the target_os in `Cargo.toml`?

I'm trying to write a crate that only works on linux. I have done some searches but all answers are just talking about:
add #[cfg(target_os = xxx)] in the source code
add [target.'cfg(target_os = "linux")'.dependencies] in
Cargo.toml.
But in my case, the crate entirely won't work in other platform so I don't want to add #[cfg] in my code. If I do so, I need to add it to every piece of my code.
Is there a easy way to make my code only compile in specific platform?
You can conditionally include an invocation of compile_error:
#[cfg(not(target_os = "linux"))]
compile_error!("only linux is supported");
This only needs to appear in your code once; I'd suggest somewhere near the start of the root module.

Is it possible to use .env file at build time?

I'm coming to Rust from the JavaScript world, where you could have a .env file (git-ignored), and environmental values from this file could be made available at runtime via an npm package (say, dotenv).
I was able to use a dotenv crate to make this work similarly when I just cargo run the project, but after I build a release version, the value of the environment variable is lost and the app panics.
I understand that cargo doesn't evaluate env variables at build time, and using env!-like macros won't work either, at least to my best understanding.
What I'm asking is this: how do I store a variable in a file (that I can gitignore), make Rust pick up values from this file, and use them at build time so that these values are available to the release-built app?
I'm sure that there's a well-established way of doing this, but I struggle to figure it out.
There is the dotenv-codegen crate that provides a dotenv! macro that works like the env! macro except it will load from a .env file.
I'm not so sure this is well-established, but you can use a build script that will read the file and use println!("cargo:rustc-env=VAR=VALUE") to send the environment variables to Cargo, allowing you to retrieve them in the code with env!() or option_env!().
For example, to use a .env file, add dotenv to build-dependencies, and use it like so in build.rs:
fn main() {
let dotenv_path = dotenv::dotenv().expect("failed to find .env file");
println!("cargo:rerun-if-changed={}", dotenv_path.display());
// Warning: `dotenv_iter()` is deprecated! Roll your own or use a maintained fork such as `dotenvy`.
for env_var in dotenv::dotenv_iter().unwrap() {
let (key, value) = env_var.unwrap();
println!("cargo:rustc-env={key}={value}");
}
}

"cargo check" gives dead code warning for function used only by test [duplicate]

I'm writing a program in Rust and I have some tests for it. I wrote a helper function for these tests, but whenever I build using cargo build it warns me that the function is never used:
warning: function is never used: ... #[warn(dead_code)] on by default
How I can mark this function as used so as not to get the warnings?
Specific question
How I can mark this function as used so as not to get the warnings?
The Rust compiler runs many lints to warn you about possible issues in your code and the dead_code lint is one of them. It can be very useful in pointing out mistakes when code is complete, but may also be a nuisance at earlier stages. Often, this can be solved by either deleting unused code, or by marking a public method. However, all lints can be turned off by allowing them, and your error message (#[warn(dead_code)] on by default) contains the name of the lint you could disable.
#[allow(dead_code)]
fn my_unused_function() {}
Alternative for testing
I wrote a helper function for these tests, but whenever I build using cargo build it warns me that the function is never used.
This happens to be a special case, which is that code that is only used for testing isn't needed in the real executable and should probably not be included.
In order to optionally disable compilation of test code, you can mark it accordingly using the cfg attribute with the test profile.
#[cfg(test)]
fn my_test_specific_function() {}
When marked in this way, the compiler knows to ignore the method during compilation. This is similar to commonly used ifdef usage in other languages like C or C++, where you are telling a preprocessor to ignore the enclosed code unless TESTING is defined.
#ifdef TESTING
...
#endif
For people getting this warning while making a rust library, you may get this if you don't have your modules set to pub in your lib.rs.
pub mod foo;
If something is only used in tests, it should be omitted altogether. This can be done with the #[cfg(test)] attribute.
dead_code is a lint, which means you can allow it on the thing that's causing it to trigger.
#[allow(dead_code)]
fn dummy() {}
fn main() {}
There is another situation where this can occur. If you have several helper functions in a module, e.g. in tests/utils/mod.rs and then several integration tests (tests/a.rs, tests/b.rs) each of which does
mod utils;
use utils::...;
then you will get dead code warnings if you do not use all of the code from all of the tests. For example if test a.rs only uses utils::foo and b.rs only uses utils::bar then you will get dead code warnings for both.
That is because each test is compiled as an independent crate. Here is the bug report for it. It looks difficult to solve so I wouldn't hold my breath.
You can disable specific lints for a whole project in Rust by going into your main.rs file and adding the following at the very top of the file:
#![allow(
dead_code,
unused_imports
)]
You max prefix the unused function name with an underscore:
fn _dummy() {}
fn main() {}
See: https://doc.rust-lang.org/rustc/lints/listing/warn-by-default.html#dead-code
For some reason I found that setting the main function to public:
pub fn main()
and then copying my main.rs file to lib.rs
cp src/main.rs src/lib.rs
then recompiling fixed this.
No need to use a macro for it, although the macro should work for non main functions.

How to add code only for specific Rust version without using a build script?

I use a method that appeared in Rust 1.10 for my tests but I want my crate to also work with version 1.7.
Is there a way (something like attribute #[cfg(min_version="1.10")]) to specify code that should only run in Rust 1.10 or newer?
I could use a build script, but I don't want a more complicated build just because I wanted to test my crate on an older Rust version.
While there is no way except build scripts (and in the future procedural macros) to check compiler versions, you can use feature flags to manually enable and disable code.
Usually you want to use some new compiler feature to provide new functionality which you could not do with the old compiler. In that case you use the cfg attribute with feature flags which you define to enable code. E.g.
#[cfg(feature = "foo")]
pub fn foo() {
cool_new_compiler_function();
}
And in your Cargo.toml:
[features]
foo = []
Hiding code behind feature flags like this also works in test code. In your specific case you could alternately introduce a legacy feature and disable tests using modern code like this:
#[test]
#[cfg(not(feature = "legacy"))]
fn test_foo() {
Foo::foo();
}
And then to run tests in legacy mode you run:
cargo test --features "legacy"
Note that doing it the latter way means that your tests will not compile by default on older compilers. For normal (non-test) code, adding such a legacy flag would be a very bad, breaking change.

Resources