diff --git a/.bazelrc b/.bazelrc index e8047d9..d1f17d7 100644 --- a/.bazelrc +++ b/.bazelrc @@ -7,7 +7,11 @@ common --enable_bzlmod try-import user.bazelrc -# To update these lines, execute +# To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` build --deleted_packages=examples/check_glob,examples/optional_attributes query --deleted_packages=examples/check_glob,examples/optional_attributes + +# Enable the aspect +build --aspects=//shellcheck:shellcheck_aspect.bzl%shellcheck_aspect +build --output_groups=+shellcheck_checks diff --git a/.bazelversion b/.bazelversion index 66ce77b..ae9a76b 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.0.0 +8.0.0 diff --git a/BUILD.bazel b/BUILD.bazel index 1849b99..afe829e 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -7,9 +7,7 @@ filegroup( "MODULE.bazel", "README.md", "WORKSPACE", - ":def.bzl", - ":deps.bzl", - "//internal:distribution", + "//shellcheck:distribution", # Needed for BCR registry to run the pre-submit tests "//examples:distribution", ], diff --git a/MODULE.bazel b/MODULE.bazel index 5351ba6..3c5b3d6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -4,11 +4,13 @@ module( compatibility_level = 1, ) -bazel_dep(name = "platforms", version = "0.0.8") +bazel_dep(name = "bazel_skylib", version = "1.8.2") +bazel_dep(name = "platforms", version = "1.0.0") +bazel_dep(name = "rules_shell", version = "0.6.1") -deps = use_extension("//internal:extensions.bzl", "shellcheck_dependencies") +shellcheck = use_extension("//shellcheck/internal:extensions.bzl", "shellcheck") use_repo( - deps, + shellcheck, "shellcheck_darwin_aarch64", "shellcheck_darwin_x86_64", "shellcheck_linux_aarch64", diff --git a/def.bzl b/def.bzl index 20fdeec..c55d407 100644 --- a/def.bzl +++ b/def.bzl @@ -1,19 +1,9 @@ """This file provides all user facing functions. """ -load("//internal:rules.bzl", _shellcheck_test = "shellcheck_test") +load( + "//shellcheck:defs.bzl", + _shellcheck_test = "shellcheck_test", +) -def shellcheck_test(name, data, **kwargs): - """shellcheck_test takes the files to be checked as 'data' - - Args: - name: The name of the rule. - data: The list of files to be checked using shellcheck. - **kwargs: Forwarded kwargs to the underlying rule. - """ - kwargs.pop("expect_fail", True) - return _shellcheck_test( - name = name, - data = data, - **kwargs - ) +shellcheck_test = _shellcheck_test diff --git a/deps.bzl b/deps.bzl index 0c1ad53..a5ca583 100644 --- a/deps.bzl +++ b/deps.bzl @@ -3,57 +3,6 @@ - OSX 64-bit """ -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") +load("//shellcheck/internal:extensions.bzl", _shellcheck_dependencies = "shellcheck_dependencies") -def _urls(arch, version): - archive_template_name = { - "darwin_aarch64": "shellcheck-{version}.{arch}.tar.xz", - "darwin_x86_64": "shellcheck-{version}.{arch}.tar.xz", - "linux_aarch64": "shellcheck-{version}.{arch}.tar.xz", - "linux_armv6hf": "shellcheck-{version}.{arch}.tar.xz", - "linux_x86_64": "shellcheck-{version}.{arch}.tar.xz", - "windows_x86_64": "shellcheck-{version}.zip", - } - url = "https://github.com/koalaman/shellcheck/releases/download/{version}/{archive}".format( - version = version, - archive = archive_template_name[arch].format( - version = version, - arch = arch.replace("_", ".", 1), - ) - ) - - return [ - url, - ] - -def shellcheck_dependencies(): - version = "v0.11.0" - sha256 = { - "darwin_aarch64": "56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79", - "darwin_x86_64": "3c89db4edcab7cf1c27bff178882e0f6f27f7afdf54e859fa041fca10febe4c6", - "linux_aarch64": "12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588", - "linux_armv6hf": "8afc50b302d5feeac9381ea114d563f0150d061520042b254d6eb715797c8223", - "linux_x86_64": "8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198", - } - - for arch, sha256 in sha256.items(): - maybe( - http_archive, - name = "shellcheck_{arch}".format(arch = arch), - strip_prefix = "shellcheck-{version}".format(version = version), - build_file_content = """exports_files(["shellcheck"]) -""", - sha256 = sha256, - urls = _urls(arch = arch, version = version), - ) - - # Special case, as it is a zip archive with no prefix to strip. - maybe( - http_archive, - name = "shellcheck_windows_x86_64", - build_file_content = """exports_files(["shellcheck"]) -""", - sha256 = "8a4e35ab0b331c85d73567b12f2a444df187f483e5079ceffa6bda1faa2e740e", - urls = _urls(arch = "windows_x86_64", version = version), - ) +shellcheck_dependencies = _shellcheck_dependencies diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel index a011dca..e69de29 100644 --- a/internal/BUILD.bazel +++ b/internal/BUILD.bazel @@ -1,5 +0,0 @@ -filegroup( - name = "distribution", - srcs = glob(["*"]), - visibility = ["//:__pkg__"], -) diff --git a/internal/extensions.bzl b/internal/extensions.bzl deleted file mode 100644 index 9c1358b..0000000 --- a/internal/extensions.bzl +++ /dev/null @@ -1,13 +0,0 @@ -"""Provides shellcheck dependencies on all supported platforms: -- Linux 64-bit and ARM64 -- OSX 64-bit -""" - -load("@rules_shellcheck//:deps.bzl", _deps = "shellcheck_dependencies") - -def _impl(_): - _deps() - -shellcheck_dependencies = module_extension( - implementation = _impl, -) diff --git a/internal/pkg/BUILD.bazel b/internal/pkg/BUILD.bazel index d474bb4..4fc6250 100644 --- a/internal/pkg/BUILD.bazel +++ b/internal/pkg/BUILD.bazel @@ -1,5 +1,6 @@ load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix") load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("//:def.bzl", "shellcheck_test") pkg_files( diff --git a/internal/rules.bzl b/internal/rules.bzl deleted file mode 100644 index 59b7791..0000000 --- a/internal/rules.bzl +++ /dev/null @@ -1,56 +0,0 @@ -"""This file provides all user facing functions. -""" - -def _impl_test(ctx): - cmd = [ctx.file._shellcheck.short_path] - if ctx.attr.format: - cmd.append("--format={}".format(ctx.attr.format)) - if ctx.attr.severity: - cmd.append("--severity={}".format(ctx.attr.severity)) - cmd += [f.short_path for f in ctx.files.data] - cmd = " ".join(cmd) - - if ctx.attr.expect_fail: - script = "{cmd} || exit 0\nexit1".format(cmd = cmd) - else: - script = "exec {cmd}".format(cmd = cmd) - - ctx.actions.write( - output = ctx.outputs.executable, - content = script, - ) - - return [ - DefaultInfo( - executable = ctx.outputs.executable, - runfiles = ctx.runfiles(files = [ctx.file._shellcheck] + ctx.files.data), - ), - ] - -shellcheck_test = rule( - implementation = _impl_test, - attrs = { - "data": attr.label_list( - allow_files = True, - ), - "expect_fail": attr.bool( - default = False, - ), - "format": attr.string( - values = ["checkstyle", "diff", "gcc", "json", "json1", "quiet", "tty"], - doc = "The format of the outputted lint results.", - ), - "severity": attr.string( - values = ["error", "info", "style", "warning"], - doc = "The severity of the lint results.", - ), - "_shellcheck": attr.label( - default = Label("//:shellcheck"), - allow_single_file = True, - cfg = "exec", - executable = True, - doc = "The shellcheck executable to use.", - ), - }, - test = True, -) diff --git a/shellcheck/BUILD.bazel b/shellcheck/BUILD.bazel new file mode 100644 index 0000000..bbbb976 --- /dev/null +++ b/shellcheck/BUILD.bazel @@ -0,0 +1,12 @@ +filegroup( + name = "distribution", + srcs = [ + "BUILD.bazel", + "defs.bzl", + "shellcheck_aspect.bzl", + "shellcheck_test.bzl", + "//shellcheck/internal:distribution", + "//shellcheck/settings:distribution", + ], + visibility = ["//:__pkg__"], +) diff --git a/shellcheck/defs.bzl b/shellcheck/defs.bzl new file mode 100644 index 0000000..4c81afb --- /dev/null +++ b/shellcheck/defs.bzl @@ -0,0 +1,13 @@ +"""# Shellcheck rules""" + +load( + ":shellcheck_aspect.bzl", + _shellcheck_aspect = "shellcheck_aspect", +) +load( + ":shellcheck_test.bzl", + _shellcheck_test = "shellcheck_test", +) + +shellcheck_test = _shellcheck_test +shellcheck_aspect = _shellcheck_aspect diff --git a/shellcheck/internal/BUILD.bazel b/shellcheck/internal/BUILD.bazel new file mode 100644 index 0000000..36916e3 --- /dev/null +++ b/shellcheck/internal/BUILD.bazel @@ -0,0 +1,5 @@ +filegroup( + name = "distribution", + srcs = glob(["*"]), + visibility = ["//shellcheck:__pkg__"], +) diff --git a/shellcheck/internal/extensions.bzl b/shellcheck/internal/extensions.bzl new file mode 100644 index 0000000..b625ca7 --- /dev/null +++ b/shellcheck/internal/extensions.bzl @@ -0,0 +1,67 @@ +"""Provides shellcheck dependencies on all supported platforms: +- Linux 64-bit and ARM64 +- OSX 64-bit +""" + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +def _urls(arch, version): + archive_template_name = { + "darwin_aarch64": "shellcheck-{version}.{arch}.tar.xz", + "darwin_x86_64": "shellcheck-{version}.{arch}.tar.xz", + "linux_aarch64": "shellcheck-{version}.{arch}.tar.xz", + "linux_armv6hf": "shellcheck-{version}.{arch}.tar.xz", + "linux_x86_64": "shellcheck-{version}.{arch}.tar.xz", + "windows_x86_64": "shellcheck-{version}.zip", + } + url = "https://github.com/koalaman/shellcheck/releases/download/{version}/{archive}".format( + version = version, + archive = archive_template_name[arch].format( + version = version, + arch = arch.replace("_", ".", 1), + ), + ) + + return [ + url, + ] + +def shellcheck_dependencies(): + """Define shellcheck repositories""" + version = "v0.11.0" + sha256 = { + "darwin_aarch64": "56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79", + "darwin_x86_64": "3c89db4edcab7cf1c27bff178882e0f6f27f7afdf54e859fa041fca10febe4c6", + "linux_aarch64": "12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588", + "linux_armv6hf": "8afc50b302d5feeac9381ea114d563f0150d061520042b254d6eb715797c8223", + "linux_x86_64": "8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198", + } + + for arch, sha256 in sha256.items(): + maybe( + http_archive, + name = "shellcheck_{arch}".format(arch = arch), + strip_prefix = "shellcheck-{version}".format(version = version), + build_file_content = """exports_files(["shellcheck"]) +""", + sha256 = sha256, + urls = _urls(arch = arch, version = version), + ) + + # Special case, as it is a zip archive with no prefix to strip. + maybe( + http_archive, + name = "shellcheck_windows_x86_64", + build_file_content = """exports_files(["shellcheck"]) +""", + sha256 = "8a4e35ab0b331c85d73567b12f2a444df187f483e5079ceffa6bda1faa2e740e", + urls = _urls(arch = "windows_x86_64", version = version), + ) + +def _impl(_): + shellcheck_dependencies() + +shellcheck = module_extension( + implementation = _impl, +) diff --git a/shellcheck/internal/rules.bzl b/shellcheck/internal/rules.bzl new file mode 100644 index 0000000..fac6c43 --- /dev/null +++ b/shellcheck/internal/rules.bzl @@ -0,0 +1,193 @@ +"""This file provides all user facing functions. +""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") + +def shellcheck_test_impl(ctx, expect_fail = False): + """The implementation of the `shellcheck_test` rule. + + Args: + ctx (ctx): The rule's context object. + expect_fail (bool, optional): Whether or not shellcheck is expected to fail. + + Returns: + list: All providers. + """ + cmd = [ctx.file._shellcheck.short_path] + if ctx.attr.format: + cmd.append("--format={}".format(ctx.attr.format)) + if ctx.attr.severity: + cmd.append("--severity={}".format(ctx.attr.severity)) + cmd += [f.short_path for f in ctx.files.data] + cmd = " ".join(cmd) + + if expect_fail: + script = "{cmd} || exit 0\nexit1".format(cmd = cmd) + else: + script = "exec {cmd}".format(cmd = cmd) + + ctx.actions.write( + output = ctx.outputs.executable, + content = script, + is_executable = True, + ) + + return [ + DefaultInfo( + executable = ctx.outputs.executable, + runfiles = ctx.runfiles(files = [ctx.file._shellcheck] + ctx.files.data), + ), + ] + +ATTRS = { + "data": attr.label_list( + allow_files = True, + ), + "expect_fail": attr.bool( + default = False, + ), + "format": attr.string( + values = ["checkstyle", "diff", "gcc", "json", "json1", "quiet", "tty"], + doc = "The format of the outputted lint results.", + ), + "severity": attr.string( + values = ["error", "info", "style", "warning"], + doc = "The severity of the lint results.", + ), + "_shellcheck": attr.label( + default = Label("//:shellcheck"), + allow_single_file = True, + cfg = "exec", + executable = True, + doc = "The shellcheck executable to use.", + ), +} + +shellcheck_test = rule( + implementation = shellcheck_test_impl, + attrs = ATTRS, + test = True, +) + +_SHELL_CONTENT = """\ +#!/bin/sh + +echo '' > '{output}' +exec '{shellcheck}' $@ +""" + +_BATCH_CONTENT = """\ +@ECHO OFF + +echo "" > {output} +{shellcheck} %* +""" + +def _shellcheck_aspect_impl(target, ctx): + if target.label.workspace_root.startswith("external"): + return [] + + ignore_tags = [ + "no_shellcheck", + "no_lint", + "nolint", + "noshellcheck", + ] + for tag in ctx.rule.attr.tags: + if tag.replace("-", "_").lower() in ignore_tags: + return [] + + # TODO: https://github.com/aignas/rules_shellcheck/issues/23 + rule_name = ctx.rule.kind + if rule_name not in ["sh_binary", "sh_test", "sh_library"]: + return [] + + srcs = [ + src + for src in getattr(ctx.rule.files, "srcs", []) + if src.is_source + ] + + if not srcs: + return [] + + inputs_direct = getattr(ctx.rule.files, "srcs", []) + getattr(ctx.rule.files, "data", []) + inputs_transitive = [] + + if DefaultInfo in target: + inputs_transitive.extend([ + target[DefaultInfo].files, + target[DefaultInfo].default_runfiles.files, + ]) + + format = ctx.attr._format[BuildSettingInfo].value + severity = ctx.attr._format[BuildSettingInfo].value + + shellcheck = ctx.executable._shellcheck + is_windows = True if shellcheck.basename.endswith(".exe") else False + + executable = ctx.actions.declare_file("{}.shellcheck.{}".format(target.label.name, "bat" if is_windows else "sh")) + output = ctx.actions.declare_file("{}.shellcheck.ok".format(target.label.name)) + + ctx.actions.write( + output = executable, + content = (_BATCH_CONTENT if is_windows else _SHELL_CONTENT).format( + output = output.path, + shellcheck = ctx.executable._shellcheck.path, + ), + is_executable = True, + ) + + tools = depset([ctx.executable._shellcheck]) + if DefaultInfo in ctx.attr._shellcheck: + tools = depset(transitive = [ + tools, + ctx.attr._shellcheck[DefaultInfo].files, + ctx.attr._shellcheck[DefaultInfo].default_runfiles.files, + ]) + + args = ctx.actions.args() + if format: + args.add(format, format = "--format=%s") + + if severity: + args.add(severity, format = "--severity=%s") + + args.add_all(srcs) + + ctx.actions.run( + mnemonic = "Shellcheck", + progress_message = "Shellcheck {}".format(target.label), + executable = executable, + inputs = depset(inputs_direct, transitive = inputs_transitive), + arguments = [args], + env = ctx.configuration.default_shell_env, + tools = tools, + outputs = [output], + ) + + return [ + OutputGroupInfo( + shellcheck_checks = depset([output]), + ), + ] + +shellcheck_aspect = aspect( + doc = "An aspect for performing shellcheck checks on `rules_shell` rules.", + implementation = _shellcheck_aspect_impl, + attrs = { + "_format": attr.label( + default = Label("//shellcheck/settings:format"), + ), + "_severity": attr.label( + default = Label("//shellcheck/settings:severity"), + ), + "_shellcheck": attr.label( + default = Label("//:shellcheck"), + allow_single_file = True, + cfg = "exec", + executable = True, + doc = "The shellcheck executable to use.", + ), + }, +) diff --git a/shellcheck/settings/BUILD.bazel b/shellcheck/settings/BUILD.bazel new file mode 100644 index 0000000..d16a346 --- /dev/null +++ b/shellcheck/settings/BUILD.bazel @@ -0,0 +1,36 @@ +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") + +filegroup( + name = "distribution", + srcs = glob(["*"]), + visibility = ["//shellcheck:__pkg__"], +) + +string_flag( + name = "format", + build_setting_default = "", + values = [ + "", + "checkstyle", + "diff", + "gcc", + "json", + "json1", + "quiet", + "tty", + ], + visibility = ["//visibility:public"], +) + +string_flag( + name = "severity", + build_setting_default = "", + values = [ + "", + "error", + "info", + "style", + "warning", + ], + visibility = ["//visibility:public"], +) diff --git a/shellcheck/shellcheck_aspect.bzl b/shellcheck/shellcheck_aspect.bzl new file mode 100644 index 0000000..d5cd13c --- /dev/null +++ b/shellcheck/shellcheck_aspect.bzl @@ -0,0 +1,8 @@ +"""shellcheck_aspect""" + +load( + "//shellcheck/internal:rules.bzl", + _shellcheck_aspect = "shellcheck_aspect", +) + +shellcheck_aspect = _shellcheck_aspect diff --git a/shellcheck/shellcheck_test.bzl b/shellcheck/shellcheck_test.bzl new file mode 100644 index 0000000..7e5e809 --- /dev/null +++ b/shellcheck/shellcheck_test.bzl @@ -0,0 +1,8 @@ +"""shellcheck_test""" + +load( + "//shellcheck/internal:rules.bzl", + _shellcheck_test = "shellcheck_test", +) + +shellcheck_test = _shellcheck_test diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index b12392a..8ff45f3 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -1,4 +1,5 @@ -load("//internal:rules.bzl", "shellcheck_test") +load(":test.bzl", shellcheck_test = "shellcheck_internal_test") +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") shellcheck_test( name = "fail_test", @@ -14,3 +15,14 @@ shellcheck_test( "testdata/good.sh", ], ) + +sh_binary( + name = "bad", + srcs = ["testdata/bad.sh"], + tags = ["manual"], +) + +sh_binary( + name = "good", + srcs = ["testdata/good.sh"], +) diff --git a/tests/test.bzl b/tests/test.bzl new file mode 100644 index 0000000..7a0eecd --- /dev/null +++ b/tests/test.bzl @@ -0,0 +1,21 @@ +"""Shellcheck internal test rules""" + +# buildifier: disable=bzl-visibility +load( + "//shellcheck/internal:rules.bzl", + "ATTRS", + "shellcheck_test_impl", +) + +def _shellcheck_internal_test_impl(ctx): + return shellcheck_test_impl(ctx, ctx.attr.expect_fail) + +shellcheck_internal_test = rule( + implementation = _shellcheck_internal_test_impl, + attrs = ATTRS | { + "expect_fail": attr.bool( + default = False, + ), + }, + test = True, +)