rules_batch

Bazel rules for Microsoft Batch.

Setup

bazel_dep(name = "rules_batch", version = "{version}")

Overview

rules_batch provides Bazel rules for building and testing Microsoft Batch (.bat / .cmd) scripts. The rules handle runfiles propagation, dependency tracking, and runtime path resolution so that batch scripts integrate cleanly into Bazel builds.

The rule set consists of:

  • bat_binary -- declares an executable batch script.
  • bat_test -- declares a test batch script.
  • bat_library -- groups batch scripts and data for reuse as dependencies.

Example

load("@rules_batch//batch:bat_binary.bzl", "bat_binary")
load("@rules_batch//batch:bat_library.bzl", "bat_library")

bat_library(
    name = "helpers",
    srcs = ["helper.bat"],
)

bat_binary(
    name = "tool",
    srcs = ["main.bat"],
    deps = [":helpers"],
)

Runfiles

To resolve runfiles paths at runtime, add @rules_batch//batch/runfiles to your target's deps and use the runfiles preamble in your script:

call "%RLOCATION%" "workspace/path/to/file" RESULT_VAR

See the Runfiles page for the full preamble, repo mapping, and manifest discovery details.

Rules (rules_batch)

This repository provides Microsoft Batch rules for Bazel.

bat_binary

  • Purpose: Declares an executable batch script with a runfiles-aware launcher.
  • Sources: Exactly one .bat or .cmd in srcs (the entry script).
  • Runfiles: Merges deps, data, the entry script, and runfiles support from //batch/runfiles.

Runtime resolution uses the runfiles preamble to set the RLOCATION variable, then resolves paths via:

call "%RLOCATION%" "workspace/path/to/file" RESULT_VAR

bat_library

  • Purpose: Groups batch scripts (and optional data) for reuse. There is no link step; libraries only bundle files and propagate DefaultInfo runfiles.
  • Sources: Any number of .bat / .cmd files in srcs, plus optional data and deps on other libraries.

Depend on a library from bat_binary via deps so helper scripts appear next to the binary in the runfiles tree.

bat_library vs filegroup

UsePrefer
Reusable batch helpers depended on by bat_binary / bat_librarybat_library
Arbitrary files in runfiles without batch-specific meaningfilegroup and data on bat_binary

Both end up in runfiles once attached; the distinction is clarity and convention, not a separate mechanism.

Example

load("@rules_batch//batch:bat_binary.bzl", "bat_binary")
load("@rules_batch//batch:bat_library.bzl", "bat_library")

bat_library(
    name = "helpers",
    srcs = ["helper.bat"],
)

bat_binary(
    name = "tool",
    srcs = ["main.bat"],
    deps = [":helpers"],
)

Use the module name from your bazel_dep in place of @rules_batch if needed.

bat_binary rule.

Rules

bat_binary

load("@rules_batch//batch:bat_binary.bzl", "bat_binary")

bat_binary(name, deps, srcs, data)

Declares an executable batch script.

The user script is symlinked as the executable entry point. Dependencies declared via deps and data are merged into the runfiles tree.

To resolve runfiles at runtime, add a dependency on @rules_batch//batch/runfiles and use the runfiles preamble in your script.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsDependencies (e.g. bat_library) merged into the executable runfiles.List of labelsoptional[]
srcsThe batch script source file. Must be a singleton list.List of labelsoptional[]
dataData dependencies merged into the executable runfiles.List of labelsoptional[]

bat_test rule.

Rules

bat_test

load("@rules_batch//batch:bat_test.bzl", "bat_test")

bat_test(name, deps, srcs, data)

Declares a test batch script.

The user script is symlinked as the test entry point. Dependencies declared via deps and data are merged into the runfiles tree.

To resolve runfiles at runtime, add a dependency on @rules_batch//batch/runfiles and use the runfiles preamble in your script.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsDependencies (e.g. bat_library) merged into the executable runfiles.List of labelsoptional[]
srcsThe batch script source file. Must be a singleton list.List of labelsoptional[]
dataData dependencies merged into the executable runfiles.List of labelsoptional[]

bat_library rule implementation.

Rules

bat_library

load("@rules_batch//batch:bat_library.bzl", "bat_library")

bat_library(name, deps, srcs, data)

Groups batch scripts and optional data for use as dependencies of bat_binary or other bat_library targets. Batch has no link step; this rule only bundles files and propagates runfiles, similar in spirit to sh_library.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsOther batch libraries whose scripts and runfiles are merged in.List of labelsoptional[]
srcsBatch script sources in this library.List of labelsoptional[]
dataAdditional runfiles (any files or targets with runfiles).List of labelsoptional[]

