2

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.

Matt Passell
  • 4,549
  • 3
  • 24
  • 39

1 Answers1

5

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
"""
Brent Douglas
  • 383
  • 3
  • 9
  • Although this is still roughly what's in my build files, if the command line was instead built up using [an Args object](https://docs.bazel.build/versions/master/skylark/lib/Args.html), you could probably use a param file to get around the command-line length restrictions. – Matt Passell Jul 16 '19 at 19:58
  • Is there a way to write this to the console instead of a file? – austin_ce Aug 21 '19 at 16:39
  • 1
    @austince If you add `--test_output=errors` after the target it will print the output of the failing ones. If you add `--test_output=all` it will print output from all of them. https://docs.bazel.build/versions/master/command-line-reference.html#flag--test_output – Brent Douglas Aug 28 '19 at 13:48