diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml new file mode 100644 index 0000000..4ca63d2 --- /dev/null +++ b/.bazelci/presubmit.yml @@ -0,0 +1,56 @@ +--- +buildifier: latest + +matrix: + platform: + - ubuntu2004 + - macos + # TODO: Enable Windows once the private API it allowlists rules_shell. + # - windows + +tasks: + test_module_bzlmod: + name: "Test module (Bazel 7.3.2, Bzlmod)" + working_directory: "tests/bcr" + bazel: 7.3.2 + platform: ${{ platform }} + build_flags: + - "--enable_bzlmod" + - "--noenable_workspace" + build_targets: + - "//..." + test_flags: + - "--enable_bzlmod" + - "--noenable_workspace" + test_targets: + - "//..." + test_module_workspace: + name: "Test module (Bazel 7.3.2, WORKSPACE)" + working_directory: "tests/bcr" + bazel: 7.3.2 + platform: ${{ platform }} + build_flags: + - "--noenable_bzlmod" + - "--enable_workspace" + build_targets: + - "//..." + test_flags: + - "--noenable_bzlmod" + - "--enable_workspace" + test_targets: + - "//..." + test_module_head: + name: "Test module (Bazel@HEAD, Bzlmod)" + working_directory: "tests/bcr" + bazel: last_green + platform: ${{ platform }} + build_flags: + - "--enable_bzlmod" + - "--noenable_workspace" + build_targets: + - "//..." + test_flags: + - "--enable_bzlmod" + - "--noenable_workspace" + test_targets: + - "//..." diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..23758d5 --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +tests/bcr diff --git a/.bcr/metadata.template.json b/.bcr/metadata.template.json new file mode 100644 index 0000000..7de9c0d --- /dev/null +++ b/.bcr/metadata.template.json @@ -0,0 +1,20 @@ +{ + "homepage": "https://github.com/bazelbuild/rules_shell", + "maintainers": [ + { + "email": "pcloudy@google.com", + "github": "meteorcloudy", + "name": "Yun Peng" + }, + { + "email": "fabian@meumertzhe.im", + "github": "fmeum", + "name": "Fabian Meumertzheim" + } + ], + "repository": [ + "github:bazelbuild/rules_shell" + ], + "versions": [], + "yanked_versions": {} +} diff --git a/.bcr/presubmit.yml b/.bcr/presubmit.yml new file mode 100644 index 0000000..fd5f3ba --- /dev/null +++ b/.bcr/presubmit.yml @@ -0,0 +1,17 @@ +bcr_test_module: + module_path: tests/bcr + matrix: + platform: + - centos7 + - debian10 + - ubuntu2004 + - macos + - windows + bazel: [6.x, 7.x] + tasks: + run_test_module: + name: Run test module + platform: ${{ platform }} + bazel: ${{ bazel }} + test_targets: + - "//..." diff --git a/.bcr/source.template.json b/.bcr/source.template.json new file mode 100644 index 0000000..acee4b1 --- /dev/null +++ b/.bcr/source.template.json @@ -0,0 +1,5 @@ +{ + "integrity": "**leave this alone**", + "strip_prefix": "{REPO}-{VERSION}", + "url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/{REPO}-{TAG}.tar.gz" +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b5f94b5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +# Cut a release whenever a new tag is pushed to the repo. +# You should use an annotated tag, like `git tag -a v1.2.3` +# and put the release notes into the commit message for the tag. +name: Release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + +jobs: + release: + uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@v6 + with: + release_files: rules_shell-*.tar.gz diff --git a/.github/workflows/release_prep.sh b/.github/workflows/release_prep.sh new file mode 100755 index 0000000..2b1534a --- /dev/null +++ b/.github/workflows/release_prep.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -o errexit -o nounset -o pipefail + +# Set by GH actions, see +# https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables +TAG=${GITHUB_REF_NAME} +# The prefix is chosen to match what GitHub generates for source archives +# This guarantees that users can easily switch from a released artifact to a source archive +# with minimal differences in their code (e.g. strip_prefix remains the same) +PREFIX="rules_shell-${TAG:1}" +ARCHIVE="rules_shell-$TAG.tar.gz" + +# NB: configuration for 'git archive' is in /.gitattributes +git archive --format=tar --prefix="${PREFIX}/" "${TAG}" | gzip > "$ARCHIVE" +SHA=$(shasum -a 256 "$ARCHIVE" | awk '{print $1}') + +cat << EOF +## Using Bzlmod with Bazel 6 or greater + +1. (Bazel 6 only) Enable with \`common --enable_bzlmod\` in \`.bazelrc\`. +2. Add to your \`MODULE.bazel\` file: + +\`\`\`starlark +bazel_dep(name = "rules_shell", version = "${TAG:1}") +\`\`\` + +## Using WORKSPACE + +Paste this snippet into your \`WORKSPACE.bazel\` file: + +\`\`\`starlark +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +http_archive( + name = "rules_shell", + sha256 = "${SHA}", + strip_prefix = "${PREFIX}", + url = "https://github.com/bazelbuild/rules_shell/releases/download/${TAG}/${ARCHIVE}", +) + +load("@rules_shell//shell:repositories.bzl", "rules_shell_dependencies", "rules_shell_toolchains") +rules_shell_dependencies() +rules_shell_toolchains() +\`\`\` +EOF diff --git a/.gitignore b/.gitignore index 57c461f..0d4fed2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ bazel-* - +MODULE.bazel.lock diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..ab05338 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,11 @@ +module( + name = "rules_shell", + version = "0.0.0", +) + +bazel_dep(name = "platforms", version = "0.0.10") + +sh_configure = use_extension("//shell/private/extensions:sh_configure.bzl", "sh_configure") +use_repo(sh_configure, "local_config_shell") + +register_toolchains("@local_config_shell//:all") diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..e69de29 diff --git a/shell/BUILD b/shell/BUILD new file mode 100644 index 0000000..2e2f8ca --- /dev/null +++ b/shell/BUILD @@ -0,0 +1,15 @@ +# A runtime toolchain for shell scripts. +# +# Use `sh_toolchain` to register a toolchain for this type. +# +# Every toolchain registered for this type has the following attributes: +# - `path`: The path to the shell interpreter for the target platform. +# +# Other attribute may be present but are considered implementation details of +# Bazel's sh_* rules. +# +# Toolchains registered for this type should have target constraints. +toolchain_type( + name = "toolchain_type", + visibility = ["//visibility:public"], +) diff --git a/shell/private/BUILD b/shell/private/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/shell/private/extensions/BUILD b/shell/private/extensions/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/shell/private/extensions/sh_configure.bzl b/shell/private/extensions/sh_configure.bzl new file mode 100644 index 0000000..71ff021 --- /dev/null +++ b/shell/private/extensions/sh_configure.bzl @@ -0,0 +1,23 @@ +# Copyright 2018 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The sh_configure module extension.""" + +load("//shell/private/repositories:sh_config.bzl", "sh_config") + +def _sh_configure_impl(module_ctx): + sh_config(name = "local_config_shell") + return module_ctx.extension_metadata(reproducible = True) + +sh_configure = module_extension(implementation = _sh_configure_impl) diff --git a/shell/private/repositories/BUILD b/shell/private/repositories/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/shell/private/repositories/sh_config.bzl b/shell/private/repositories/sh_config.bzl new file mode 100644 index 0000000..0b8c1c7 --- /dev/null +++ b/shell/private/repositories/sh_config.bzl @@ -0,0 +1,140 @@ +# Copyright 2018 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Configure sh_toolchains based on the local machine.""" + +visibility(["//shell", "//shell/private/extensions"]) + +_DEFAULT_SHELL_PATHS = { + "windows": "c:/msys64/usr/bin/bash.exe", + "linux": "/bin/bash", + "osx": "/bin/bash", + "freebsd": "/usr/local/bin/bash", + "openbsd": "/usr/local/bin/bash", +} + +_UNIX_SH_TOOLCHAIN_TEMPLATE = """ +sh_toolchain( + name = "{os}_sh", + path = {sh_path}, +) +""" + +_WINDOWS_SH_TOOLCHAIN_TEMPLATE = """ +sh_toolchain( + name = "{os}_sh", + path = {sh_path}, + launcher = "@bazel_tools//tools/launcher", + launcher_maker = "@bazel_tools//tools/launcher:launcher_maker", +) +""" + +_TOOLCHAIN_TEMPLATE = """ +toolchain( + name = "{os}_sh_toolchain", + toolchain = ":{os}_sh", + toolchain_type = "@rules_shell//shell:toolchain_type", + target_compatible_with = [ + "@platforms//os:{os}", + ], +) +""" + +def _sh_config_impl(repository_ctx): + """sh_config rule implementation. + + Creates sh_toolchains for commonly supported target platforms. + For the target platform matching the local machine, it detects the path of + the shell interpreter instead of using the default path. + + Args: + repository_ctx: the repository rule context object + """ + toolchains = [] + for os, default_shell_path in _DEFAULT_SHELL_PATHS.items(): + is_host = repository_ctx.os.name.startswith(os) + if is_host: + # This toolchain was first added before optional toolchains were + # available, so instead of not registering a toolchain if we + # couldn't find the shell, we register a toolchain with an empty + # path. + sh_path = _detect_local_shell_path(repository_ctx) or "" + else: + sh_path = default_shell_path + + sh_toolchain_template = _WINDOWS_SH_TOOLCHAIN_TEMPLATE if os == "windows" else _UNIX_SH_TOOLCHAIN_TEMPLATE + toolchains.append(sh_toolchain_template.format( + os = os, + sh_path = repr(sh_path), + )) + toolchains.append(_TOOLCHAIN_TEMPLATE.format( + os = os, + )) + + repository_ctx.file("BUILD", """ +load("@rules_shell//shell/toolchains:sh_toolchain.bzl", "sh_toolchain") +""" + "\n".join(toolchains)) + +sh_config = repository_rule( + environ = [ + "WINDIR", + "PATH", + ], + # TODO: Replace this with configure = True and add BAZEL_SH to the + # environ list above for consistency with CC and other repo rules. + # This would make discovery differ from --shell_executable. + local = True, + implementation = _sh_config_impl, +) + +def _detect_local_shell_path(repository_ctx): + if repository_ctx.os.name.startswith("windows"): + return _detect_local_shell_path_windows(repository_ctx) + else: + return _detect_local_shell_path_unix(repository_ctx) + +def _detect_local_shell_path_windows(repository_ctx): + sh_path = repository_ctx.os.environ.get("BAZEL_SH") + if sh_path: + return sh_path.replace("\\", "/") + + sh_path_obj = repository_ctx.which("bash.exe") + if sh_path_obj: + # repository_ctx.which returns a path object, convert that to + # string so we can call string.startswith on it. + sh_path = str(sh_path_obj) + + # When the Windows Subsystem for Linux is installed there's a + # bash.exe under %WINDIR%\system32\bash.exe that launches Ubuntu + # Bash which cannot run native Windows programs so it's not what + # we want. + windir = repository_ctx.os.environ.get("WINDIR") + if not windir or not sh_path.startswith(windir): + return sh_path.replace("\\", "/") + + return None + +def _detect_local_shell_path_unix(repository_ctx): + sh_path = repository_ctx.os.environ.get("BAZEL_SH") + if sh_path: + return sh_path + + sh_path_obj = repository_ctx.which("bash") + if sh_path_obj: + return str(sh_path_obj) + + sh_path_obj = repository_ctx.which("sh") + if sh_path_obj: + return str(sh_path_obj) + + return None diff --git a/shell/private/sh_executable.bzl b/shell/private/sh_executable.bzl new file mode 100644 index 0000000..1306c80 --- /dev/null +++ b/shell/private/sh_executable.bzl @@ -0,0 +1,197 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common code for sh_binary and sh_test rules.""" + +visibility(["//shell"]) + +_SH_TOOLCHAIN_TYPE = "//shell:toolchain_type" + +def _sh_executable_impl(ctx): + if len(ctx.files.srcs) != 1: + fail("you must specify exactly one file in 'srcs'", attr = "srcs") + + symlink = ctx.actions.declare_file(ctx.label.name) + src = ctx.files.srcs[0] + + ctx.actions.symlink( + output = symlink, + target_file = src, + is_executable = True, + progress_message = "Symlinking %{label}", + ) + + direct_files = [src, symlink] + + # TODO: Consider extracting this logic into a function provided by + # sh_toolchain to allow users to inject launcher creation logic for + # non-Windows platforms. + if ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]): + main_executable = _launcher_for_windows(ctx, symlink, src) + direct_files.append(main_executable) + else: + main_executable = symlink + + files = depset(direct = direct_files) + runfiles = ctx.runfiles(transitive_files = files, collect_default = True) + default_info = DefaultInfo( + executable = main_executable, + files = files, + runfiles = runfiles, + ) + + instrumented_files_info = coverage_common.instrumented_files_info( + ctx, + source_attributes = ["srcs"], + dependency_attributes = ["deps", "data"], + ) + + run_environment_info = RunEnvironmentInfo( + environment = { + key: ctx.expand_make_variables( + "env", + ctx.expand_location(value, ctx.attr.data, short_paths = True), + {}, + ) + for key, value in ctx.attr.env.items() + }, + inherited_environment = ctx.attr.env_inherit, + ) + + return [ + default_info, + instrumented_files_info, + run_environment_info, + ] + +_WINDOWS_EXECUTABLE_EXTENSIONS = [ + "exe", + "cmd", + "bat", +] + +def _is_windows_executable(file): + return file.extension in _WINDOWS_EXECUTABLE_EXTENSIONS + +def _create_windows_exe_launcher(ctx, sh_toolchain, primary_output): + if not sh_toolchain.launcher or not sh_toolchain.launcher_maker: + fail("Windows sh_toolchain requires both 'launcher' and 'launcher_maker' to be set") + + bash_launcher = ctx.actions.declare_file(ctx.label.name + ".exe") + + launch_info = ctx.actions.args().use_param_file("%s", use_always = True).set_param_file_format("multiline") + launch_info.add("binary_type=Bash") + launch_info.add(ctx.workspace_name, format = "workspace_name=%s") + launch_info.add("1" if ctx.configuration.runfiles_enabled() else "0", format = "symlink_runfiles_enabled=%s") + launch_info.add(sh_toolchain.path, format = "bash_bin_path=%s") + bash_file_short_path = primary_output.short_path + if bash_file_short_path.startswith("../"): + bash_file_rlocationpath = bash_file_short_path[3:] + else: + bash_file_rlocationpath = ctx.workspace_name + "/" + bash_file_short_path + launch_info.add(bash_file_rlocationpath, format = "bash_file_rlocationpath=%s") + + launcher_artifact = sh_toolchain.launcher + ctx.actions.run( + executable = sh_toolchain.launcher_maker, + inputs = [launcher_artifact], + outputs = [bash_launcher], + arguments = [launcher_artifact.path, launch_info, bash_launcher.path], + use_default_shell_env = True, + toolchain = _SH_TOOLCHAIN_TYPE, + ) + return bash_launcher + +def _launcher_for_windows(ctx, primary_output, main_file): + if _is_windows_executable(main_file): + if main_file.extension == primary_output.extension: + return primary_output + else: + fail("Source file is a Windows executable file, target name extension should match source file extension") + + # bazel_tools should always registers a toolchain for Windows, but it may have an empty path. + sh_toolchain = ctx.toolchains[_SH_TOOLCHAIN_TYPE] + if not sh_toolchain or not sh_toolchain.path: + fail("""No suitable shell toolchain found: +* if you are running Bazel on Windows, set the BAZEL_SH environment variable to the path of bash.exe +* if you are running Bazel on a non-Windows platform but are targeting Windows, register an sh_toolchain for the {} toolchain type +""".format(_SH_TOOLCHAIN_TYPE)) + + return _create_windows_exe_launcher(ctx, sh_toolchain, primary_output) + +def make_sh_executable_rule(extra_attrs = {}, **kwargs): + return rule( + _sh_executable_impl, + doc = """ +
+ The sh_binary
rule is used to declare executable shell scripts.
+ (sh_binary
is a misnomer: its outputs aren't necessarily binaries.) This rule ensures
+ that all dependencies are built, and appear in the runfiles
area at execution time.
+ We recommend that you name your sh_binary()
rules after the name of the script minus
+ the extension (e.g. .sh
); the rule name and the file name must be distinct.
+ sh_binary
respects shebangs, so any available interpreter may be used (eg.
+ #!/bin/zsh
)
+
For a simple shell script with no dependencies and some data files: +
++sh_binary( + name = "foo", + srcs = ["foo.sh"], + data = glob(["datafiles/*.txt"]), +) ++""", + attrs = { + "srcs": attr.label_list( + allow_files = True, + doc = """ +The list of input files. +
+ This attribute should be used to list shell script source files that belong to
+ this library. Scripts can load other scripts using the shell's source
+ or .
command.
+
deps
+at Typical attributes defined by
+most build rules.
+
+ This attribute should be used to list other sh_library
rules that provide
+ interpreted program source code depended on by the code in srcs
. The files
+ provided by these rules will be present among the runfiles
of this target.
+
+ The main use for this rule is to aggregate together a logical
+ "library" consisting of related scripts—programs in an
+ interpreted language that does not require compilation or linking,
+ such as the Bourne shell—and any data those programs need at
+ run-time. Such "libraries" can then be used from
+ the data
attribute of one or
+ more sh_binary
rules.
+
+ You can use the filegroup
rule to aggregate data
+ files.
+
+ In interpreted programming languages, there's not always a clear
+ distinction between "code" and "data": after all, the program is
+ just "data" from the interpreter's point of view. For this reason
+ this rule has three attributes which are all essentially equivalent:
+ srcs
, deps
and data
.
+ The current implementation does not distinguish between the elements of these lists.
+ All three attributes accept rules, source files and generated files.
+ It is however good practice to use the attributes for their usual purpose (as with other rules).
+
+sh_library( + name = "foo", + data = [ + ":foo_service_script", # an sh_binary with srcs + ":deploy_foo", # another sh_binary with srcs + ], +) ++""", + attrs = { + "srcs": attr.label_list( + allow_files = True, + doc = """ +The list of input files. +
+ This attribute should be used to list shell script source files that belong to
+ this library. Scripts can load other scripts using the shell's source
+ or .
command.
+
deps
+at Typical attributes defined by
+most build rules.
+
+ This attribute should be used to list other sh_library
rules that provide
+ interpreted program source code depended on by the code in srcs
. The files
+ provided by these rules will be present among the runfiles
of this target.
+