bazel

How can I generate a file with the time of bazel build and include it in build output?


I want the output binary to know the details of the build process: time of the build and build target. How can I add an additional step to bazel build process to generate a file with this info and attach this file to build output?


Solution

  • Bazel calls this "build stamping". See https://bazel.build/docs/user-manual#workspace-status.

    The build timestamp is part of the "volatile status". As an example:

    main.c:

    #include <stdio.h>
    #include "build_info.h"
    
    int main() {
      printf("build timestamp is %d\n", BUILD_TIMESTAMP);
      return 0;
    }
    

    BUILD:

    cc_binary(
      name = "main",
      srcs = [
        "main.c",
        ":build_info.h",
      ],
    )
    
    genrule(
      name = "gen_build_info",
      outs = ["build_info.h"],
      stamp = True,
      cmd = """
    echo stable status:
    cat bazel-out/stable-status.txt
    echo volatile status:
    cat bazel-out/volatile-status.txt
    
    BUILD_TIMESTAMP="$$(cat bazel-out/volatile-status.txt | grep BUILD_TIMESTAMP)"
    
    echo "#define $$BUILD_TIMESTAMP" > $@
    """,
    )
    

    usage:

    $ bazel run main
    Starting local Bazel server and connecting to it...
    INFO: Analyzed target //:main (87 packages loaded, 404 targets configured).
    INFO: From Executing genrule //:gen_build_info:
    stable status:
    BUILD_EMBED_LABEL 
    BUILD_HOST ...
    BUILD_USER ...
    volatile status:
    BUILD_TIMESTAMP 1727191789
    FORMATTED_DATE 2024 Sep 24 15 29 49 Tue
    INFO: Found 1 target...
    Target //:main up-to-date:
      bazel-bin/main
    INFO: Elapsed time: 5.287s, Critical Path: 0.25s
    INFO: 8 processes: 5 internal, 3 linux-sandbox.
    INFO: Build completed successfully, 8 total actions
    INFO: Running command line: bazel-bin/main
    build timestamp is 1727191789
    

    Embedding the label is trickier.

    As a special case, the deploy jar of java_binary embeds the label in a build-data.properties file at the root of the jar:

    Main.java:

    package main;
    
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.IOException;
    import java.nio.file.Files;
    
    public class Main {
      public static void main(String[] args) throws IOException {
        BufferedReader buf = new BufferedReader(
            new InputStreamReader(Main.class.getResourceAsStream("/build-data.properties")));
        buf.lines().forEach(System.out::println);
      }
    }
    

    BUILD:

    java_binary(
      name = "java_main",
      srcs = ["Main.java"],
      main_class = "main.Main",
    )
    

    usage:

    $ bazel run java_main_deploy.jar
    INFO: Analyzed target //:java_main_deploy.jar (9 packages loaded, 1158 targets configured).
    INFO: From Building java_main.jar (1 source file):
    INFO: Found 1 target...
    Target //:java_main_deploy.jar up-to-date:
      bazel-bin/java_main_deploy.jar
    INFO: Elapsed time: 7.111s, Critical Path: 3.65s
    INFO: 6 processes: 2 internal, 3 linux-sandbox, 1 worker.
    INFO: Build completed successfully, 6 total actions
    build.target=@@//:java_main
    main.class=main.Main
    build.time=Thu Jan 01 00\:00\:00 1970 (0)
    build.timestamp=Thu Jan 01 00\:00\:00 1970 (0)
    build.timestamp.as.int=0
    

    Note specifically that the target being built is java_main_deploy.jar (an additional output of java_binary), the regular java_main target is not built as a deploy jar (rolling up itself and all its dependencies in one jar) and won't have the build-data.properties file.

    For other rulesets, other approaches might be necessary. There's --embed_label bazel flag which will put the text of that flag as the value of BUILD_EMBED_LABEL in the stable status file, which could be read from a build data genrule as above or other rule. You could pass the target on the command line that way, but you would need to be sure that the correct value is passed to the flag and to bazel as the target to build (handling cases like relative labels).

    Another approach I could imagine is a macro that creates the top-level target along with the build data generating rule, in order to get the name of the top-level target. Something like:

    def cc_binary_with_build_data(name, srcs, **args):
      build_info_header_name = name + "_build_info.h"
      native.cc_binary(
        name = name,
        srcs = srcs + [build_info_header_name],
        **args,
      )
    
      native.genrule(
        name = name + "_gen_build_info",
        outs = [build_info_header_name],
        stamp = True,
        cmd = """
    echo stable status:
    cat bazel-out/stable-status.txt
    echo volatile status:
    cat bazel-out/volatile-status.txt
    
    BUILD_TIMESTAMP="$$(cat bazel-out/volatile-status.txt | grep BUILD_TIMESTAMP)"
    
    cat > $@ <<EOF
    #define $$BUILD_TIMESTAMP
    #define BUILD_LABEL "%s"
    EOF
    """ % ("//" + native.package_name() + ":" + name),
    )
    

    Finally, note also that depending on the situation and use case, it's not enough to have just the target / label to reproduce a binary. There's the repo commit it was built at (the link above has an example for git), and there's the build flags passed to bazel, any environment variables that rulesets might depend on (generally discouraged, but it exists), etc.