diff --git a/.bazelrc b/.bazelrc index decddbd..b92b77a 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,14 +1,33 @@ +# we require symlinks +startup --windows_enable_symlinks + common --lockfile_mode=off -build --remote_cache=grpcs://remote.buildbuddy.io -build --remote_timeout=3600 +# Automatically apply --config=linux, --config=windows etc +common --enable_platform_specific_config + +# Don’t want to push a rules author to update their deps if not needed. +# https://bazel.build/reference/command-line-reference#flag--check_direct_dependencies +# https://bazelbuild.slack.com/archives/C014RARENH0/p1691158021917459?thread_ts=1691156601.420349&cid=C014RARENH0 +common --check_direct_dependencies=off + +common --remote_cache=grpcs://remote.buildbuddy.io +common --remote_timeout=3600 # TODO: Remove once stardoc's protobuf doesn't have warnings -build --host_per_file_copt="external/.*protobuf.*@-Wno-everything" -build --per_file_copt="external/.*protobuf.*@-Wno-everything" +common:linux --host_per_file_copt="external/.*protobuf.*@-Wno-everything" +common:linux --per_file_copt="external/.*protobuf.*@-Wno-everything" # TODO: Remove once stardoc's zlib doesn't have warnings -build --host_per_file_copt="external/.*zlib.*@-Wno-everything" -build --per_file_copt="external/.*zlib.*@-Wno-everything" +common:linux --host_per_file_copt="external/.*zlib.*@-Wno-everything" +common:linux --per_file_copt="external/.*zlib.*@-Wno-everything" + +# rules_go's go sdk cgo compiler doesn't build with some compilers +# that may be configured by bazel, but we don't need it; disable +common --@rules_go//go/config:pure + +common --test_output=errors -test --test_output=errors +# PATH varies when running vs testing, this makes it more like running to validate the actual behavior. Specifically '.' is included for tests but not runs +common:linux --test_env=PATH=/usr/bin:/bin +common:mac --test_env=PATH=/usr/bin:/bin diff --git a/MODULE.bazel b/MODULE.bazel index 393f8d8..aa7926a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -5,7 +5,8 @@ module( ) bazel_dep(name = "bazel_skylib", version = "1.4.2") -bazel_dep(name = "rules_python", version = "0.27.1") +bazel_dep(name = "platforms", version = "0.0.9") +bazel_dep(name = "aspect_bazel_lib", version = "2.7.7") bazel_dep( name = "stardoc", @@ -13,3 +14,17 @@ bazel_dep( dev_dependency = True, repo_name = "io_bazel_stardoc", ) + +# Hermetic go toolchain +bazel_dep(name = "rules_go", version = "0.47.1", dev_dependency = True) +bazel_dep(name = "gazelle", version = "0.36.0", dev_dependency = True) +go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") +go_sdk.download( + version = "1.22.3", + # explicitly specify SDK names/checksums to avoid a query which fails in airgapped builds + # to update this, view checksums at https://go.dev/dl/?mode=json&include=all + sdks = { + "linux_amd64": ("go1.22.3.linux-amd64.tar.gz", "8920ea521bad8f6b7bc377b4824982e011c19af27df88a815e3586ea895f1b36"), + "windows_amd64": ("go1.22.3.windows-amd64.zip", "cab2af6951a6e2115824263f6df13ff069c47270f5788714fa1d776f7f60cb39"), + }, +) diff --git a/command.bzl b/command.bzl index c5f6f0c..93ff7af 100644 --- a/command.bzl +++ b/command.bzl @@ -8,9 +8,25 @@ load( "//internal:constants.bzl", "CommandInfo", "RUNFILES_PREFIX", - "rlocation_path", "update_attrs", ) +load("//internal/bazel-lib:windows_utils.bzl", "BATCH_RLOCATION_FUNCTION") +load("@aspect_bazel_lib//lib:paths.bzl", "BASH_RLOCATION_FUNCTION", "to_rlocation_path") + +_COMMAND_LAUNCHER_BAT_TMPL = """@echo off +SETLOCAL ENABLEEXTENSIONS +SETLOCAL ENABLEDELAYEDEXPANSION +set RUNFILES_LIB_DEBUG=0 +{BATCH_RLOCATION_FUNCTION} +{envs} + +call :rlocation "{command}" command_path +::echo rlocation({command}) returns %command_path% +::echo command bat launcher +::echo RUNFILES_MANIFEST_FILE=!RUNFILES_MANIFEST_FILE! +::echo launching: {exec}%command_path% {args} +{exec}%command_path% {args} +""" def _force_opt_impl(_settings, _attr): return {"//command_line_option:compilation_mode": "opt"} @@ -22,45 +38,65 @@ _force_opt = transition( ) def _command_impl(ctx): - runfiles = ctx.runfiles().merge(ctx.attr._bash_runfiles[DefaultInfo].default_runfiles) - - for data_dep in ctx.attr.data: - default_runfiles = data_dep[DefaultInfo].default_runfiles - if default_runfiles != None: - runfiles = runfiles.merge(default_runfiles) - + is_windows = ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]) command = ctx.attr.command if type(ctx.attr.command) == "Target" else ctx.attr.command[0] - default_info = command[DefaultInfo] - executable = default_info.files_to_run.executable - - default_runfiles = default_info.default_runfiles - if default_runfiles != None: - runfiles = runfiles.merge(default_runfiles) + executable = command[DefaultInfo].files_to_run.executable expansion_targets = ctx.attr.data + shell_type = "bash" if not is_windows or executable.extension in ["bash", "sh"] else "cmd" + if (shell_type == "bash"): + str_args = [ + "%s" % shell.quote(ctx.expand_location(v, targets = expansion_targets)) + for v in ctx.attr.arguments + ] + else: + str_args = [ + "%s" % shell.quote(ctx.expand_location(v, targets = expansion_targets)) + for v in ctx.attr.arguments + ] + + if not is_windows: + str_env = [ + "export %s=%s" % (k, shell.quote(ctx.expand_location(v, targets = expansion_targets))) + for k, v in ctx.attr.environment.items() + ] + command_exec = " ".join(["exec $(rlocation %s)" % shell.quote(to_rlocation_path(ctx, executable))] + str_args + ['"$@"\n']) + #print(command_exec) + launcher = ctx.actions.declare_file(ctx.label.name + ".bash") + ctx.actions.write( + output = launcher, + content = "\n".join([RUNFILES_PREFIX] + str_env + [command_exec]), + is_executable = True, + ) + else: + str_env = [ + "set \"%s=%s\"" % (k, ctx.expand_location(v, targets = expansion_targets)) + for k, v in ctx.attr.environment.items() + ] + launcher = ctx.actions.declare_file(ctx.label.name + ".bat") + ctx.actions.write( + output = launcher, + content = _COMMAND_LAUNCHER_BAT_TMPL.format( + envs = "\n".join(str_env), + exec = "%BAZEL_SH% " if shell_type == "bash" else "", + command = to_rlocation_path(ctx, executable), + args = " ".join(str_args), + BATCH_RLOCATION_FUNCTION = BATCH_RLOCATION_FUNCTION, + ), + is_executable = True, + ) - str_env = [ - "export %s=%s" % (k, shell.quote(ctx.expand_location(v, targets = expansion_targets))) - for k, v in ctx.attr.environment.items() - ] - str_args = [ - "%s" % shell.quote(ctx.expand_location(v, targets = expansion_targets)) - for v in ctx.attr.arguments - ] - command_exec = " ".join(["exec $(rlocation %s)" % shell.quote(rlocation_path(ctx, executable))] + str_args + ['"$@"\n']) - - out_file = ctx.actions.declare_file(ctx.label.name + ".bash") - ctx.actions.write( - output = out_file, - content = "\n".join([RUNFILES_PREFIX] + str_env + [command_exec]), - is_executable = True, - ) + runfiles = ctx.runfiles(files = ctx.files.data + ctx.files._bash_runfiles + [executable]) + runfiles = runfiles.merge_all([ + d[DefaultInfo].default_runfiles + for d in ctx.attr.data + [command] + ]) providers = [ DefaultInfo( - files = depset([out_file]), - runfiles = runfiles.merge(ctx.runfiles(files = ctx.files.data + [executable])), - executable = out_file, + files = depset([launcher]), + runfiles = runfiles, + executable = launcher, ), ] @@ -109,6 +145,9 @@ def command_with_transition(cfg, allowlist = None, doc = None): "_bash_runfiles": attr.label( default = Label("@bazel_tools//tools/bash/runfiles"), ), + "_windows_constraint": attr.label( + default = "@platforms//os:windows" + ), } return rule( diff --git a/internal/BUILD b/internal/BUILD index 70f09d8..88b4526 100644 --- a/internal/BUILD +++ b/internal/BUILD @@ -1,15 +1,4 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") -load("@rules_python//python:defs.bzl", "py_binary") - -py_binary( - name = "multirun", - srcs = ["multirun.py"], - python_version = "PY3", - visibility = ["//visibility:public"], - deps = [ - "@rules_python//python/runfiles", - ], -) bzl_library( name = "constants", diff --git a/internal/bazel-lib/BUILD b/internal/bazel-lib/BUILD new file mode 100644 index 0000000..86afbef --- /dev/null +++ b/internal/bazel-lib/BUILD @@ -0,0 +1,11 @@ +# Significant update to windows_utils.bzl from bazel-lib +# Now contains v3 runfiles logic +# Would like to contribute this back to bazel-lib + +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "windows_utils", + srcs = ["windows_utils.bzl"], + visibility = ["//:__pkg__"], +) diff --git a/internal/bazel-lib/windows_utils.bzl b/internal/bazel-lib/windows_utils.bzl new file mode 100644 index 0000000..924e176 --- /dev/null +++ b/internal/bazel-lib/windows_utils.bzl @@ -0,0 +1,445 @@ +# Copyright 2017 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. + +"Helpers for rules running on windows" + +load("@aspect_bazel_lib//lib/private:paths.bzl", "paths") + +# cmd.exe function for looking up runfiles. +# Equivalent of the BASH_RLOCATION_FUNCTION in paths.bzl. +# Use this to write actions that don't require bash. +# Originally by @meteorcloudy in +# https://github.com/bazelbuild/rules_nodejs/commit/f06553a +BATCH_RLOCATION_FUNCTION = r""" +rem Usage of rlocation function: +rem call :rlocation +rem The rlocation function maps the given to its absolute +rem path and stores the result in a variable named . +rem This function fails if the doesn't exist in mainifest +rem file. +:: Start of rlocation +goto :end +:rlocation +if "%~2" equ "" ( + echo ERROR: Expected two arguments for rlocation function. 1>&2 1>&2 + exit 1 +) + +:: if set outside this script, these variables may have unix paths. Update to windows. +if not "%RUNFILES_MANIFEST_FILE%"=="" ( + set RUNFILES_MANIFEST_FILE=!RUNFILES_MANIFEST_FILE:/=\! +) +if not "%RUNFILES_DIR%"=="" ( + set RUNFILES_DIR=!RUNFILES_DIR:/=\! +) +if not "%RUNFILES_REPO_MAPPING%"=="" ( + set RUNFILES_REPO_MAPPING=!RUNFILES_REPO_MAPPING:/=\! +) + +set GOT_RF=0 +if not "%RUNFILES_DIR%"=="" if exist "%RUNFILES_DIR%" (set GOT_RF=1) +if not "%RUNFILES_MANIFEST_FILE%"=="" if exist "%RUNFILES_MANIFEST_FILE%" (set GOT_RF=1) +if "%GOT_RF%"=="0" ( + if exist "%~f0.runfiles_manifest" ( + set "RUNFILES_MANIFEST_FILE=%~f0.runfiles_manifest" + ) else if exist "%~f0.runfiles\MANIFEST" ( + set "RUNFILES_MANIFEST_FILE=%~f0.runfiles\MANIFEST" + ) else if exist "%~f0.runfiles" ( + set "RUNFILES_DIR=%~f0.runfiles" + ) +) + +if not exist "%RUNFILES_REPO_MAPPING%" ( + set RUNFILES_REPO_MAPPING=%~f0.repo_mapping +) +if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo RUNFILES_LIB_DEBUG=!RUNFILES_LIB_DEBUG! 1>&2 + echo RUNFILES_REPO_MAPPING=%RUNFILES_REPO_MAPPING% 1>&2 + echo RUNFILES_MANIFEST_FILE=%RUNFILES_MANIFEST_FILE% 1>&2 +) + +:: we always set these; unlike the bash script, this always runs on windows +set _RLOCATION_ISABS_PATTERN="^[a-zA-Z]:[/\\]" +:: Windows paths are case insensitive and Bazel and MSYS2 capitalize differently, so we can't +:: assume that all paths are in the same native case. +set _RLOCATION_GREP_CASE_INSENSITIVE_ARGS=-i + +if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%1^): start 1>&2 +) + +REM Check if the path is absolute +if "%~f1"=="%1" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%1^): absolute path, return 1>&2 + ) + set "convert=%~1" + set "%~2=!convert:/=\!" + exit /b 0 +) +REM Check if the path is not normalized +if "%1"=="../*" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: rlocation(%1^): path is not normalized 1>&2 + ) + exit /b 1 +) +REM Check if the path is absolute without drive name +if "%1:~0,1%"=="\\" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: rlocation(%1^): absolute path without drive name 1>&2 + ) + exit /b 1 +) + +if exist "%RUNFILES_REPO_MAPPING%" ( + for /f "tokens=1 delims=/" %%a in ("%1") do ( + set "target_repo_apparent_name=%%a" + ) + rem Use -s to get an empty remainder if the argument does not contain a slash. + rem The repo mapping should not be applied to single segment paths, which may + rem be root symlinks. + for /f "tokens=2-99 delims=/" %%a in ("%1") do ( + set "remainder=%%a" + ) + if not "!remainder!"=="" ( + if "%2"=="" ( + call :runfiles_current_repository source_repo 2 + ) else ( + set "source_repo=%2" + ) + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%1^): looking up canonical name for (!target_repo_apparent_name!^) from (!source_repo!^) in (%RUNFILES_REPO_MAPPING%^) 1>&2 + ) + for /f "tokens=1-3 delims=," %%a in (findstr /r /c:"^!source_repo!,!target_repo_apparent_name!," "%RUNFILES_REPO_MAPPING%") do ( + set "target_repo=%%c" + ) + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%1^): canonical name of target repo is (!target_repo!^) 1>&2 + ) + if not "!target_repo!"=="" ( + set "rlocation_path=!target_repo!/!remainder!" + ) else ( + set "rlocation_path=%1" + ) + ) else ( + set "rlocation_path=%1" + ) +) else ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%1^): not using repository mapping (%RUNFILES_REPO_MAPPING%^) since it does not exist 1>&2 + ) + set "rlocation_path=%1" +) + +set "rlocation_checked_out=" +call :runfiles_rlocation_checked !rlocation_path! rlocation_checked_out +if not "%rlocation_checked_out%"=="" ( + set "%~2=%rlocation_checked_out:/=\%" +) +if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%1^): returning (%~2) 1>&2 +) +exit /b 0 +:: End of rlocation + +:: :runfiles_current_repository +:: Returns the canonical name of the Bazel repository containing the script that +:: calls this function. +:: n: return the canonical name of the N-th caller (pass 1 for standard use cases) +:: result: variable name for the result +:: +:: Note: This function only works correctly with Bzlmod enabled. Without Bzlmod, +:: its return value is ignored if passed to rlocation. +:runfiles_current_repository +set "idx=%~1" +if "%idx%"=="" set "idx=1" + +for /f "tokens=%idx%" %%a in ("%~f0") do ( + set "raw_caller_path=%%a" +) + +if not exist "!raw_caller_path!" ( + set "caller_path=%~dp0\!raw_caller_path!" +) else ( + set "caller_path=!raw_caller_path!" +) + +if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: runfiles_current_repository(!idx!^): caller's path is (!caller_path!^) 1>&2 +) + +set "rlocation_path=" + +if exist "%RUNFILES_MANIFEST_FILE%" ( + REM Escape caller_path for use in the findstr regex below. Also replace \ with / since the manifest + REM uses / as the path separator even on Windows. + set "normalized_caller_path=!caller_path:\=/!" + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: runfiles_current_repository(!idx!^): normalized caller's path is (!normalized_caller_path!^) 1>&2 + ) + for /f "tokens=1 delims= " %%a in (findstr /r /c:"^[^ ]* !normalized_caller_path!$" "%RUNFILES_MANIFEST_FILE%") do ( + set "rlocation_path=%%a" + ) + if "%rlocation_path%"=="" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: runfiles_current_repository(!idx!^): (!normalized_caller_path!^) is not the target of an entry in the runfiles manifest (%RUNFILES_MANIFEST_FILE%^) 1>&2 + ) + REM The binary may also be run directly from bazel-bin or bazel-out. + for /f "tokens=5 delims=/" %%a in ('echo %normalized_caller_path% ^| findstr /r /c:"(^|/)(bazel-out/[^/]+/bin|bazel-bin)/external/[^/]+/"') do ( 1>&2 + set "repository=%%a" + ) + if not "!repository!"=="" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: runfiles_current_repository(!idx!^): (!normalized_caller_path!^) lies in repository (!repository!^) (parsed exec path^) 1>&2 + ) + set "%~2=!repository!" + ) else ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: runfiles_current_repository(!idx!^): (!normalized_caller_path!^) lies in the main repository (parsed exec path^) 1>&2 + ) + set %~2="" + ) + exit /b 1 + ) else ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: runfiles_current_repository(!idx!^): (!normalized_caller_path!^) is the target of (!rlocation_path!^) in the runfiles manifest 1>&2 + ) + ) +) + +if "!rlocation_path!"=="" if exist "%RUNFILES_DIR%" ( + set "normalized_caller_path=!caller_path:\=/!" + set "normalized_dir=!RUNFILES_DIR:/=\!" + rem if not "!_RLOCATION_GREP_CASE_INSENSITIVE_ARGS!"=="" ( + rem for /f "tokens=*" %%a in ('echo !normalized_caller_path! ^| tr "[:upper:]" "[:lower:]"') do ( 1>&2 + rem set "normalized_caller_path=%%a" + rem ) + rem for /f "tokens=*" %%a in ('echo !normalized_dir! ^| tr "[:upper:]" "[:lower:]"') do ( 1>&2 + rem set "normalized_dir=%%a" + rem ) + rem ) + if "!normalized_caller_path:~0,%normalized_dir:~0,-1%!"=="!normalized_dir!" ( + set "rlocation_path=!normalized_caller_path:~%normalized_dir:~0,-1%!" + set "rlocation_path=!rlocation_path:~1!" + ) + if "!rlocation_path!"=="" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: runfiles_current_repository(!idx!^): (!normalized_caller_path!^) does not lie under the runfiles directory (!normalized_dir!^) 1>&2 + ) + REM The only shell script that is not executed from the runfiles directory (if it is populated) + REM is the sh_binary entrypoint. Parse its path under the execroot, using the last match to + REM allow for nested execroots (e.g. in Bazel integration tests). The binary may also be run + REM directly from bazel-bin. + rem for /f "tokens=5 delims=/" %%a in ('echo !normalized_caller_path! ^| findstr /r /c:"(^|/)(bazel-out/[^/]+/bin|bazel-bin)/external/[^/]+/"') do ( 1>&2 + rem set "repository=%%a" + rem ) + if not "!repository!"=="" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: runfiles_current_repository(!idx!^): (!normalized_caller_path!^) lies in repository (!repository!^) (parsed exec path^) 1>&2 + ) + set "%~2=!repository!" + ) else ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: runfiles_current_repository(!idx!^): (!normalized_caller_path!^) lies in the main repository (parsed exec path^) 1>&2 + ) + set %~2="" + ) + exit /b 0 + ) else ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: runfiles_current_repository(!idx!^): (!caller_path!^) has path (!rlocation_path^) relative to the runfiles directory (%RUNFILES_DIR%:-^) 1>&2 + ) + ) +) + +if "!rlocation_path!"=="" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: runfiles_current_repository(!idx!^): cannot determine repository for (!caller_path!^) since neither the runfiles directory (%RUNFILES_DIR%:-^) nor the runfiles manifest (%RUNFILES_MANIFEST_FILE%:-^) exist 1>&2 + ) + exit /b 1 +) + +if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: runfiles_current_repository(!idx!^): (!caller_path!^) corresponds to rlocation path (!rlocation_path!^) 1>&2 +) + +REM Normalize the rlocation_path to be of the form repo/pkg/file. +set "rlocation_path=!rlocation_path:_main/external/=!" +set "rlocation_path=!rlocation_path:_main/../=!" + +for /f "tokens=1 delims=/" %%a in ("!rlocation_path!") do ( + set "repository=%%a" +) + +if "!repository!"=="_main" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: runfiles_current_repository(!idx!^): (!rlocation_path!^) lies in the main repository 1>&2 + ) + set %~2="" +) else ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: runfiles_current_repository(!idx!^): (!rlocation_path!^) lies in repository (!repository!^) 1>&2 + ) + set %~2=!repository! +) + +endlocal +exit /b +:: end of runfiles_current_repository + +:runfiles_rlocation_checked +:: there may be both a manifest file and runfiles dir. Look in the manifest first, then runfiles. +if exist "%RUNFILES_MANIFEST_FILE%" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%~1^): looking in RUNFILES_MANIFEST_FILE (!RUNFILES_MANIFEST_FILE!^) 1>&2 + ) + for /f "tokens=2 delims= " %%a in ('findstr /b /c:"%~1 " "%RUNFILES_MANIFEST_FILE%"') do ( + set "result=%%a" + ) + if "!result!"=="" ( + REM If path references a runfile that lies under a directory that itself + REM is a runfile, then only the directory is listed in the manifest. Look + REM up all prefixes of path in the manifest and append the relative path + REM from the prefix if there is a match. + set "prefix=%~1" + set "prefix_result=" + set "new_prefix=" + :while + for /f "delims=/ tokens=1,*" %%a in ("!prefix!") do ( + set "new_prefix=%%a" + ) + if "!new_prefix!"=="!prefix!" ( + goto :end_while + ) + set "prefix=!new_prefix!" + for /f "tokens=2 delims= " %%a in ('findstr /b /c:"!prefix! " %RUNFILES_MANIFEST_FILE%') do ( + set "prefix_result=%%a" + ) + if "!prefix_result!"=="" ( + echo NO PREFIX RESULT, LOOPING + goto :while + ) + set "candidate=!prefix_result!!%~1:~!prefix!:~1!" + if exist "!candidate!" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%~1^): found in manifest as (!candidate!^) via prefix (!prefix!^) 1>&2 + ) + set %~2=!candidate! + exit /b 0 + ) + REM At this point, the manifest lookup of prefix has been successful, + REM but the file at the relative path given by the suffix does not + REM exist. We do not continue the lookup with a shorter prefix for two + REM reasons: + REM 1. Manifests generated by Bazel never contain a path that is a + REM prefix of another path. + REM 2. Runfiles libraries for other languages do not check for file + REM existence and would have returned the non-existent path. It seems + REM better to return no path rather than a potentially different, + REM non-empty path. + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%~1^): found in manifest as (!candidate!^) via prefix (!prefix!^), but file does not exist 1>&2 + ) + goto :end_while + :end_while + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%~1^): not found in manifest 1>&2 + ) + set %~2="" + exit /b 0 + ) else ( + if exist "!result!" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%~1^): found in manifest as (!result!^) 1>&2 + ) + set "%~2=!result!" + exit /b 0 + ) else ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%~1^): found in manifest as (!result!^), but file does not exist 1>&2 + ) + set %~2="" + exit /b 0 + ) + ) +) +if exist "!RUNFILES_DIR!\%~1" ( + if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo INFO[runfiles.bat]: rlocation(%~1^): found under RUNFILES_DIR (!RUNFILES_DIR!^), return 1>&2 + ) + set "%~2=!RUNFILES_DIR!\%~1" + exit /b 0 +) +if "!RUNFILES_LIB_DEBUG!"=="1" ( + echo ERROR[runfiles.bat]: cannot look up runfile "%~1" (RUNFILES_DIR="!RUNFILES_DIR!", RUNFILES_MANIFEST_FILE="!RUNFILES_MANIFEST_FILE!"^) 1>&2 +) +exit /b 1 +::end of runfiles_rlocation_checked + +:end +:: leave these variables set with forward slashes, for compatibility with any +:: bash runfile calls made downstream +if not "%RUNFILES_MANIFEST_FILE%"=="" ( + set RUNFILES_MANIFEST_FILE=%RUNFILES_MANIFEST_FILE:\=/% +) +if not "%RUNFILES_DIR%"=="" ( + set RUNFILES_DIR=%RUNFILES_DIR:\=/% +) +if not "%RUNFILES_REPO_MAPPING%"=="" ( + set RUNFILES_REPO_MAPPING=%RUNFILES_REPO_MAPPING:\=/% +) +""" + +def create_windows_native_launcher_script(ctx, shell_script): + """Create a Windows Batch file to launch the given shell script. + + The rule should specify @bazel_tools//tools/sh:toolchain_type as a required toolchain. + + Args: + ctx: Rule context + shell_script: The bash launcher script + + Returns: + A windows launcher script + """ + name = shell_script.basename + if name.endswith(".sh"): + name = name[:-3] + win_launcher = ctx.actions.declare_file(name + ".bat", sibling = shell_script) + ctx.actions.write( + output = win_launcher, + content = r"""@echo off 1>&2 +SETLOCAL ENABLEEXTENSIONS +SETLOCAL ENABLEDELAYEDEXPANSION +set RUNFILES_MANIFEST_ONLY=1 +{rlocation_function} +call :rlocation "{sh_script}" run_script +for %%a in ("{bash_bin}") do set "bash_bin_dir=%%~dpa" +set PATH=%bash_bin_dir%;%PATH% +set args=%* +rem Escape \ and * in args before passsing it with double quote +if defined args ( + set args=!args:\=\\\\! + set args=!args:"=\"! +) +"{bash_bin}" -c "!run_script! !args!" +""".format( + bash_bin = ctx.toolchains["@bazel_tools//tools/sh:toolchain_type"].path, + sh_script = paths.to_rlocation_path(ctx, shell_script), + rlocation_function = BATCH_RLOCATION_FUNCTION, + ), + is_executable = True, + ) + return win_launcher diff --git a/internal/multirun.py b/internal/multirun.py deleted file mode 100644 index 2acf766..0000000 --- a/internal/multirun.py +++ /dev/null @@ -1,122 +0,0 @@ -import json -import os -import shutil -import subprocess -import sys -import platform -from typing import Dict, List, NamedTuple, Union - -from python.runfiles import runfiles - -_R = runfiles.Create() - - -class Command(NamedTuple): - path: str - tag: str - args: List[str] - env: Dict[str, str] - - -def _run_command(command: Command, block: bool, **kwargs) -> Union[int, subprocess.Popen]: - if platform.system() == "Windows": - bash = shutil.which("bash.exe") - if not bash: - raise SystemExit("error: bash.exe not found in PATH") - - args = [bash, "-c", f'{command.path} "$@"', "--"] + command.args - else: - args = [command.path] + command.args - env = dict(os.environ) - env.update(command.env) - if block: - return subprocess.check_call(args, env=env) - else: - return subprocess.Popen(args, env=env, **kwargs) - - -def _perform_concurrently(commands: List[Command], print_command: bool, buffer_output: bool) -> bool: - kwargs = {} - if buffer_output: - kwargs = { - "stdout" : subprocess.PIPE, - "stderr" : subprocess.STDOUT - } - - processes = [ - (command, _run_command(command, block=False, **kwargs)) - for command - in commands - ] - - success = True - try: - for command, process in processes: - process.wait() - if print_command and buffer_output: - print(command.tag, flush=True) - - stdout = process.communicate()[0] - if stdout: - print(stdout.decode().strip(), flush=True) - - if process.returncode != 0: - success = False - except KeyboardInterrupt: - for command, process in processes: - process.kill() - process.wait() - success = False - - return success - - -def _perform_serially(commands: List[Command], print_command: bool, keep_going: bool) -> bool: - success = True - for command in commands: - if print_command: - print(command.tag, flush=True) - - try: - _run_command(command, block=True) - except subprocess.CalledProcessError: - if keep_going: - success = False - else: - return False - except KeyboardInterrupt: - return False - - return success - - -def _script_path(workspace_name: str, path: str) -> str: - # Even on Windows runfiles require forward slashes. - if path.startswith("../"): - return _R.Rlocation(path[3:]) - else: - return _R.Rlocation(f"{workspace_name}/{path}") - - -def _main(instructions_path: str, extra_args: List[str]) -> None: - with open(instructions_path) as f: - instructions = json.load(f) - - workspace_name = instructions["workspace_name"] - commands = [ - Command(_script_path(workspace_name, blob["path"]), blob["tag"], - blob["args"] + extra_args, blob["env"]) - for blob in instructions["commands"] - ] - parallel = instructions["jobs"] == 0 - print_command: bool = instructions["print_command"] - if parallel: - success = _perform_concurrently(commands, print_command, instructions["buffer_output"]) - else: - success = _perform_serially(commands, print_command, instructions["keep_going"]) - - sys.exit(0 if success else 1) - - -if __name__ == "__main__": - _main(sys.argv[1], sys.argv[2:]) diff --git a/internal/multirun/BUILD.bazel b/internal/multirun/BUILD.bazel new file mode 100644 index 0000000..3da3c9b --- /dev/null +++ b/internal/multirun/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "multirun-lib", + srcs = [ + "multirun.go", + ], + #importpath = "github.com/keith/rules_multirun/multirun", + deps = ["@rules_go//go/runfiles"], + visibility = ["//visibility:private"], +) + +go_binary( + name = "multirun", + embed = [":multirun-lib"], + msan = "off", + #pure = "off", + race = "off", + static = "off", + visibility = ["//visibility:public"], +) diff --git a/internal/multirun/multirun.go b/internal/multirun/multirun.go new file mode 100644 index 0000000..c3fbb2d --- /dev/null +++ b/internal/multirun/multirun.go @@ -0,0 +1,310 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + + "github.com/bazelbuild/rules_go/go/runfiles" +) + +func runfile(path string) (string, error) { + fullPath, err1 := runfiles.Rlocation(path) + if err1 != nil { + // This block may not be needed now we are using the latest + // runfiles library, try without + strippedPath := strings.SplitN(path, "/", 2)[1] + fullPath2, err2 := runfiles.Rlocation(strippedPath) + if err2 != nil { + fmt.Fprintf(os.Stderr, "Failed to lookup runfile for %s %s\n", path, err1.Error()) + fmt.Fprintf(os.Stderr, "also tried %s %s\n", strippedPath, err2.Error()) + return "", err1 + } + fullPath = fullPath2 + } + return fullPath, nil +} + +func debugEnv() { + env := os.Environ() + for _, e := range env { + if strings.HasPrefix(e, "RUNFILES_") || strings.HasPrefix(e, "BUILD_") || strings.HasPrefix(e, "TEST_") { + fmt.Println(e) + } + } + + manifest := os.Getenv("RUNFILES_MANIFEST_FILE") + fmt.Println("RUNFILES_MANIFEST_FILE="+manifest) +} + +type Command struct { + Tag string `json:"tag"` + Path string `json:"path"` + Args []string `json:"args"` + Env map[string]string `json:"env"` +} + +type Instructions struct { + Commands []Command `json:"commands"` + Jobs int `json:"jobs"` + Print_command bool `json:"print_command"` + Keep_going bool `json:"keep_going"` + Buffer_output bool `json:"buffer_output"` + Verbose bool `json:"verbose"` +} + +func readInstructions(instructionsFile string) (Instructions, error) { + content, err := ioutil.ReadFile(instructionsFile) + if err != nil { + return Instructions{}, fmt.Errorf("failed to read instructions file %q: %v", instructionsFile, err) + } + var instr Instructions + if err = json.Unmarshal(content, &instr); err != nil { + return Instructions{}, fmt.Errorf("failed to parse file %q as JSON: %v", instructionsFile, err) + } + return instr, nil +} + +func runCommand(command Command, bufferOutput bool, verbose bool) (int, string, error) { + var cmd *exec.Cmd + args := command.Args + env := os.Environ() // Convert map to format "key=value" + for k, v := range command.Env { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + } + + if verbose { + cmdStr := command.Path + " " + strings.Join(args, " ") + fmt.Println("Command line: ", cmdStr) + } + cmd = exec.Command(command.Path, args...) + cmd.Env = env + + var stdoutBuf bytes.Buffer + if bufferOutput { + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stdoutBuf + } else { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + err := cmd.Run() // Run and wait for the command to complete + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + return exitError.ExitCode(), stdoutBuf.String(), nil + } + return 0, stdoutBuf.String(), err + } + return 0, stdoutBuf.String(), nil +} + +func performConcurrently(commands []Command, printCommand bool, bufferOutput bool, verbose bool) bool { + if (verbose) { + fmt.Printf("performConcurrently: %d commands\n", len(commands)) + } + var wg sync.WaitGroup + success := true + mu := &sync.Mutex{} // To safely update `success` + + for _, cmd := range commands { + wg.Add(1) + go func(cmd Command) { + defer wg.Done() + exitCode, output, err := runCommand(cmd, bufferOutput, verbose) + if err != nil { + fmt.Println("Error running command:", err) + mu.Lock() + success = false + mu.Unlock() + return + } + + if printCommand && bufferOutput { + // If print command is set, buffer output isn't, we don't print commands + // TODO: is this correct?!! + fmt.Println(cmd.Tag) + } + + if bufferOutput { + fmt.Print(output) // Print buffered output + } + + if exitCode != 0 { + mu.Lock() + success = false + mu.Unlock() + } + }(cmd) + } + + wg.Wait() // Wait for all goroutines to finish + return success +} + +func performSerially(commands []Command, printCommand bool, keepGoing bool, verbose bool) bool { + if (verbose) { + fmt.Printf("performSerially: %d commands\n", len(commands)) + } + success := true + for _, cmd := range commands { + if printCommand { + fmt.Println(cmd.Tag) + } + + // Serial always discards output, regardless of setting in json + bufferOutput := false + code, _, err := runCommand(cmd, bufferOutput, verbose) + if code != 0 || err != nil { + if keepGoing { + success = false + } else { + return false + } + } + } + return success +} + +// cancelOnInterrupt calls f when os.Interrupt or SIGTERM is received. +// It ignores subsequent interrupts on purpose - program should exit correctly after the first signal. +func cancelOnInterrupt(ctx context.Context, f context.CancelFunc) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + select { + case <-ctx.Done(): + case <-c: + f() + } + }() +} + +// As we are invoked via symlink, on linux os.Executable() doesn't tell us the +// executable we were invoked as. Use pwd and args[0] to +// reconstruct the path. +func invokingExe() (string) { + if runtime.GOOS == "windows" { + exe, _ := os.Executable() + return exe + } + arg0 := os.Args[0] + if (strings.HasPrefix(arg0, "/")) { + return arg0 + } + cwd := os.Getenv("PWD") + exe := cwd + "/" + arg0 + return exe +} + +func windowsRunViaBash(command Command) (bool) { + if runtime.GOOS == "windows" { + if (strings.HasSuffix(command.Path, ".bash") || strings.HasSuffix(command.Path, ".bash")) { + return true + } + } + return false +} + +// Resolve runfiles and rewrite bash commands on windows to execute +// via bash -c +func resolveCommands(commands []Command) ([]Command) { + var out []Command + bashPath := "" + for _, command := range commands { + path, err := runfile(command.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "%+v\n", err) + os.Exit(1) + } + command.Path = path + if (windowsRunViaBash(command)) { + if runtime.GOOS == "windows" && bashPath == "" { + bash, err := exec.LookPath("bash.exe") + if err != nil { + fmt.Errorf("error: bash.exe not found in PATH") + os.Exit(1) + } + bashPath = bash + } + unixPath := strings.Replace(command.Path, "\\", "/", -1) + command.Args = append([]string{"-c", unixPath + " \"$@\"", "--"}, command.Args...) + command.Path = bashPath + } + out = append(out, command) + } + return out +} + +func main() { + verbose := os.Getenv("MULTIRUN_VERBOSE") != "" + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + cancelOnInterrupt(ctx, cancelFunc) + + // Because we are invoked via a symlink, we cannot accept any command line args + // The instructions file is always adjacent to the symlink location + exe := invokingExe() + + // We must only set runfiles env if it isn't already set + if val := os.Getenv("RUNFILES_MANIFEST_FILE"); val == "" { + if dir := os.Getenv("RUNFILES_DIR"); dir == "" { + manifestFile := exe + ".runfiles_manifest" + if verbose { + fmt.Println("set RUNFILES_MANIFEST_FILE="+manifestFile) + } + if err := os.Setenv("RUNFILES_MANIFEST_FILE", manifestFile); err != nil { + fmt.Println("Failed to set RUNFILES_MANIFEST_FILE") + os.Exit(1) + } + } + } + + basePath, _ := strings.CutSuffix(exe, ".exe") + instructionsFile := basePath + ".json" + instr, err := readInstructions(instructionsFile) + if err != nil { + fmt.Fprintf(os.Stderr, "%+v\n", err) + os.Exit(1) + } + + parallel := instr.Jobs != 1 + printCommand := instr.Print_command + instr.Commands = resolveCommands(instr.Commands) + + verbose = verbose || instr.Verbose + if verbose { + fmt.Println("args[0]: "+os.Args[0]) + fmt.Println("invoking exe: "+exe) + debugEnv() + fmt.Println("Read instructions "+instructionsFile) + b, err := json.MarshalIndent(instr, "", " ") + if err != nil { + fmt.Println("error:", err) + } + fmt.Print(string(b)) + } + + var success bool + if parallel { + success = performConcurrently(instr.Commands, printCommand, instr.Buffer_output, verbose) + } else { + success = performSerially(instr.Commands, printCommand, instr.Keep_going, verbose) + } + + if success { + os.Exit(0) + } else { + os.Exit(1) + } +} diff --git a/multirun.bzl b/multirun.bzl index bdecdf2..1f91bf1 100644 --- a/multirun.bzl +++ b/multirun.bzl @@ -9,10 +9,11 @@ load( "//internal:constants.bzl", "CommandInfo", "RUNFILES_PREFIX", - "rlocation_path", "update_attrs", ) +load("@aspect_bazel_lib//lib:paths.bzl", "to_rlocation_path") + _BinaryArgsEnvInfo = provider( fields = ["args", "env"], doc = "The arguments and environment to use when running the binary", @@ -45,92 +46,72 @@ _binary_args_env_aspect = aspect( implementation = _binary_args_env_aspect_impl, ) -def _multirun_impl(ctx): - instructions_file = ctx.actions.declare_file(ctx.label.name + ".json") - runner_info = ctx.attr._runner[DefaultInfo] - runner_exe = runner_info.files_to_run.executable +def _command_exe(command): + default_info = command[DefaultInfo] + if default_info.files_to_run == None: + fail("%s is not executable" % command.label, attr = "commands") + exe = default_info.files_to_run.executable + if exe == None: + fail("%s does not have an executable file" % command.label, attr = "commands") + return exe - runfiles = ctx.runfiles(files = [instructions_file, runner_exe]) - runfiles = runfiles.merge(ctx.attr._bash_runfiles[DefaultInfo].default_runfiles) - runfiles = runfiles.merge(runner_info.default_runfiles) - - for data_dep in ctx.attr.data: - default_runfiles = data_dep[DefaultInfo].default_runfiles - if default_runfiles != None: - runfiles = runfiles.merge(default_runfiles) +def _multirun_impl(ctx): + if ctx.attr.jobs < 0: + fail("'jobs' attribute should be at least 0") commands = [] - tagged_commands = [] - runfiles_files = [] + command_executables = [] for command in ctx.attr.commands: - tagged_commands.append(struct(tag = str(command.label), command = command)) - - for tag_command in tagged_commands: - command = tag_command.command - - default_info = command[DefaultInfo] - if default_info.files_to_run == None: - fail("%s is not executable" % command.label, attr = "commands") - exe = default_info.files_to_run.executable - if exe == None: - fail("%s does not have an executable file" % command.label, attr = "commands") - runfiles_files.append(exe) - - args = [] - env = {} - if _BinaryArgsEnvInfo in command: - args = command[_BinaryArgsEnvInfo].args - env = command[_BinaryArgsEnvInfo].env - - default_runfiles = default_info.default_runfiles - if default_runfiles != None: - runfiles = runfiles.merge(default_runfiles) + args = command[_BinaryArgsEnvInfo].args if _BinaryArgsEnvInfo in command else [] + env = command[_BinaryArgsEnvInfo].env if _BinaryArgsEnvInfo in command else {} + exe = _command_exe(command) if CommandInfo in command: tag = command[CommandInfo].description else: - tag = "Running {}".format(tag_command.tag) + tag = "Running {}".format(str(command.label)) commands.append(struct( tag = tag, - path = exe.short_path, + path = to_rlocation_path(ctx, exe), args = args, env = env, )) + command_executables.append(exe) - if ctx.attr.jobs < 0: - fail("'jobs' attribute should be at least 0") - - jobs = ctx.attr.jobs instructions = struct( commands = commands, - jobs = jobs, + jobs = ctx.attr.jobs, print_command = ctx.attr.print_command, keep_going = ctx.attr.keep_going, buffer_output = ctx.attr.buffer_output, - workspace_name = ctx.workspace_name, + verbose = ctx.attr.verbose, ) + instructions_file = ctx.actions.declare_file(ctx.label.name + ".json") ctx.actions.write( output = instructions_file, content = json.encode(instructions), ) - script = """\ -multirun_script="$(rlocation {})" -instructions="$(rlocation {})" -exec "$multirun_script" "$instructions" "$@" -""".format(shell.quote(rlocation_path(ctx, runner_exe)), shell.quote(rlocation_path(ctx, instructions_file))) - out_file = ctx.actions.declare_file(ctx.label.name + ".bash") - ctx.actions.write( - output = out_file, - content = RUNFILES_PREFIX + script, + # approach from https://github.com/bazelbuild/bazel-skylib/blob/main/rules/native_binary.bzl + runner_link = ctx.actions.declare_file(ctx.label.name + ".exe") + ctx.actions.symlink( + target_file = ctx.executable._runner, + output = runner_link, is_executable = True, ) + + runfiles = ctx.runfiles(files = command_executables + [instructions_file]) + runfiles = runfiles.merge_all([ + d[DefaultInfo].default_runfiles + for d in ctx.attr.commands + ctx.attr.data + [ctx.attr._runner] + ]) + return [ DefaultInfo( - files = depset([out_file]), - runfiles = runfiles.merge(ctx.runfiles(files = runfiles_files + ctx.files.data)), - executable = out_file, + files = depset([runner_link]), + runfiles = runfiles, + executable = runner_link, ), ] @@ -172,12 +153,16 @@ def multirun_with_transition(cfg, allowlist = None): default = False, doc = "Buffer the output of the commands and print it after each command has finished. Only for parallel execution.", ), + "verbose": attr.bool( + default = False, + doc = "Print some debugging information during the multirun process", + ), "_bash_runfiles": attr.label( default = Label("@bazel_tools//tools/bash/runfiles"), ), "_runner": attr.label( - default = Label("//internal:multirun"), - cfg = "exec", + default = Label("//internal/multirun:multirun"), + cfg = "target", executable = True, ), } @@ -185,6 +170,9 @@ def multirun_with_transition(cfg, allowlist = None): return rule( implementation = _multirun_impl, attrs = update_attrs(attrs, cfg, allowlist), + toolchains = [ + "@bazel_tools//tools/sh:toolchain_type", + ], executable = True, doc = """\ A multirun composes multiple command rules in order to run them in a single diff --git a/tests/test.sh b/tests/test.sh index a110122..fbd7886 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -2,8 +2,8 @@ set -euo pipefail -# --- begin runfiles.bash initialization v2 --- -# Copy-pasted from the Bazel Bash runfiles library v2. +# --- begin runfiles.bash initialization v3 --- +# Copy-pasted from the Bazel Bash runfiles library v3. set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash # shellcheck disable=SC1090 source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ @@ -12,57 +12,75 @@ source "${RUNFILES_DIR:-/dev/null}/$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; }; f=; set -e -# --- end runfiles.bash initialization v2 --- - -# PATH varies when running vs testing, this makes it more like running to validate the actual behavior. Specifically '.' is included for tests but not runs -export PATH=/usr/bin:/bin - -script=$(rlocation rules_multirun/tests/hello.bash) +# --- end runfiles.bash initialization v3 --- + +# without this, test cases silently pass if rlocation returns a missing script +check_rlocation() { + script=$(rlocation $1) + if [ ! -f "$script" ]; then + echo "Error: '$script' returned by rlocation($1) is invalid" >2 + exit 1 + fi + echo "$script" +} + +unameOut="$(uname -s)" +case "${unameOut}" in + Linux*) ext=bash;; + Darwin*) ext=bash;; + CYGWIN*) ext=bat;; + MINGW*) ext=bat;; + MSYS_NT*) ext=bat;; + *) ext=bash +esac + +script=$(check_rlocation _main/tests/hello.$ext) output=$($script) if [[ "$output" != "hello" ]]; then echo "Expected 'hello', got '$output'" exit 1 fi -script=$(rlocation rules_multirun/tests/validate_args_cmd.bash) +script=$(check_rlocation _main/tests/validate_args_cmd.$ext) $script -script=$(rlocation rules_multirun/tests/validate_chdir_location_cmd.bash) +script=$(check_rlocation _main/tests/validate_chdir_location_cmd.$ext) $script -script=$(rlocation rules_multirun/tests/validate_env_cmd.bash) +script=$(check_rlocation _main/tests/validate_env_cmd.$ext) $script -script=$(rlocation rules_multirun/tests/multirun_binary_args.bash) +script=$(check_rlocation _main/tests/multirun_binary_args.exe) $script -script=$(rlocation rules_multirun/tests/multirun_binary_env.bash) +script=$(check_rlocation _main/tests/multirun_binary_env.exe) $script -script=$(rlocation rules_multirun/tests/multirun_binary_args_location.bash) +script=$(check_rlocation _main/tests/multirun_binary_args_location.exe) $script -script="$(rlocation rules_multirun/tests/multirun_parallel.bash)" +script="$(check_rlocation _main/tests/multirun_parallel.exe)" parallel_output="$($script)" if [[ -n "$parallel_output" ]]; then echo "Expected no output, got '$parallel_output'" exit 1 fi -script="$(rlocation rules_multirun/tests/multirun_parallel_no_buffer.bash)" +script="$(check_rlocation _main/tests/multirun_parallel_no_buffer.exe)" parallel_output="$($script)" if [[ -n "$parallel_output" ]]; then echo "Expected no output, got '$parallel_output'" exit 1 fi -script="$(rlocation rules_multirun/tests/multirun_parallel_with_output.bash)" -parallel_output=$($script | sed 's=@[^/]*/=@/=g') -if [[ "$parallel_output" != "Running @//tests:echo_hello -hello -Running @//tests:echo_hello2 -hello2" ]]; then +script="$(check_rlocation _main/tests/multirun_parallel_with_output.exe)" +# commands are executing in parallel, we can't test any ordering. sort output +parallel_output=$($script | sed 's=@[^/]*/=@/=g' 2>&1 | sort -f) +if [[ "$parallel_output" != "hello +hello2 +Running @//tests:echo_hello +Running @//tests:echo_hello2" ]]; then echo "Expected output, got '$parallel_output'" exit 1 fi -script=$(rlocation rules_multirun/tests/multirun_serial.bash) +script=$(check_rlocation _main/tests/multirun_serial.exe) serial_output=$($script | sed 's=@[^/]*/=@/=g') if [[ "$serial_output" != "Running @//tests:validate_args_cmd Running @//tests:validate_env_cmd" ]]; then @@ -70,7 +88,7 @@ Running @//tests:validate_env_cmd" ]]; then exit 1 fi -script=$(rlocation rules_multirun/tests/multirun_serial_keep_going.bash) +script=$(check_rlocation _main/tests/multirun_serial_keep_going.exe) if serial_output=$($script | sed 's=@[^/]*/=@/=g'); then echo "Expected failure" >&2 exit 1 @@ -84,7 +102,7 @@ hello" ]]; then exit 1 fi -script=$(rlocation rules_multirun/tests/multirun_serial_description.bash) +script=$(check_rlocation _main/tests/multirun_serial_description.exe) serial_output=$($script | sed 's=@[^/]*/=@/=g') if [[ "$serial_output" != "some custom string Running @//tests:validate_env_cmd" ]]; then @@ -92,14 +110,14 @@ Running @//tests:validate_env_cmd" ]]; then exit 1 fi -script=$(rlocation rules_multirun/tests/multirun_serial_no_print.bash) +script=$(check_rlocation _main/tests/multirun_serial_no_print.exe) serial_no_output=$($script) if [[ -n "$serial_no_output" ]]; then echo "Expected no output, got '$serial_no_output'" exit 1 fi -script=$(rlocation rules_multirun/tests/multirun_with_transition.bash) +script=$(check_rlocation _main/tests/multirun_with_transition.exe) serial_with_transition_output=$($script | sed 's=@[^/]*/=@/=g') if [[ "$serial_with_transition_output" != "Running @//tests:validate_env_cmd Running @//tests:validate_args_cmd" ]]; then @@ -107,7 +125,7 @@ Running @//tests:validate_args_cmd" ]]; then exit 1 fi -script=$(rlocation rules_multirun/tests/root_multirun.bash) +script=$(check_rlocation _main/tests/root_multirun.exe) root_output=$($script) if [[ "$root_output" != "hello" ]]; then echo "Expected 'hello' from root, got '$root_output'"