Providers

rules_batch defines two marker providers for type-safe dependency graphs:

ProviderAdvertised byRequired by
BatInfobat_librarydeps of bat_binary, bat_test, bat_library
BatBinaryInfobat_binary, bat_test--

These providers carry no fields; they exist so that deps attributes can restrict the set of allowed targets to the appropriate rule types.

Load them from the public API:

load("@rules_batch//batch:bat_info.bzl", "BatInfo")
load("@rules_batch//batch:bat_binary_info.bzl", "BatBinaryInfo")

BatInfo provider definition.

Providers

BatInfo

load("@rules_batch//batch:bat_info.bzl", "BatInfo")

BatInfo()

A provider for batch library rules.

BatBinaryInfo provider definition.

Providers

BatBinaryInfo

load("@rules_batch//batch:bat_binary_info.bzl", "BatBinaryInfo")

BatBinaryInfo()

A provider for batch binary rules.

Runfiles

Utility for resolving Bazel runfiles paths at runtime in batch scripts.

Setup

Add the runfiles target to your bat_binary or bat_test dependencies:

bat_binary(
    name = "my_tool",
    srcs = ["my_tool.bat"],
    deps = ["@rules_batch//batch/runfiles"],
)

Preamble

Copy-paste this block at the top of your script (after setlocal enabledelayedexpansion) to locate runfiles.bat and set the RLOCATION variable:

@REM --- begin runfiles.bat initialization v1 ---
set "_rf=rules_batch/batch/runfiles/runfiles.bat"
set "RLOCATION="
if defined RUNFILES_DIR if exist "!RUNFILES_DIR!\!_rf:/=\!" set "RLOCATION=!RUNFILES_DIR!\!_rf:/=\!"
if not defined RLOCATION if exist "%~f0.runfiles\!_rf:/=\!" (
    set "RUNFILES_DIR=%~f0.runfiles"
    set "RLOCATION=!RUNFILES_DIR!\!_rf:/=\!"
)
if not defined RLOCATION (
    set "_rf_mf="
    if defined RUNFILES_MANIFEST_FILE if exist "!RUNFILES_MANIFEST_FILE!" set "_rf_mf=!RUNFILES_MANIFEST_FILE!"
    if not defined _rf_mf if defined RUNFILES_DIR (
        if exist "!RUNFILES_DIR!\MANIFEST" (set "_rf_mf=!RUNFILES_DIR!\MANIFEST") else if exist "!RUNFILES_DIR!_manifest" set "_rf_mf=!RUNFILES_DIR!_manifest"
    )
    if not defined _rf_mf for %%m in ("%~f0.runfiles\MANIFEST" "%~f0.runfiles_manifest" "%~f0.exe.runfiles_manifest") do if not defined _rf_mf if exist "%%~m" set "_rf_mf=%%~m"
    if defined _rf_mf (
        set "_rf_mf=!_rf_mf:/=\!"
        for /F "tokens=1,* usebackq" %%i in ("!_rf_mf!") do if not defined RLOCATION (
            set "_k=%%i"
            if "%%i" equ "!_rf!" (set "RLOCATION=%%j") else if "!_k:~-28!" equ "/batch/runfiles/runfiles.bat" set "RLOCATION=%%j"
        )
    )
    if defined _rf_mf set "RUNFILES_MANIFEST_FILE=!_rf_mf!"
    set "_rf_mf="
    set "_k="
)
if not defined RLOCATION (echo>&2 ERROR: cannot find !_rf! & exit /b 1)
set "_rf="
@REM --- end runfiles.bat initialization v1 ---

After the preamble, %RLOCATION% points to runfiles.bat and you can resolve runfiles:

call "%RLOCATION%" "my_workspace/data/config.txt" CONFIG_PATH
echo Config is at: %CONFIG_PATH%

The preamble tries three strategies in order:

  1. Directory lookup via RUNFILES_DIR -- fast path when the runfiles tree exists.
  2. Sibling .runfiles directory next to the script (%~f0.runfiles).
  3. Manifest scan -- reads the manifest file line-by-line to find the runfiles.bat entry. A suffix fallback matches any canonical repo prefix ending in /batch/runfiles/runfiles.bat to handle Bzlmod version suffixes.

Full example

@echo off
setlocal enableextensions enabledelayedexpansion

