Skip to content

Commit bd509fd

Browse files
committed
fix: Windows batch file line endings to avoid cmd parsing bug
Windows cmd has a known bug where GOTO/CALL to labels fails when batch files use LF-only line endings and the label crosses a 512-byte boundary during parsing. This causes "cannot find batch label" errors. References: - https://www.dostips.com/forum/viewtopic.php?t=8988 - rocq-prover/rocq#8610 This fix ensures all generated .bat files use CRLF line endings by: - Converting diff_test_tmpl.bat template to CRLF - Using \r\n in diff_test.bzl for Windows fail_msg substitutions - Converting BATCH_RLOCATION_FUNCTION to use \r\n - Converting create_windows_native_launcher_script output to CRLF - Converting write_source_file .bat output to CRLF This resolves test failures on Windows when generated batch files exceed ~4KB with LF-only line endings.
1 parent 49bf18c commit bd509fd

File tree

6 files changed

+226
-113
lines changed

6 files changed

+226
-113
lines changed

lib/private/diff_test.bzl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,21 @@ def _diff_test_impl(ctx):
6868
template = ctx.file._diff_test_tmpl_bat
6969
file1_path = to_rlocation_path(ctx, file1)
7070
file2_path = to_rlocation_path(ctx, file2)
71-
fail_msg = ["@echo" + (" " + line if line.strip() != "" else ".") for line in ctx.attr.failure_message[:-1].split("\n")]
71+
fail_msg_lines = ctx.attr.failure_message[:-1].split("\n")
72+
fail_msg = "\r\n".join(["@echo" + (" " + line if line.strip() != "" else ".") for line in fail_msg_lines])
7273
else:
7374
test_suffix = "-test.sh"
7475
template = ctx.file._diff_test_tmpl_sh
75-
fail_msg = ctx.attr.failure_message.split("\n")
76+
fail_msg = ctx.attr.failure_message
7677

