Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions concore_cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down
33 changes: 33 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down Expand Up @@ -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()
Expand Down
Loading