Search code examples
javacheckstylebazel

What is the best way to invoke Checkstyle from within Bazel?


I'm trying to add support for invoking Checkstyle as part of our Bazel build. I've seen some code using Extra Actions to accomplish that, but I'm hoping to avoid that approach and get it to work with pure Skylark code. I managed to use the following (awful) genrule to get the JVM to execute Checkstyle on a set of source files, but I realize that's incredibly hacky:

native.genrule(
    name = name,
    srcs = srcs,
    outs = ["src_output.txt"],
    cmd = "$(JAVA) -Dconfig_loc=<full-config-loc-path> -classpath <path>/checkstyle-8.4-all.jar com.puppycrawl.tools.checkstyle.Main -c <config-file-path> -o $@ $(SRCS)",
    **kwargs
)

Any suggestions on how to do it the right way? I already have all the necessarily JAR dependencies in our dependencies.bzl file, so I'd be happy to refer to those instead of the checkstyle-all JAR.


Solution

  • As discussed on IRC this is the rule that I have been using (at the end of this post). I have a directory config/ that contains my checkstyle config, suppressions and a license file which are referenced here aa default arguments. In your WORKSPACE you can pull in all the deps with a macro:

    load("//tools:checkstyle.bzl", "checkstyle_repositories")
    checkstyle_repositories()
    

    In your build files import and use the rule with:

    load("//tools:checkstyle.bzl", "checkstyle_test")
    
    filegroup(
        name = "java-srcs",
        srcs = glob(["src/main/java/**/*.java"]),
    )
    
    checkstyle_test(
        name = "check",
        srcs = [
            ":java-srcs",
        ],
    ) 
    

    Then you can run it with bazel test //path/to/dir:check.

    This rule does have the limitation that it takes the arguments on the command line so for larger modules you will need the split up the file groups to stop hitting the command line length limit, e.g.

    load("//tools:checkstyle.bzl", "checkstyle_test")
    
    filegroup(
        name = "java-foo-srcs",
        srcs = glob(["src/main/java/foo/**/*.java"]),
    )
    filegroup(
        name = "java-bar-srcs",
        srcs = glob(["src/main/java/bar/**/*.java"]),
    )
    
    checkstyle_test(
        name = "check-foo",
        srcs = [
            ":java-foo-srcs",
        ],
    ) 
    checkstyle_test(
        name = "check-bar",
        srcs = [
            ":java-bar-srcs",
        ],
    ) 
    test_suite(
        name = "check",
        tests = [
            ":check-bar",
            ":check-foo",
        ],
    )
    

    If you have a BUILD file per package this will likely be unnecessary though, its more an issue if you are converting a large maven module and keep a similar structure in your bazel build files.

    tools/checkstyle.bzl

    load("//tools/gerrit:maven_jar.bzl", "maven_jar")
    
    def checkstyle_repositories(
        omit = [],
        versions = {
          "antlr_antlr": "2.7.7",
          "org_antlr_antlr4_runtime": "4.5.1-1",
          "com_puppycrawl_tools_checkstyle": "8.2",
          "commons_beanutils_commons_beanutils": "1.9.3",
          "commons_cli_commons_cli": "1.4",
          "commons_collections_commons_collections": "3.2.2",
          "com_google_guava_guava23": "23.0",
          "org_slf4j_slf4j_api": "1.7.7",
          "org_slf4j_slf4j_jcl": "1.7.7",
        }
    ):
      if not "antlr_antlr" in omit:
        maven_jar(
            name = "antlr_antlr",
            attach_source = False,
            artifact = "antlr:antlr:" + versions["antlr_antlr"],
        )
      if not "org_antlr_antlr4_runtime" in omit:
        maven_jar(
            name = "org_antlr_antlr4_runtime",
            artifact = "org.antlr:antlr4-runtime:" + versions["org_antlr_antlr4_runtime"],
        )
      if not "com_puppycrawl_tools_checkstyle" in omit:
        maven_jar(
            name = "com_puppycrawl_tools_checkstyle",
            artifact = "com.puppycrawl.tools:checkstyle:" + versions["com_puppycrawl_tools_checkstyle"],
        )
      if not "commons_beanutils_commons_beanutils" in omit:
        maven_jar(
            name = "commons_beanutils_commons_beanutils",
            artifact = "commons-beanutils:commons-beanutils:" + versions["commons_beanutils_commons_beanutils"],
        )
      if not "commons_cli_commons_cli" in omit:
        maven_jar(
            name = "commons_cli_commons_cli",
            artifact = "commons-cli:commons-cli:" + versions["commons_cli_commons_cli"],
        )
      if not "commons_collections_commons_collections" in omit:
        maven_jar(
            name = "commons_collections_commons_collections",
            artifact = "commons-collections:commons-collections:" + versions["commons_collections_commons_collections"],
        )
      if not "com_google_guava_guava23" in omit:
        maven_jar(
            name = "com_google_guava_guava23",
            artifact = "com.google.guava:guava:" + versions["com_google_guava_guava23"],
        )
      if not "org_slf4j_slf4j_api" in omit:
        maven_jar(
            name = "org_slf4j_slf4j_api",
            artifact = "org.slf4j:slf4j-api:" + versions["org_slf4j_slf4j_api"],
        )
      if not "org_slf4j_slf4j_jcl" in omit:
        maven_jar(
            name = "org_slf4j_slf4j_jcl",
            artifact = "org.slf4j:jcl-over-slf4j:" + versions["org_slf4j_slf4j_jcl"],
        )
    
    
    
    def _checkstyle_test_impl(ctx):
        name = ctx.label.name
        srcs = ctx.files.srcs
        deps = ctx.files.deps
        config = ctx.file.config
        properties = ctx.file.properties
        suppressions = ctx.file.suppressions
        opts = ctx.attr.opts
        sopts = ctx.attr.string_opts
    
        classpath=""
        add=False
        for file in ctx.files._classpath:
            if add:
                classpath += ":"
            add=True
            classpath += file.path
        for file in ctx.files.deps:
            classpath += ":" + file.path
    
        args = ""
        inputs = []
        if config:
          args += " -c %s" % config.path
          inputs.append(config)
        if properties:
          args += " -p %s" % properties.path
          inputs.append(properties)
        if suppressions:
          inputs.append(suppressions)
    
        cmd = " ".join(
            ["java -cp %s com.puppycrawl.tools.checkstyle.Main" % classpath] +
            [args] +
            ["--%s" % x for x in opts] +
            ["--%s %s" % (k, sopts[k]) for k in sopts] +
            [x.path for x in srcs]
        )
    
        ctx.file_action(
            output = ctx.outputs.executable,
            content = cmd,
            executable = True,
        )
        files = [ctx.outputs.executable, ctx.file.license] + srcs + deps + ctx.files._classpath + inputs
        runfiles = ctx.runfiles(
            files = files,
            collect_data = True
        )
        return struct(
            files = depset(files),
            runfiles = runfiles,
        )
    
    checkstyle_test = rule(
        implementation = _checkstyle_test_impl,
        test = True,
        attrs = {
            "_classpath": attr.label_list(default=[
                Label("@com_puppycrawl_tools_checkstyle//jar"),
                Label("@commons_beanutils_commons_beanutils//jar"),
                Label("@commons_cli_commons_cli//jar"),
                Label("@commons_collections_commons_collections//jar"),
                Label("@org_slf4j_slf4j_api//jar"),
                Label("@org_slf4j_slf4j_jcl//jar"),
                Label("@antlr_antlr//jar"),
                Label("@org_antlr_antlr4_runtime//jar"),
                Label("@com_google_guava_guava//jar"),
            ]),
            "config": attr.label(allow_single_file=True, default = "//config:checkstyle"),
            "suppressions": attr.label(allow_single_file=True, default = "//config:suppressions"),
            "license": attr.label(allow_single_file=True, default = "//config:license"),
            "properties": attr.label(allow_single_file=True),
            "opts": attr.string_list(),
            "string_opts": attr.string_dict(),
            "srcs": attr.label_list(allow_files = True),
            "deps": attr.label_list(),
        },
    )
    """Run checkstyle
    
    Args:
      config: A checkstyle configuration file
      suppressions: A checkstyle suppressions file
      license: A license file that can be used with the checkstyle license
        target
      properties: A properties file to be used
      opts: Options to be passed on the command line that have no
        argument
      string_opts: Options to be passed on the command line that have an
        argument
      srcs: The files to check
    """