7778
test_bin = ctx.actions.declare_file(ctx.label.name + test_suffix)
7879
ctx.actions.expand_template(
7980
template = template,
8081
output = test_bin,
8182
substitutions = {
82-
"{BATCH_RLOCATION_FUNCTION}": BATCH_RLOCATION_FUNCTION,
83+
"{BATCH_RLOCATION_FUNCTION}": BATCH_RLOCATION_FUNCTION.replace("\n", "\r\n"),
8384
"{name}": ctx.attr.name,
84-
"{fail_msg}": "\n".join(fail_msg),
85+
"{fail_msg}": fail_msg,
8586
"{file1}": file1_path,
8687
"{file2}": file2_path,
8788
"{file1_sub_path}": file1_sub_path,

lib/private/diff_test_tmpl.bat

Lines changed: 107 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,107 @@
1-
@rem @generated by @bazel_lib//lib/private:diff_test.bzl
2-
@echo off
3-
:: TODO: Add support for XML_OUTPUT_FILE like in diff_test_tmpl.sh
4-
SETLOCAL ENABLEEXTENSIONS
5-
SETLOCAL ENABLEDELAYEDEXPANSION
6-
set RUNFILES_MANIFEST_ONLY=1
7-
{BATCH_RLOCATION_FUNCTION}
8-
set MF=%RUNFILES_MANIFEST_FILE:/=\\%
9-
set PATH=%SYSTEMROOT%\\system32
10-
call :rlocation {file1} RF1
11-
call :rlocation {file2} RF2
12-
set RF1=!RF1:/=\!
13-
set RF2=!RF2:/=\!
14-
if "{file1_sub_path}" neq "" (
15-
set RF1=!RF1!\\{file1_sub_path}
16-
)
17-
if "{file2_sub_path}" neq "" (
18-
set RF2=!RF2!\\{file2_sub_path}
19-
)
20-
set DF1=0
21-
set DF2=0
22-
if exist "!RF1!\\*" (
23-
set DF1=1
24-
)
25-
if exist "!RF2!\\*" (
26-
set DF2=1
27-
)
28-
if %DF1% equ 1 (
29-
if %DF2% equ 0 (
30-
echo>&2 ERROR: Cannot compare directory "{file1}" and a file "{file2}"
31-
exit /b 1
32-
)
33-
)
34-
if %DF1% equ 0 (
35-
if %DF2% equ 1 (
36-
echo>&2 ERROR: Cannot compare file "{file1}" and a directory "{file2}"
37-
exit /b 1
38-
)
39-
)
40-
set DFX=0
41-
if %DF1% equ 1 (
42-
if %DF2% equ 1 (
43-
set DFX=1
44-
)
45-
)
46-
47-
if %DFX% equ 0 goto :compare_files
48-
:compare_directories
49-
for /f "delims=" %%F in (
50-
'echo "."^&forfiles /s /p "!RF1!" /m "*" /c "cmd /c echo @relpath"'
51-
) do (
52-
if not exist "!RF2!\\%%~F" (
53-
echo>&2 FAIL: file "%%~F" exists in "{file1}" and not in "{file2}".
54-
GOTO fail
55-
)
56-
if not exist "!RF1!\\%%~F\\*" (
57-
fc.exe "!RF1!\\%%~F" "!RF2!\\%%~F" 2>NUL 1>NUL
58-
if !ERRORLEVEL! neq 0 (
59-
if !ERRORLEVEL! equ 1 (
60-
echo>&2 FAIL: files "!RF1!\\%%~F" and "!RF2!\\%%~F" differ.
61-
set RF1=!RF1!\\%%~F
62-
set RF2=!RF2!\\%%~F
63-
GOTO fail
64-
) else (
65-
fc.exe "!RF1!\\%%~F" "!RF2!\\%%~F"
66-
GOTO fail
67-
)
68-
)
69-
)
70-
)
71-
for /f "delims=" %%F in (
72-
'echo "."^&forfiles /s /p "!RF2!" /m "*" /c "cmd /c echo @relpath"'
73-
) do (
74-
if not exist "!RF1!\\%%~F" (
75-
echo>&2 FAIL: file "%%~F" exists in "{file2}" and not in "{file1}".
76-
GOTO fail
77-
)
78-
)
79-
goto :success
80-
81-
:compare_files
82-
echo compare_files
83-
fc.exe "!RF1!" "!RF2!" 2>NUL 1>NUL
84-
set result=%ERRORLEVEL%
85-
if !result! neq 0 (
86-
if !result! equ 1 (
87-
echo>&2 FAIL: files "!RF1!" and "!RF2!" differ.
88-
goto :fail
89-
) else (
90-
echo fc.exe "!RF1!" "!RF2!"
91-
fc.exe "!RF1!" "!RF2!"
92-
set result=%ERRORLEVEL%
93-
exit /b !result!
94-
)
95-
) else (
96-
echo fc returned 0
97-
)
98-
:success
99-
exit /b 0
100-
101-
:fail
102-
{fail_msg}
103-
echo To see differences run:
104-
echo.
105-
echo diff "!RF1!" "!RF2!"
106-
echo.
107-
exit /b 1
1+
@rem @generated by @bazel_lib//lib/private:diff_test.bzl
2+
@echo off
3+
:: TODO: Add support for XML_OUTPUT_FILE like in diff_test_tmpl.sh
4+
SETLOCAL ENABLEEXTENSIONS
5+
SETLOCAL ENABLEDELAYEDEXPANSION
6+
set RUNFILES_MANIFEST_ONLY=1
7+
{BATCH_RLOCATION_FUNCTION}
8+
set MF=%RUNFILES_MANIFEST_FILE:/=\\%
9+
set PATH=%SYSTEMROOT%\\system32
10+
call :rlocation {file1} RF1
11+
call :rlocation {file2} RF2
12+
set RF1=!RF1:/=\!
13+
set RF2=!RF2:/=\!
14+
if "{file1_sub_path}" neq "" (
15+
set RF1=!RF1!\\{file1_sub_path}
16+
)
17+
if "{file2_sub_path}" neq "" (
18+
set RF2=!RF2!\\{file2_sub_path}
19+
)
20+
set DF1=0
21+
set DF2=0
22+
if exist "!RF1!\\*" (
23+
set DF1=1
24+
)
25+
if exist "!RF2!\\*" (
26+
set DF2=1
27+
)
28+
if %DF1% equ 1 (
29+
if %DF2% equ 0 (
30+
echo>&2 ERROR: Cannot compare directory "{file1}" and a file "{file2}"
31+
exit /b 1
32+
)
33+
)
34+
if %DF1% equ 0 (
35+
if %DF2% equ 1 (
36+
echo>&2 ERROR: Cannot compare file "{file1}" and a directory "{file2}"
37+
exit /b 1
38+
)
39+
)
40+
set DFX=0
41+
if %DF1% equ 1 (
42+
if %DF2% equ 1 (
43+
set DFX=1
44+
)
45+
)
46+
47+
if %DFX% equ 0 goto :compare_files
48+
:compare_directories
49+
for /f "delims=" %%F in (
50+
'echo "."^&forfiles /s /p "!RF1!" /m "*" /c "cmd /c echo @relpath"'
51+
) do (
52+
if not exist "!RF2!\\%%~F" (
53+
echo>&2 FAIL: file "%%~F" exists in "{file1}" and not in "{file2}".
54+
GOTO fail
55+
)
56+
if not exist "!RF1!\\%%~F\\*" (
57+
fc.exe "!RF1!\\%%~F" "!RF2!\\%%~F" 2>NUL 1>NUL
58+
if !ERRORLEVEL! neq 0 (
59+
if !ERRORLEVEL! equ 1 (
60+
echo>&2 FAIL: files "!RF1!\\%%~F" and "!RF2!\\%%~F" differ.
61+
set RF1=!RF1!\\%%~F
62+
set RF2=!RF2!\\%%~F
63+
GOTO fail
64+
) else (
65+
fc.exe "!RF1!\\%%~F" "!RF2!\\%%~F"
66+
GOTO fail
67+
)
68+
)
69+
)
70+
)
71+
for /f "delims=" %%F in (
72+
'echo "."^&forfiles /s /p "!RF2!" /m "*" /c "cmd /c echo @relpath"'
73+
) do (
74+
if not exist "!RF1!\\%%~F" (
75+
echo>&2 FAIL: file "%%~F" exists in "{file2}" and not in "{file1}".
76+
GOTO fail
77+
)
78+
)
79+
goto :success
80+
81+
:compare_files
82+
echo compare_files
83+
fc.exe "!RF1!" "!RF2!" 2>NUL 1>NUL
84+
set result=%ERRORLEVEL%
85+
if !result! neq 0 (
86+
if !result! equ 1 (
87+
echo>&2 FAIL: files "!RF1!" and "!RF2!" differ.
88+
goto :fail
89+
) else (
90+
echo fc.exe "!RF1!" "!RF2!"
91+
fc.exe "!RF1!" "!RF2!"
92+
set result=%ERRORLEVEL%
93+
exit /b !result!
94+
)
95+
) else (
96+
echo fc returned 0
97+
)
98+
:success
99+
exit /b 0
100+
101+
:fail
102+
{fail_msg}
103+
echo To see differences run:
104+
echo.
105+
echo diff "!RF1!" "!RF2!"
106+
echo.
107+
exit /b 1

lib/private/write_source_file.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ if !ERRORLEVEL! neq 0 (
390390
ctx.actions.write(
391391
output = updater,
392392
is_executable = True,
393-
content = "\n".join(contents),
393+
content = "\n".join(contents).replace("\n", "\r\n"),
394394
)
395395
return updater
396396

lib/tests/diff_test/BUILD.bazel

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
load("@bazel_lib//lib:diff_test.bzl", "diff_test")
2+
load("@bazel_skylib//rules:write_file.bzl", "write_file")
3+
load("@rules_shell//shell:sh_test.bzl", "sh_test")
4+
5+
# Create two simple files to compare
6+
write_file(
7+
name = "gen_file1",
8+
out = "file1.txt",
9+
content = ["line1", "line2"],
10+
)
11+
12+
write_file(
13+
name = "gen_file2",
14+
out = "file2.txt",
15+
content = ["line1", "line2"],
16+
)
17+
18+
# This diff_test will generate a .bat file on Windows
19+
diff_test(
20+
name = "simple_test",
21+
file1 = ":file1.txt",
22+
file2 = ":file2.txt",
23+
)
24+
25+
# Generate a test .bat file using the same code path as create_windows_native_launcher_script
26+
# This simulates how .bat files are generated and allows us to test their line endings
27+
genrule(
28+
name = "gen_test_bat",
29+
outs = ["test_script.bat"],
30+
cmd = select({
31+
"@platforms//os:windows": """
32+
echo @echo off > $@
33+
echo SETLOCAL ENABLEEXTENSIONS >> $@
34+
echo echo This is a test >> $@
35+
echo exit /b 0 >> $@
36+
""",
37+
"//conditions:default": "touch $@",
38+
}),
39+
)
40+
41+
# Test to verify that generated .bat files have CRLF line endings on Windows
42+
sh_test(
43+
name = "bat_line_endings_test",
44+
size = "small",
45+
srcs = ["verify_bat_crlf.sh"],
46+
args = ["$(rlocationpath :simple_test)"],
47+
data = [
48+
":simple_test",
49+
"@bazel_tools//tools/bash/runfiles",
50+
],
51+
target_compatible_with = select({
52+
"@platforms//os:windows": [],
53+
"//conditions:default": ["@platforms//:incompatible"],
54+
}),
55+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# This test verifies that .bat files generated by diff_test have CRLF line endings
5+
# This is critical on Windows due to a cmd.exe bug where labels aren't found
6+
# when files >~4KB have LF-only line endings.
7+
# See: https://www.dostips.com/forum/viewtopic.php?t=8988
8+
9+
# Load runfiles library to resolve paths
10+
# shellcheck disable=SC1090
11+
source "$RUNFILES_DIR/_main/external/bazel_tools/tools/bash/runfiles/runfiles.bash"
12+
13+
if [[ $# -lt 1 ]]; then
14+
echo "ERROR: Expected test file path as argument"
15+
exit 1
16+
fi
17+
18+
test_file="$1"
19+
20+
# On Windows, the test file will be a .bat file
21+
# On other platforms, it will be a .sh file and we skip this test
22+
if [[ "$test_file" != *.bat ]]; then
23+
echo "SKIP: Test file is not a .bat file: $test_file"
24+
exit 0
25+
fi
26+
27+
# Resolve the runfile path
28+
bat_file="$(rlocation "$test_file")"
29+
30+
if [[ ! -f "$bat_file" ]]; then
31+
echo "ERROR: Could not find $bat_file"
32+
exit 1
33+
fi
34+
35+
echo "Checking line endings in: $bat_file"
36+
37+
# Check if the file contains CRLF line endings
38+
# We use od to check for the presence of \r\n (0x0d 0x0a)
39+
if ! od -An -tx1 "$bat_file" | grep -q "0d 0a"; then
40+
echo "ERROR: $bat_file does not contain CRLF (\\r\\n) line endings"
41+
echo "File may have LF-only line endings, which causes cmd.exe parsing issues on Windows"
42+
exit 1
43+
fi
44+
45+
# Additionally verify that we don't have double CRLF (\r\r\n or \r\n\n)
46+
if od -An -tx1 "$bat_file" | grep -q "0d 0d 0a"; then
47+
echo "ERROR: $bat_file contains double carriage returns (\\r\\r\\n)"
48+
exit 1
49+
fi
50+
51+
if od -An -tx1 "$bat_file" | grep -q "0d 0a 0a"; then
52+
echo "ERROR: $bat_file contains CRLF followed by LF (\\r\\n\\n)"
53+
exit 1
54+
fi
55+
56+
echo "SUCCESS: $bat_file has correct CRLF line endings"
57+
exit 0

lib/windows_utils.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ if defined args (
104104
bash_bin = ctx.toolchains["@bazel_tools//tools/sh:toolchain_type"].path,
105105
sh_script = paths.to_rlocation_path(ctx, shell_script),
106106
rlocation_function = BATCH_RLOCATION_FUNCTION,
107-
),
107+
).replace("\n", "\r\n"),
108108
is_executable = True,
109109
)
110110
return win_launcher

0 commit comments

Comments
 (0)