@REM --- begin runfiles.bat initialization v1 ---
set "_rf=rules_batch/batch/runfiles/runfiles.bat"
set "RLOCATION="
if defined RUNFILES_DIR if exist "!RUNFILES_DIR!\!_rf:/=\!" set "RLOCATION=!RUNFILES_DIR!\!_rf:/=\!"
if not defined RLOCATION if exist "%~f0.runfiles\!_rf:/=\!" (
    set "RUNFILES_DIR=%~f0.runfiles"
    set "RLOCATION=!RUNFILES_DIR!\!_rf:/=\!"
)
if not defined RLOCATION (
    set "_rf_mf="
    if defined RUNFILES_MANIFEST_FILE if exist "!RUNFILES_MANIFEST_FILE!" set "_rf_mf=!RUNFILES_MANIFEST_FILE!"
    if not defined _rf_mf if defined RUNFILES_DIR (
        if exist "!RUNFILES_DIR!\MANIFEST" (set "_rf_mf=!RUNFILES_DIR!\MANIFEST") else if exist "!RUNFILES_DIR!_manifest" set "_rf_mf=!RUNFILES_DIR!_manifest"
    )
    if not defined _rf_mf for %%m in ("%~f0.runfiles\MANIFEST" "%~f0.runfiles_manifest" "%~f0.exe.runfiles_manifest") do if not defined _rf_mf if exist "%%~m" set "_rf_mf=%%~m"
    if defined _rf_mf (
        set "_rf_mf=!_rf_mf:/=\!"
        for /F "tokens=1,* usebackq" %%i in ("!_rf_mf!") do if not defined RLOCATION (
            set "_k=%%i"
            if "%%i" equ "!_rf!" (set "RLOCATION=%%j") else if "!_k:~-28!" equ "/batch/runfiles/runfiles.bat" set "RLOCATION=%%j"
        )
    )
    if defined _rf_mf set "RUNFILES_MANIFEST_FILE=!_rf_mf!"
    set "_rf_mf="
    set "_k="
)
if not defined RLOCATION (echo>&2 ERROR: cannot find !_rf! & exit /b 1)
set "_rf="
@REM --- end runfiles.bat initialization v1 ---

call "%RLOCATION%" "my_workspace/data/config.txt" CONFIG_PATH
echo Config is at: %CONFIG_PATH%

Repo mapping

When Bazel provides a _repo_mapping file in the runfiles manifest, rlocation automatically translates the first path segment (the apparent repo name) to the canonical runfiles directory name before the manifest lookup. This supports both the standard format and the compact wildcard format introduced by --incompatible_compact_repo_mapping_manifest in Bazel 9.

The translation uses the source repository to determine which view of the repo mapping applies. By default, the main repository is assumed (empty source repo). An optional third argument can override this:

@REM Default: resolves using the main repo's mapping (most common case).
call "%RLOCATION%" "my_dep/data/file.txt" RESULT

@REM Explicit: resolves using +my_ext+my_repo's mapping.
call "%RLOCATION%" "my_dep/data/file.txt" RESULT "+my_ext+my_repo"

In practice, the third argument is almost never needed. Batch scripts live in the main repository in the vast majority of cases, and the default covers that. The explicit form exists for the rare case where a batch script in an external repository needs to resolve paths from its own repo mapping context.

Exporting environment variables

Before spawning child processes that depend on runfiles (e.g. tools built with rules_venv), call :runfiles_export_envvars to ensure both RUNFILES_DIR and RUNFILES_MANIFEST_FILE are set:

call :runfiles_export_envvars
if errorlevel 1 (
    echo>&2 ERROR: runfiles environment not available
    exit /b 1
)

The preamble already sets whichever variable it discovers during initialization (mirroring the shell init block). This function fills in the other variable so children see a consistent pair:

  • If only RUNFILES_DIR is set, derives RUNFILES_MANIFEST_FILE from RUNFILES_DIR\MANIFEST or RUNFILES_DIR_manifest.
  • If only RUNFILES_MANIFEST_FILE is set, strips the \MANIFEST or _manifest suffix to derive RUNFILES_DIR.
  • If both are already set and valid, no changes are made.
  • Returns exit /b 1 if neither variable points to a valid path.

This mirrors runfiles_export_envvars from rules_shell.

Inline mode

Instead of calling runfiles.bat externally, you can concatenate or paste its content into your script at build time. The :rlocation subroutine is then available as a local label call:

call :rlocation "my_workspace/data/config.txt" CONFIG_PATH
echo Config is at: !CONFIG_PATH!

When inlined, only the subroutine block (between goto :_rl_end and :_rl_end) is active. The standalone entry point at the bottom of the file is unreachable because the user's script never falls through to it. Intermediate variables use a _rl_ prefix to minimize collisions.