diff --git a/concore_cli/commands/build.py b/concore_cli/commands/build.py index 5865500..ee21470 100644 --- a/concore_cli/commands/build.py +++ b/concore_cli/commands/build.py @@ -2,6 +2,7 @@ import shlex import subprocess import sys +import shutil from pathlib import Path from rich.panel import Panel from rich.progress import Progress, SpinnerColumn, TextColumn @@ -88,26 +89,46 @@ def _write_docker_compose(output_path): if not services: return None - compose_lines = ["services:"] + compose_lines = [ + "networks:", + " concore-net:", + " driver: bridge", + "", + "services:", + ] + named_volumes = set() for index, service in enumerate(services, start=1): service_name = re.sub(r"[^A-Za-z0-9_.-]", "-", service["container_name"]).strip( "-." ) if not service_name: service_name = f"service-{index}" - elif not service_name[0].isalnum(): + elif not service_name[0].isalpha(): service_name = f"service-{service_name}" - compose_lines.append(f" {service_name}:") + compose_lines.append(f" {_yaml_quote(service_name)}:") compose_lines.append(f" image: {_yaml_quote(service['image'])}") compose_lines.append( f" container_name: {_yaml_quote(service['container_name'])}" ) + compose_lines.append(" restart: on-failure") + compose_lines.append(" networks:") + compose_lines.append(" - concore-net") + if service["volumes"]: compose_lines.append(" volumes:") for volume_spec in service["volumes"]: compose_lines.append(f" - {_yaml_quote(volume_spec)}") + part1 = volume_spec.split(":")[0] + if re.match(r"^[a-zA-Z0-9_-]+$", part1): + named_volumes.add(part1) + + if named_volumes: + compose_lines.append("") + compose_lines.append("volumes:") + for v in sorted(named_volumes): + compose_lines.append(f" {v}:") compose_lines.append("") compose_path = output_path / "docker-compose.yml" @@ -180,6 +201,35 @@ def build_workflow( progress.update(task, completed=True) + if exec_type == "docker": + req_src = Path.cwd() / "requirements.txt" + if not req_src.exists(): + req_src = source_path / "requirements.txt" + req_dest = output_path / "src" / "requirements.txt" + if req_src.exists() and (output_path / "src").exists(): + shutil.copy2(req_src, req_dest) + elif (output_path / "src").exists(): + req_dest.touch() + + # Append requirement copying to generated scripts robustly + for s_name in ["build", "build.bat"]: + s_path = output_path / s_name + if s_path.exists(): + content = s_path.read_text(encoding="utf-8") + lines = content.splitlines() + if s_name == "build": + insert_line = "cp ../src/requirements.txt ." + else: + insert_line = "copy ..\\src\\requirements.txt ." + + new_lines = [] + for line in lines: + if " build" in line and "-t " in line: + new_lines.append(insert_line) + new_lines.append(line) + + s_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8") + if result.stdout: console.print(result.stdout) diff --git a/tests/test_cli.py b/tests/test_cli.py index a36a66a..d47f9d0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -383,6 +383,10 @@ def test_build_command_docker_compose_single_node(self): self.assertIn("services:", compose_content) self.assertIn("container_name: 'N1'", compose_content) self.assertIn("image: 'docker-script'", compose_content) + self.assertIn("networks:", compose_content) + self.assertIn("concore-net:", compose_content) + self.assertIn("- concore-net", compose_content) + self.assertIn("restart: on-failure", compose_content) metadata = json.loads(Path("out/STUDY.json").read_text()) self.assertIn("docker-compose.yml", metadata["checksums"]) @@ -431,6 +435,35 @@ def test_build_command_docker_compose_multi_node(self): self.assertIn("container_name: 'C'", compose_content) self.assertIn("image: 'docker-common'", compose_content) + def test_build_command_docker_requirements_injection(self): + with self.runner.isolated_filesystem(temp_dir=self.temp_dir): + result = self.runner.invoke(cli, ["init", "test-project"]) + self.assertEqual(result.exit_code, 0) + + Path("requirements.txt").write_text("pandas==1.0.0") + + result = self.runner.invoke( + cli, + [ + "build", + "test-project/workflow.graphml", + "--source", + "test-project/src", + "--output", + "out", + "--type", + "docker", + ], + ) + self.assertEqual(result.exit_code, 0) + + req_path = Path("out/src/requirements.txt") + self.assertTrue(req_path.exists()) + self.assertEqual(req_path.read_text(), "pandas==1.0.0") + + build_script = Path("out/build").read_text() + self.assertIn("cp ../src/requirements.txt .", build_script) + def test_build_command_shared_source_specialization_merges_edge_params(self): with self.runner.isolated_filesystem(temp_dir=self.temp_dir): Path("src").mkdir()