diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f4b8539..7cd5c92 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,14 +58,6 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 - - name: Configure Windows Bash and Unix utilities - if: runner.os == 'Windows' - shell: pwsh - run: | - & C:/msys64/usr/bin/bash.exe -lc 'pacman -S --needed --noconfirm bash coreutils diffutils grep sed' - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - "BAZEL_SH=C:/msys64/usr/bin/bash.exe" >> $env:GITHUB_ENV - "C:/msys64/usr/bin" >> $env:GITHUB_PATH - uses: bazel-contrib/setup-bazel@0.19.0 with: cache-save: ${{ github.ref == 'refs/heads/main' }} @@ -75,6 +67,7 @@ jobs: # Share repository cache between workflows. repository-cache: true - name: Built & Test + if: runner.os != 'Windows' shell: bash env: MSYS_NO_PATHCONV: "1" @@ -82,6 +75,7 @@ jobs: bazel test //... bazel test --enable_runfiles=no //... - name: Test external module consumer + if: runner.os != 'Windows' env: MSYS_NO_PATHCONV: "1" shell: bash @@ -89,6 +83,11 @@ jobs: bash bashtest/tests/external_consumer_test.sh bash bashtest/tests/tools_consumer_test.sh + - name: Test Windows without installed Unix tools + if: runner.os == 'Windows' + shell: pwsh + run: ./bashtest/tests/windows_runtime_test.ps1 + done: permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 73002ee..509e6c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -# 0.6.2 +# 0.7.0 + +* Automatically provision Bash and Unix utilities, including PCRE support, for + bashtest on Windows x86-64 without system installation or PATH configuration. * Added named `tools` mappings that build executables for the test execution platform and export their resolved paths with their runtime runfiles. diff --git a/MODULE.bazel b/MODULE.bazel index 1ae0f1d..2b05a8b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -17,9 +17,15 @@ module( name = "mboworks_bashtest", - version = "0.6.2", + version = "0.7.0", ) bazel_dep(name = "bazel_skylib", version = "1.9.2") bazel_dep(name = "platforms", version = "1.1.0") bazel_dep(name = "rules_shell", version = "0.8.0") + +windows_runtime = use_repo_rule("//bashtest/runtime:repositories.bzl", "windows_runtime") + +windows_runtime(name = "bashtest_windows_runtime") + +register_toolchains("//bashtest/runtime:windows_toolchain") diff --git a/README.md b/README.md index 9b13995..8f03147 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,18 @@ hand-rolled pipelines. ## Installation and requirements -This repository requires Bash and Unix utilities (`coreutils`, `diff`, `sed`, and -`grep`) on Linux, macOS, or Windows. Windows uses native Bazel with MSYS2 Bash; -it does not require WSL. Set `BAZEL_SH` to the Bash executable, for example -`C:/msys64/usr/bin/bash.exe`, and put `C:/msys64/usr/bin` on `PATH`. Install the -MSYS2 packages `bash coreutils diffutils grep sed`. PCRE assertions additionally -need a supported PCRE tool, such as a `grep` with `-P` support. +Linux and macOS require Bash and Unix utilities (`coreutils`, `diff`, `sed`, and +`grep`); PCRE assertions need a supported PCRE tool. + +On Windows x86-64, `bashtest` automatically downloads a pinned MSYS2 runtime, +including Bash, coreutils, diffutils, grep with PCRE support, sed, and their DLLs. +No MSYS2 installation, `BAZEL_SH`, or manual `PATH` setup is needed for bashtest +rules. The runtime is private to Bazel and does not install or update system +software. A Windows shell toolchain also supplies Bazel's shell-based test +helpers. Native Bazel 7 shell rules and `genrule` actions retain their own +requirements. +The first Windows build needs network access; subsequent builds can use Bazel's +repository cache. See [runtime details](bashtest/runtime/README.md). Run Bazel from PowerShell or Command Prompt on Windows. When invoking it from MSYS2 or Git Bash, set `MSYS_NO_PATHCONV=1` to preserve Bazel labels such as `//...`. diff --git a/bashtest/BUILD.bazel b/bashtest/BUILD.bazel index 320b26d..a871cc6 100644 --- a/bashtest/BUILD.bazel +++ b/bashtest/BUILD.bazel @@ -23,6 +23,7 @@ bzl_library( srcs = [ "bashtest.bzl", "tools.bzl", + "windows.bzl", ], ) @@ -63,3 +64,9 @@ genrule( ) exports_files(["launcher.sh"]) + +config_setting( + name = "windows", + constraint_values = ["@platforms//os:windows"], + visibility = ["//visibility:public"], +) diff --git a/bashtest/README.md b/bashtest/README.md index 4789717..de1deed 100644 --- a/bashtest/README.md +++ b/bashtest/README.md @@ -24,7 +24,7 @@ Bashtest provides `sh_test` wrapper that simplifies the creation of shell tests. * special test function `test::test_done`: If present, then this function runs last! The macro supports directory and manifest-only runfiles on Linux, macOS, and -Windows with MSYS2 Bash. See the root [installation requirements](../README.md#installation-and-requirements) +Windows x86-64 with an automatically provisioned MSYS2 runtime. See the root [installation requirements](../README.md#installation-and-requirements) for Windows setup and resolving data dependencies with `rlocation`. ## Executable tools diff --git a/bashtest/bashtest.bzl b/bashtest/bashtest.bzl index ae0bc0a..2f3c5db 100644 --- a/bashtest/bashtest.bzl +++ b/bashtest/bashtest.bzl @@ -16,8 +16,8 @@ """mboworks_bashtest/bashtest:bashtest""" load("@rules_shell//shell:sh_binary.bzl", "sh_binary") -load("@rules_shell//shell:sh_test.bzl", "sh_test") load(":tools.bzl", "bashtest_tools_test") +load(":windows.bzl", "windows_launcher_binary") # Resolve the bashtest runtime relative to *this* module's repository. A `Label` # constructed in a `.bzl` file is resolved against the repo that defines the @@ -94,16 +94,6 @@ def bashtest( "mboworks_bashtest": "$(rlocationpath {label})".format(label = _BASHTEST_SH), "_BASHTEST_SCRIPT": "$(rlocationpath {label})".format(label = ":" + script), } - if not tools: - sh_test( - name = name, - srcs = [_LAUNCHER], - deps = deps + [_BASHTEST_SH, _RUNFILES], - data = data + [":" + script, _BASHTEST_SH], - env = env | extra_env, - **kwargs - ) - return # Define an executable launcher, not another test. The single test below # owns both this executable and every execution-platform tool dependency. @@ -112,6 +102,11 @@ def bashtest( for attribute in ["args", "env_inherit", "size", "timeout", "flaky", "shard_count", "local", "exec_group_compatible_with"]: launcher_kwargs.pop(attribute, None) launcher_kwargs["testonly"] = True + compatibility = kwargs.get("target_compatible_with", []) + launcher_kwargs["target_compatible_with"] = compatibility + select({ + str(Label("//bashtest:windows")): ["@platforms//:incompatible"], + "//conditions:default": [], + }) sh_binary( name = runner_name, srcs = [_LAUNCHER], @@ -119,6 +114,17 @@ def bashtest( data = data + [":" + script, _BASHTEST_SH], **launcher_kwargs ) + launcher_kwargs.pop("use_bash_launcher", None) + launcher_kwargs["target_compatible_with"] = compatibility + select({ + str(Label("//bashtest:windows")): [], + "//conditions:default": ["@platforms//:incompatible"], + }) + windows_launcher_binary( + name = runner_name + "_windows", + deps = deps + [_BASHTEST_SH, _RUNFILES], + data = data + [":" + script, _BASHTEST_SH], + **launcher_kwargs + ) outer_kwargs = dict(kwargs) outer_kwargs.pop("use_bash_launcher", None) tool_names = {} @@ -126,7 +132,10 @@ def bashtest( tool_names.setdefault(label, []).append(variable) bashtest_tools_test( name = name, - runner = ":" + runner_name, + runner = select({ + str(Label("//bashtest:windows")): ":" + runner_name + "_windows", + "//conditions:default": ":" + runner_name, + }), env = env | extra_env, tools = {label: json.encode(names) for label, names in tool_names.items()}, data = data + [":" + script, _BASHTEST_SH], diff --git a/bashtest/launcher.sh b/bashtest/launcher.sh index e458725..bcbef01 100755 --- a/bashtest/launcher.sh +++ b/bashtest/launcher.sh @@ -27,7 +27,16 @@ source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' source "$0.runfiles/$f" 2>/dev/null || source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || -{ echo >&2 "ERROR: cannot find $f"; exit 1; } +{ + echo >&2 "ERROR: cannot find $f" + printf >&2 'Bash: %s\nPATH: %s\nRunfiles directory: %s\nRunfiles manifest: %s\n' \ + "${BASH}" "${PATH}" "${RUNFILES_DIR:-}" "${RUNFILES_MANIFEST_FILE:-}" + printf >&2 'Script: %s\nTest source directory: %s\n' "$0" "${TEST_SRCDIR:-}" + if command -v cygpath >/dev/null; then cygpath -am "${BASH}" >&2; fi + command -v grep cut >&2 + grep -m1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" >&2 + exit 1 +} set -e # The macro supplies canonical rlocation paths, independent of the consumer's diff --git a/bashtest/runtime/BUILD.bazel b/bashtest/runtime/BUILD.bazel new file mode 100644 index 0000000..3014c1d --- /dev/null +++ b/bashtest/runtime/BUILD.bazel @@ -0,0 +1,11 @@ +exports_files(["repositories.bzl"]) + +toolchain( + name = "windows_toolchain", + target_compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], + toolchain = "@bashtest_windows_runtime//:shell", + toolchain_type = "@rules_shell//shell:toolchain_type", +) diff --git a/bashtest/runtime/README.md b/bashtest/runtime/README.md new file mode 100644 index 0000000..ba65058 --- /dev/null +++ b/bashtest/runtime/README.md @@ -0,0 +1,41 @@ +# Managed Windows runtime + +Bashtest's Windows x86-64 launcher resolves a pinned MSYS2 Bash executable through +Bazel runfiles. The shell entry point prepends Bash's directory to the process PATH, +so both framework utilities and child processes find the bundled programs. +All executables and DLLs in `usr/bin` are declared runtime inputs, including for +manifest-only execution. Linux and macOS keep using the installed shell tools. + +When passing paths from another MSYS2 or Git Bash installation, use Windows absolute +paths, for example `--update-golden="$(cygpath -am "${PWD}")"`. Installation-relative +paths such as `/tmp` can refer to different directories in the managed runtime. + +The repository rule downloads the official MSYS2 base archive dated 2026-09-27 +and diffutils 3.12-1, verifying their SHA-256 hashes. The base includes Bash, +coreutils, grep with PCRE, sed, and runtime libraries; diffutils is an additional +package. No package manager is run, and no system installation is modified. +The toolchain declaration is local; its runtime is needed only for Windows. Expect roughly +42 MB of compressed downloads plus extracted files and the Bazel cache. + +Sources and package metadata: + +- [MSYS2 distribution archives](https://repo.msys2.org/distrib/x86_64/) +- [MSYS2 package archives](https://repo.msys2.org/msys/x86_64/) +- [MSYS2 package sources](https://github.com/msys2/MSYS2-packages) + +Upstream binaries retain their own licenses, including GPL licenses; bashtest's +Apache-2.0 license does not relicense them. The downloaded archive keeps upstream +license and package metadata, and license files are included in runtime inputs. +Update the archive URLs and hashes together, then run the clean-environment +Windows CI test, including the PCRE and golden-file checks. + +The launcher uses Bazel's native Windows launcher and launcher-maker targets, +matching rules_shell's approach. A registered Windows shell toolchain also points +to the downloaded Bash: Bazel's implicit coverage-helper dependency requires a +shell toolchain even for ordinary tests. This default can also supply rules_shell +binaries and tests on the local Windows machine. A root module can register its +own higher-priority shell toolchain. Native Bazel 7 shell rules and `genrule` +actions retain their separate shell configuration requirements. Native Windows ARM64 is not +provided. Remote Windows execution has not been validated; declaring the runtime +inputs alone is not a claim of remote-execution support. MSYS2 runtime relocation, +worker filesystem policies, and launcher availability also need validation. diff --git a/bashtest/runtime/repositories.bzl b/bashtest/runtime/repositories.bzl new file mode 100644 index 0000000..b7c5c76 --- /dev/null +++ b/bashtest/runtime/repositories.bzl @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""Pinned Windows runtime, downloaded only when a Windows test needs it.""" + +def _runtime_impl(ctx): + ctx.download_and_extract( + url = "https://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20260927.tar.xz", + sha256 = "ea2f31a0b6ade63914ce441ffb022f0f6aa96982bfefa2326460a26d5fb01322", + stripPrefix = "msys64", + ) + ctx.download_and_extract( + url = "https://repo.msys2.org/msys/x86_64/diffutils-3.12-1-x86_64.pkg.tar.zst", + sha256 = "7902c8ce3d4dd69a0f5e98dc9d5c83c17b23314ba486169db57ef6e2835ce3b6", + ) + ctx.file("BUILD.bazel", ''' +load("@rules_shell//shell/toolchains:sh_toolchain.bzl", "sh_toolchain") + +package(default_visibility = ["//visibility:public"]) +exports_files(["usr/bin/bash.exe"]) +filegroup( + name = "runtime", + srcs = glob(["usr/bin/**", "usr/share/licenses/**"]), +) +sh_toolchain( + name = "shell", + path = {bash_path}, + launcher = "@bazel_tools//tools/launcher:launcher", + launcher_maker = "@bazel_tools//tools/launcher:launcher_maker", +) +'''.format(bash_path = repr(str(ctx.path("usr/bin/bash.exe"))))) + +windows_runtime = repository_rule(implementation = _runtime_impl) diff --git a/bashtest/tests/BUILD.bazel b/bashtest/tests/BUILD.bazel index b81debd..60a633a 100644 --- a/bashtest/tests/BUILD.bazel +++ b/bashtest/tests/BUILD.bazel @@ -100,3 +100,12 @@ bashtest( "SECOND_HELPER": ":second_helper", }, ) + +bashtest( + name = "runtime_test", + srcs = ["runtime_test.sh"], + target_compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], +) diff --git a/bashtest/tests/external_consumer_test.sh b/bashtest/tests/external_consumer_test.sh index 7e9cc63..ba01b12 100755 --- a/bashtest/tests/external_consumer_test.sh +++ b/bashtest/tests/external_consumer_test.sh @@ -34,7 +34,7 @@ cd "${root}/consumer with spaces" cp "${repo}/.bazelversion" .bazelversion cat > MODULE.bazel < BUILD.bazel <<'EOF' @@ -65,9 +65,15 @@ bazel test --enable_runfiles=no --test_output=errors //:consumer_test bazel run --enable_runfiles=no //:consumer_test -- --keep-tmpdir=never # Exercise both documented update spellings with a workspace path containing spaces. +# Separate MSYS2 installations have different /tmp mounts. Cross the native +# Bazel boundary with a Windows path that identifies the same directory in both. +workspace="${PWD}" +case "${OSTYPE}" in + msys*|cygwin*) workspace="$(cygpath -am "${workspace}")" ;; +esac printf 'stale\n' > golden.txt -bazel run --enable_runfiles=no //:consumer_test -- --update-golden="${PWD}" --keep-tmpdir=never +bazel run --enable_runfiles=no //:consumer_test -- --update-golden="${workspace}" --keep-tmpdir=never [[ "$(cat golden.txt)" == 'hello world' ]] printf 'stale again\n' > golden.txt -bazel run --enable_runfiles=no //:consumer_test -- -u="${PWD}" --keep-tmpdir=never +bazel run --enable_runfiles=no //:consumer_test -- -u="${workspace}" --keep-tmpdir=never [[ "$(cat golden.txt)" == 'hello world' ]] diff --git a/bashtest/tests/runtime_test.sh b/bashtest/tests/runtime_test.sh new file mode 100755 index 0000000..f678396 --- /dev/null +++ b/bashtest/tests/runtime_test.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +set -euo pipefail +# shellcheck disable=SC1090,SC1091,SC2154 +source "${mboworks_bashtest}" + +# shellcheck disable=SC2317 # Functions are discovered by test_runner. +test::managed_windows_runtime() { + expect_matches 'bashtest_windows_runtime' "$(cygpath -am "${BASH}")" + for tool in cat cp cut date diff grep mkdir mktemp rm sed cygpath; do + expect_matches 'bashtest_windows_runtime' "$(cygpath -am "$(command -v "${tool}")")" + done + expect_pcre_matches '(?<=prefix)VALUE' 'prefixVALUE' + expect_eq 'second' "$(sed -n '2p' <<<$'first\nsecond')" + local scratch + scratch="$(test_tmpdir 'runtime with spaces')" + printf '%s\n' content > "${scratch}/one" + cp "${scratch}/one" "${scratch}/two" + expect_files_eq "${scratch}/one" "${scratch}/two" +} + +test_runner diff --git a/bashtest/tests/tools_consumer_test.sh b/bashtest/tests/tools_consumer_test.sh index 04b03d0..76d104f 100755 --- a/bashtest/tests/tools_consumer_test.sh +++ b/bashtest/tests/tools_consumer_test.sh @@ -34,7 +34,7 @@ cp "${repo}/.bazelversion" .bazelversion cp "${repo}/bashtest/tests/tool_fixture.bzl" tool_fixture.bzl cat > MODULE.bazel <