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.
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.
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
"""