Not really: idiomatically, build.rs should generate code to OUT_DIR and not to the src, and that would be quite a different pattern. But yeah, I should’ve added build.rs approach as penultimate one.
Between build.rs writing to OUT_DIR, and test modifying src, I find that test work better, when it is feasible:
Generated code is human visible.
If this is a library, than reverse-dependencies don’t have to run build.rs (build-dependencies are contagious, while dev-dependencies are not).
With tests, you get more control over when the code generation is run, which is helpful when you hacking on code generation itself. More generally, build.rs is a pain to debug.
Kinda esoteric, but tests allow reflectiony-things – unlike build.rs, the test has access to the crate itself, so it can use types defined therein. As an example, in rust-analyzer we use the real Config type we use at runtime to generate documentation (source)
However, if you want the output of code generation to actually not be reproducible, and instead depend on the host where the compilation is happening (this is useful, to, e.g., bind to system’s libraries), build.rs is the only way to go. That’s the core difference between two approaches: one runs code generation at build time, another at development time.
As an aside, I always found it disagreeable that Rust assumes tests are only run during development. I can see many reasons (different platforms, different versions of dependencies, to name a few) why I as a user would want to run tests of a crate I use. But maybe I am missing something.
Hm, I don’t think Rust assumes that? You can run tests of dependencies. For example, in rust-analyzer I can do cargo test --package num_cpus to check if that dependency works.
Though, to be fair, this often fails (with a nice error message) if the dependency in question have dev-dependencies. Cargo intentionally doesn’t prepare dev-dependencies of dependencies, so it can’t compile tests using those. But it doesn’t seem to be a fundamental limitation – I think it would be feasible to make cargo test --package dep for any dependency, it just needs some code and design legwork (eg, how lockfiles and transitive dev dependencies interact).
In practice, when I want to run tests for my dependency, I usually just checkout the dependency locally and run the tests there.
Rust’s declarative macros (”macro_rules”) seem pretty similar. They’re a bit more verbose, sure, and a little harder to read maybe. But fundamentally similar in that they’re declarative.
Procedural macros, where you write arbitrary code to produce an AST from an AST, are definitely more difficult—but also more powerful.
proc macros are token -> token conversions instead of AST -> AST conversions, it’s one of the reasons you need quote/syn for most macro crates. The rfc explains the reasoning here, the big one being so changes to the AST don’t break procedural macros.
This is interesting, I do agree with the other comments that it feels better as part of the build process and not the tests. But on another note, I’ve never really ran into the need for self-modifying code and I’ve been programming for 20 years so I thought I’d run into nearly everything. Is there a certain problem set that lends itself to self modifying code?
I have a feeling it lends itself well in the context of maybe the problem domain of compilers or other sort of things that are development related, but I can’t think of a use case off the top off of my head.
Edit: I forgot about targeting multiple platforms where it won’t necessarily build and you don’t want a separate code base for each platform, beyond that I cannot think of anything that probably wouldn’t be more readable or elegant using another design pattern.
Instead of doing this via a test, I think the idiomatic approach is to run this code in
build.rs
whenever the specific source file changes.Not really: idiomatically, build.rs should generate code to OUT_DIR and not to the src, and that would be quite a different pattern. But yeah, I should’ve added build.rs approach as penultimate one.
Between build.rs writing to OUT_DIR, and test modifying src, I find that test work better, when it is feasible:
build.rs
(build-dependencies
are contagious, whiledev-dependencies
are not).build.rs
is a pain to debug.Config
type we use at runtime to generate documentation (source)However, if you want the output of code generation to actually not be reproducible, and instead depend on the host where the compilation is happening (this is useful, to, e.g., bind to system’s libraries), build.rs is the only way to go. That’s the core difference between two approaches: one runs code generation at build time, another at development time.
Yeah that makes sense. Pity that
cargo
doesn’t exposedev-dependencies
and the crate in a more general fashion, like ascripts
subdirectory.As an aside, I always found it disagreeable that Rust assumes tests are only run during development. I can see many reasons (different platforms, different versions of dependencies, to name a few) why I as a user would want to run tests of a crate I use. But maybe I am missing something.
Hm, I don’t think Rust assumes that? You can run tests of dependencies. For example, in rust-analyzer I can do
cargo test --package num_cpus
to check if that dependency works.Though, to be fair, this often fails (with a nice error message) if the dependency in question have
dev-dependencies
. Cargo intentionally doesn’t prepare dev-dependencies of dependencies, so it can’t compile tests using those. But it doesn’t seem to be a fundamental limitation – I think it would be feasible to makecargo test --package dep
for any dependency, it just needs some code and design legwork (eg, how lockfiles and transitive dev dependencies interact).In practice, when I want to run tests for my dependency, I usually just checkout the dependency locally and run the tests there.
I kind of love that the build process is coupled to running tests here though.
The downside is that
build.rs
has to be compiled and run before compiling your crate, thus exacerbating compile time even if the enum didn’t change.I’ve been putting off learning Rust macros because they feel too hard. Compare that to Crystal macros for example.
Rust’s declarative macros (”
macro_rules
”) seem pretty similar. They’re a bit more verbose, sure, and a little harder to read maybe. But fundamentally similar in that they’re declarative.Procedural macros, where you write arbitrary code to produce an AST from an AST, are definitely more difficult—but also more powerful.
proc macros are token -> token conversions instead of AST -> AST conversions, it’s one of the reasons you need quote/syn for most macro crates. The rfc explains the reasoning here, the big one being so changes to the AST don’t break procedural macros.
This is interesting, I do agree with the other comments that it feels better as part of the build process and not the tests. But on another note, I’ve never really ran into the need for self-modifying code and I’ve been programming for 20 years so I thought I’d run into nearly everything. Is there a certain problem set that lends itself to self modifying code?
I have a feeling it lends itself well in the context of maybe the problem domain of compilers or other sort of things that are development related, but I can’t think of a use case off the top off of my head.
Edit: I forgot about targeting multiple platforms where it won’t necessarily build and you don’t want a separate code base for each platform, beyond that I cannot think of anything that probably wouldn’t be more readable or elegant using another design pattern.