fix: normalise CRLF to LF in Docker build files

Docker's legacy builder treats \r as a literal character after a backslash
continuation, breaking RUN multi-line commands and producing
'unknown instruction: curl' errors.

Add defensive CRLF→LF normalisation for both Dockerfile and build context
files before writing. Also log hex representation of first 8 lines so we
can verify exactly what bytes Docker receives.
This commit is contained in:
Alex Blank
2026-05-28 22:22:05 +02:00
parent fba5e7c7be
commit 84f30b07c4
2 changed files with 34 additions and 9 deletions
+29 -8
View File
@@ -6,7 +6,9 @@ import subprocess
logger = logging.getLogger(__name__)
def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dict | None = None) -> tuple[int, str, str]:
def build_image(
instance_dir: str, dockerfile: str, tag: str, build_context: dict | None = None
) -> tuple[int, str, str]:
"""Build a Docker image from a Dockerfile.
Args:
@@ -20,12 +22,23 @@ def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dic
"""
from pathlib import Path
# Defensive: normalise any CRLF that may have crept in from manifest DB
# strings — Docker's legacy builder treats \r as a character after the
# backslash, breaking RUN continuations and producing
# "unknown instruction" errors.
dockerfile = dockerfile.replace("\r\n", "\n").replace("\r", "\n")
# Write Dockerfile
dockerfile_path = Path(instance_dir) / "Dockerfile"
dockerfile_path.write_text(dockerfile, newline="\n")
logger.info("Wrote Dockerfile to %s (%d bytes)", dockerfile_path, len(dockerfile))
logger.debug("Dockerfile content:\n%s", dockerfile)
# Hex-dump first 8 lines so we can see exactly what bytes Docker receives
lines_for_hex = dockerfile.split("\n")[:8]
for i, line in enumerate(lines_for_hex, start=1):
logger.info("Dockerfile line %d hex: %s", i, line.encode("utf-8").hex())
# Write build context files
if build_context:
for file_path, content in build_context.items():
@@ -34,19 +47,27 @@ def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dic
try:
full_path.resolve().relative_to(Path(instance_dir).resolve())
except ValueError:
logger.error("Build context file path escapes instance directory: %s", file_path)
raise ValueError(f"Build context file path '{file_path}' escapes instance directory")
logger.error(
"Build context file path escapes instance directory: %s", file_path
)
raise ValueError(
f"Build context file path '{file_path}' escapes instance directory"
)
full_path.parent.mkdir(parents=True, exist_ok=True)
full_path.write_text(content, newline="\n")
normalized = content.replace("\r\n", "\n").replace("\r", "\n")
full_path.write_text(normalized, newline="\n")
logger.debug("Wrote build context file: %s", full_path)
# Build image
logger.debug("Building Docker image with tag: %s", tag)
cmd = [
"docker", "build",
"-t", tag,
"-f", str(dockerfile_path),
"docker",
"build",
"-t",
tag,
"-f",
str(dockerfile_path),
instance_dir,
]