Skip to content

Commit 8a8fcd6

Browse files
authored
fix: Windows batch file line endings to avoid cmd parsing bug (#1222)
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 like, for `diff_test`: ``` The system cannot find the batch label specified - compare_files ``` 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 templates and using \r\n in string replacements throughout: - diff_test: template and substitutions, - write_source_file: batch updater scripts, - windows_utils: native launcher scripts. To verify these changes work correctly across platforms, we add a Go binary (`check_newlines`) that validates line endings: - on Windows: verifies scripts have proper CRLF line endings, - otherwise: verifies \n to \r\n replacements don't affect POSIX scripts. Note: we initially considered `sh_test` with PowerShell/batch wrappers, but encountered issues with interpreter dependencies, script portability, and symlink handling across platforms. A single Go binary invoked via `native_test` proved more reliable and maintainable, working consistently across all platforms (no new external dependencies either).
1 parent 8aff7dc commit 8a8fcd6

File tree

10 files changed

+237
-115
lines changed

10 files changed

+237
-115
lines changed

lib/private/diff_test.bzl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,20 @@ 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 = "\r\n".join(["@echo" + (" " + line if line.strip() != "" else ".") for line in ctx.attr.failure_message[:-1].splitlines()])
7272
else:
7373
test_suffix = "-test.sh"
7474
template = ctx.file._diff_test_tmpl_sh
75-
fail_msg = ctx.attr.failure_message.split("\n")
75+
fail_msg = ctx.attr.failure_message
7676

7777
test_bin = ctx.actions.declare_file(ctx.label.name + test_suffix)
7878
ctx.actions.expand_template(
7979
template = template,
8080
output = test_bin,
8181
substitutions = {
82-
"{BATCH_RLOCATION_FUNCTION}": BATCH_RLOCATION_FUNCTION,
82+
"{BATCH_RLOCATION_FUNCTION}": "\r\n".join(BATCH_RLOCATION_FUNCTION.splitlines()),
8383
"{name}": ctx.attr.name,
84-
"{fail_msg}": "\n".join(fail_msg),
84+
"{fail_msg}": fail_msg,
8585
"{file1}": file1_path,
8686
"{file2}": file2_path,
8787
"{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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,8 @@ if !ERRORLEVEL! neq 0 (
390390
ctx.actions.write(
391391
output = updater,
392392
is_executable = True,
393-
content = "\n".join(contents),
393+
# since `contents` already contains some \n, splitting lines first avoids bogus \r\r\n
394+
content = "\r\n".join([line for content in contents for line in content.splitlines()]),
394395
)
395396
return updater
396397

lib/tests/bats/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
load("@bazel_skylib//rules:native_binary.bzl", "native_test")
12
load("//lib:bats.bzl", "bats_test")
23

34
NOT_WINDOWS = select({
@@ -15,6 +16,14 @@ bats_test(
1516
target_compatible_with = NOT_WINDOWS,
1617
)
1718

19+
# Verify line endings in the launcher script generated by bats_test: CRLF-only on Windows, LF-only otherwise
20+
native_test(
21+
name = "script_newlines_test",
22+
src = "//lib/tests/check_newlines",
23+
args = ["$(rlocationpath :basic)"],
24+
data = [":basic"],
25+
)
26+
1827
bats_test(
1928
name = "env",
2029
size = "small",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_binary")
2+
3+
go_binary(
4+
name = "check_newlines",
5+
testonly = True,
6+
srcs = ["main.go"],
7+
visibility = ["//lib/tests:__subpackages__"],
8+
deps = ["@io_bazel_rules_go//go/runfiles"],
9+
)

lib/tests/check_newlines/main.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"runtime"
8+
9+
"github.com/bazelbuild/rules_go/go/runfiles"
10+
)
11+
12+
func main() {
13+
if len(os.Args) != 2 {
14+
fmt.Fprintln(os.Stderr, "Usage: check_newlines <script>")
15+
os.Exit(1)
16+
}
17+
18+
r, err := runfiles.New()
19+
if err != nil {
20+
fmt.Fprintf(os.Stderr, "Failed to initialize runfiles: %v\n", err)
21+
os.Exit(1)
22+
}
23+
24+
script, err := r.Rlocation(os.Args[1])
25+
if err != nil {
26+
fmt.Fprintf(os.Stderr, "Failed to locate %s in runfiles: %v\n", os.Args[1], err)
27+
os.Exit(1)
28+
}
29+
30+
content, err := os.ReadFile(script)
31+
if err != nil {
32+
fmt.Fprintf(os.Stderr, "Failed to read %s: %v\n", script, err)
33+
os.Exit(1)
34+
}
35+
36+
cr := bytes.Count(content, []byte("\r"))
37+
lf := bytes.Count(content, []byte("\n"))
38+
crlf := bytes.Count(content, []byte("\r\n"))
39+
if runtime.GOOS == "windows" {
40+
if cr != lf || cr != crlf {
41+
fmt.Fprintf(os.Stderr, "%s contains non-Windows line endings (\\r=%d, \\n=%d, \\r\\n=%d)\n", script, cr, lf, crlf)
42+
os.Exit(1)
43+
}
44+
} else if cr > 0 {
45+
fmt.Fprintf(os.Stderr, "%s contains Windows line endings (\\r=%d, \\n=%d, \\r\\n=%d)\n", script, cr, lf, crlf)
46+
os.Exit(1)
47+
}
48+
}

lib/tests/diff_test/BUILD.bazel

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
load("@bazel_lib//lib:diff_test.bzl", "diff_test")
2+
load("@bazel_skylib//rules:native_binary.bzl", "native_test")
3+
load("@bazel_skylib//rules:write_file.bzl", "write_file")
4+
5+
# Create two simple files to compare
6+
write_file(
7+
name = "gen_file1",
8+
out = "file1.txt",
9+
content = [
10+
"line1",
11+
"line2",
12+
],
13+
)
14+
15+
write_file(
16+
name = "gen_file2",
17+
out = "file2.txt",
18+
content = [
19+
"line1",
20+
"line2",
21+
],
22+
)
23+
24+
# Have diff_test generate a script
25+
diff_test(
26+
name = "gen_script",
27+
file1 = ":file1.txt",
28+
file2 = ":file2.txt",
29+
)
30+
31+
# Verify line endings in the script generated by diff_test: CRLF-only on Windows, LF-only otherwise
32+
native_test(
33+
name = "script_newlines_test",
34+
src = "//lib/tests/check_newlines",
35+
args = ["$(rlocationpath :gen_script)"],
36+
data = [":gen_script"],
37+
)

0 commit comments

Comments
 (0)