Can I invoke a build script without triggering full build?

Hello,
I'd like to be able to only invoke a build script without triggering full build, for GitLab CI purposes. I'd like to call cargo fmt -- --check but that fails because a file (that's generated by calling build.rs) doesn't exist yet and is depended on inside src/lib.rs via a use generated_module; clause. To generate the file we'd have to invoke cargo build first which we don't want to do.

The reason is that we push a Docker image where the project is fully built (after these preliminary CI steps finish). I don't want the GitLab CI to duplicate the compilation process. I want it to do a few sanity checks -- like checking formatting -- and then leave the actual cargo build step to a remote Docker container.

The team is open to rework their .gitlab-ci.yml file if we can insert the checking formatting step before that (currently it's not there and we'd like to have it).

Do you have any advice? Thanks for reading.

cargo check will compile and run the build script without doing a full build, though it does invoke the compiler to perform type checking and such — basically everything except codegen and linking.

I don't know of any supported way to tell cargo to run the build script without doing at least as much work as cargo check.

2 Likes

It seems there's no other way. Thank you.

There are indeed no built-in ways to do that, except through some hacks, such as those featured in this post:

https://stackoverflow.com/a/53728256

The issue then being that "running" —however may it be done— will trigger a cargo check on that very package / Cargo.toml-identified-dir. Which leads to simplifying the process to @mbrubeck's solution of directly running cargo check.

Sadly, this will pull & check all the deps and whatnot that cargo check implies, so for a quick run of a build.rs script this is less than ideal.

And yet the above hack can be improved to avoid this, by defining a new ad-hoc package:

  • Add a build_script/Cargo.toml file as follows:

      .
      ├── Cargo.toml
      ├── build.rs
    + ├── build_script
    + │   └── Cargo.toml
      └── src
          â””... 
    
  • And with the following (dummy) contents:

    [[bin]]
    name = "build_script"
    path = "../build.rs"
    
    [package]
    name = "build_script"
    version = "0.0.0"
    
  • (Optional) Feel free to add build_script to the [workspace].

This way, (cd build_script && cargo r) Just Works1.

  • Or cargo r --manifest-path build_script/Cargo.toml,

    for which you can define a cargo alias:

    # .cargo/config
    [alias]
    build-rs = ["run", "--manifest-path", "build_script/Cargo.toml"]
    

    So that cargo build-rs Just Works.

1 special env vars such as OUT_DIR won't be present, though

5 Likes

That actually worked. Every newbie says this but I can't believe I didn't study Cargo workspaces in more detail, the answer was staring me right in the face and you helped me utilize it. Thank you!

Last question: is there a way to make the main app's compilation step be dependent on this sub-app's binary, sort of like you can link Makefile targets? To clarify, I don't want to have to run cargo build-rs && cargo build every time. Maybe another cargo alias?

Well, with the hacky configuration I have suggested, build.rs belongs to both crates, in one as a main binary and in the other as the usual automatically-triggered build-script, so cargo build on your main folder should keep triggering that build.rs invocation as usual.


A (potentially) less hacky setting would be for the build.rs to become an actual / a proper src/main.rs file in the sub-directory,
and then have a build.rs script that calls cargo run in the sub-dir:

use ::std::ops::Not;

fn main ()
{
    let mut cargo_cmd =
        ::std::process::Command::new(::std::env::var("CARGO").unwrap())
    ;
    let status =
        cargo_cmd
            .args(&[
                "run", "--manifest-path", "build_script/Cargo.toml",
            ])
            .status()
            .unwrap_or_else(|err| panic!(
                "Command {:?} failed: {}", cargo_cmd, err,
            ))
    ;
    if status.success().not() {
        panic!("Command {:?} failed", cargo_cmd)
    }
}
1 Like

I hear you. I opted for making a small sub-app that has both a binary and a library -- where the binary uses the library function (that generates a file in src/) -- and then redid the parent project's build.rs to reuse the same library function.

I also skipped the [workspace] part and that turned out to be crucial: by not setting up a workspace I was able to have the sub-app have its own target/ directory and thus only fetch and compile a very minimal subset of deps before calling cargo fmt -- --check (this happens in CI and the sub-app's binary is invoked immediately before the format check; we don't use CI for building, we have a separate repo action that builds the project through Docker).

And then I made very slight changes in the Dockerfile and it all worked.

Thanks for throwing me in this rabbit hole. I emerged much better informed and experienced thanks to you. :bowing_man:

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.