diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py index 18dd13c..c3c3b74 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -1071,7 +1071,11 @@ async def _prepare_manifest_instance( dockerfile = compile_dockerfile(manifest) entrypoint = compile_entrypoint(manifest) - logger.info("Compiled Dockerfile for instance %s (%d chars)", instance.id, len(dockerfile)) + logger.info( + "Compiled Dockerfile for instance %s (%d chars)", + instance.id, + len(dockerfile), + ) logger.debug("Dockerfile content: %s", dockerfile) logger.info("Compiled entrypoint for instance %s", instance.id) logger.debug("Entrypoint content: %s", entrypoint) diff --git a/apps/api/src/services/docker_build.py b/apps/api/src/services/docker_build.py index 0dad9a9..11f9624 100644 --- a/apps/api/src/services/docker_build.py +++ b/apps/api/src/services/docker_build.py @@ -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, ]