Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
943e4b4
Support Windows Bash and manifest-only runfiles
helly25 Oct 17, 2026
5323264
Remove invalid Bazel ignore glob on Windows
helly25 Oct 17, 2026
de63bb4
Install Windows diffutils and reject diff tool errors
helly25 Oct 17, 2026
5de00f6
Explain indirect diff invocation to ShellCheck
helly25 Oct 17, 2026
7ab2142
Make consumer cleanup and golden update arguments portable
helly25 Oct 17, 2026
7fb3571
Add named executable tools to bashtest
helly25 Oct 17, 2026
729f60f
Provision a managed Windows runtime for bashtest
helly25 Oct 17, 2026
816ba7f
Validate runtime paths and exclude the Windows WSL stub
helly25 Oct 17, 2026
598d615
Keep Bazelisk available while testing isolated Windows bootstrap
helly25 Oct 17, 2026
8324cbf
Provide the shell toolchain required by Bazel test helpers
helly25 Oct 17, 2026
d7f7d32
Use manifest lookup and test distinct executable helpers
helly25 Oct 17, 2026
e6a1493
Use a non-test launcher for tests with executable tools
helly25 Oct 17, 2026
27f389f
Merge tools launcher refactor and use one runtime test target
helly25 Oct 17, 2026
3cfadff
Format the synchronized launcher definitions
helly25 Oct 17, 2026
6d1f331
Keep fixtures private and test portable environment expansion
helly25 Oct 17, 2026
2613a7c
Merge remote-tracking branch 'origin/feat/test-tools' into feat/windo…
helly25 Oct 17, 2026
557ca17
Expose runfiles bootstrap failure diagnostics
helly25 Oct 17, 2026
c6124f2
Keep the managed Bash interpreter and utilities in Windows launchers
helly25 Oct 17, 2026
d77e3dc
Use native Windows paths across managed MSYS2 installations
helly25 Oct 17, 2026
55f53ba
Synchronize after PR 55 and prepare version 0.7.0
helly25 Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand All @@ -75,20 +67,27 @@ jobs:
# Share repository cache between workflows.
repository-cache: true
- name: Built & Test
if: runner.os != 'Windows'
shell: bash
env:
MSYS_NO_PATHCONV: "1"
run: |
bazel test //...
bazel test --enable_runfiles=no //...
- name: Test external module consumer
if: runner.os != 'Windows'
env:
MSYS_NO_PATHCONV: "1"
shell: bash
run: |
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
Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
8 changes: 7 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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")
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `//...`.
Expand Down
7 changes: 7 additions & 0 deletions bashtest/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ bzl_library(
srcs = [
"bashtest.bzl",
"tools.bzl",
"windows.bzl",
],
)

Expand Down Expand Up @@ -63,3 +64,9 @@ genrule(
)

exports_files(["launcher.sh"])

config_setting(
name = "windows",
constraint_values = ["@platforms//os:windows"],
visibility = ["//visibility:public"],
)
2 changes: 1 addition & 1 deletion bashtest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 21 additions & 12 deletions bashtest/bashtest.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -112,21 +102,40 @@ 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],
deps = deps + [_BASHTEST_SH, _RUNFILES],
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 = {}
for variable, label in tools.items():
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],
Expand Down
11 changes: 10 additions & 1 deletion bashtest/launcher.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions bashtest/runtime/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
)
41 changes: 41 additions & 0 deletions bashtest/runtime/README.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions bashtest/runtime/repositories.bzl
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions bashtest/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
12 changes: 9 additions & 3 deletions bashtest/tests/external_consumer_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ cd "${root}/consumer with spaces"
cp "${repo}/.bazelversion" .bazelversion
cat > MODULE.bazel <<EOF
module(name = "consumer")
bazel_dep(name = "mboworks_bashtest", version = "0.6.2", repo_name = "framework")
bazel_dep(name = "mboworks_bashtest", version = "0.7.0", repo_name = "framework")
local_path_override(module_name = "mboworks_bashtest", path = "${repo}")
EOF
cat > BUILD.bazel <<'EOF'
Expand Down Expand Up @@ -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' ]]
37 changes: 37 additions & 0 deletions bashtest/tests/runtime_test.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion bashtest/tests/tools_consumer_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ cp "${repo}/.bazelversion" .bazelversion
cp "${repo}/bashtest/tests/tool_fixture.bzl" tool_fixture.bzl
cat > MODULE.bazel <<EOF
module(name = "tools_consumer")
bazel_dep(name = "mboworks_bashtest", version = "0.6.2", repo_name = "framework")
bazel_dep(name = "mboworks_bashtest", version = "0.7.0", repo_name = "framework")
local_path_override(module_name = "mboworks_bashtest", path = "${repo}")
bazel_dep(name = "rules_shell", version = "0.8.0")
bazel_dep(name = "platforms", version = "1.1.0")
Expand Down
Loading
Loading