From be435ccfadf9ad56470aa4e69dbe4bc36d8530a3 Mon Sep 17 00:00:00 2001 From: florian-simvia Date: Fri, 11 Sep 2026 15:14:19 +0200 Subject: [PATCH] fix: mount shared dir symlinks when opening the solver GUI build_gui_command (docker) and the singularity branch of build_runtime_gui_command mounted only the runs dir, unlike their run counterparts. With mesh_mode = symlink (the default since 0.4.1), MESH and POST symlinks pointing outside RUNS were left dangling inside the container. --- CHANGELOG.md | 5 +++++ csauto/docker.py | 4 ++++ csauto/execution.py | 2 ++ tests/unit/test_docker.py | 35 ++++++++++++++++++++++++++++++++++- tests/unit/test_execution.py | 26 ++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db3ec79..e5d7b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Fixed +- Opening the solver GUI on a case whose shared dirs are symlinks (the default since `mesh_mode = "symlink"` became the default in 0.4.1) left those symlinks dangling inside the container: `build_gui_command` (docker) and the singularity branch of `build_runtime_gui_command` mounted only the runs dir, unlike their `run` counterparts which also bind the symlink targets. Both now bind them the same way, `MESH` read-only and `POST` writable + ## [0.5.0] - 2026-08-03 Add code_aster as a second supported solver: generate, run, and monitor finite-element campaigns alongside Code_Saturne. diff --git a/csauto/docker.py b/csauto/docker.py index fdaae5e..f9f6f5d 100644 --- a/csauto/docker.py +++ b/csauto/docker.py @@ -76,7 +76,11 @@ def build_gui_command( except ValueError: setup_rel = Path(setup_path.name) container_setup = f"{container_case}/{setup_rel.as_posix()}" + from .execution import shared_dir_symlink_mounts + cmd: list[str] = ["docker", "run", "-v", f"{runs_root}:{container_root}"] + for target, readonly in shared_dir_symlink_mounts(runs_root, adapter.shared_dir_names): + cmd.extend(["-v", f"{target}:{target}:ro" if readonly else f"{target}:{target}"]) if display: cmd.extend( [ diff --git a/csauto/execution.py b/csauto/execution.py index a8789d2..5fd97df 100644 --- a/csauto/execution.py +++ b/csauto/execution.py @@ -315,6 +315,8 @@ def build_runtime_gui_command( "--pwd", container_case, ] + for target, readonly in shared_dir_symlink_mounts(runs_root, adapter.shared_dir_names): + cmd.extend(["--bind", f"{target}:{target}:ro" if readonly else f"{target}:{target}"]) if os.environ.get("DISPLAY") and Path("/tmp/.X11-unix").exists(): cmd.extend(["--bind", "/tmp/.X11-unix:/tmp/.X11-unix"]) cmd.append(selection.singularity_image) diff --git a/tests/unit/test_docker.py b/tests/unit/test_docker.py index 6c25802..4dea8f2 100644 --- a/tests/unit/test_docker.py +++ b/tests/unit/test_docker.py @@ -2,7 +2,7 @@ from pathlib import Path -from csauto.docker import build_run_command, read_container_id +from csauto.docker import build_gui_command, build_run_command, read_container_id def test_build_run_command_without_display(monkeypatch, tmp_path: Path) -> None: @@ -71,3 +71,36 @@ def test_build_run_command_mounts_symlinked_shared_dirs(monkeypatch, tmp_path: P assert f"-v {mesh.resolve()}:{mesh.resolve()}:ro" in joined assert f"-v {post.resolve()}:{post.resolve()}" in joined assert f"{post.resolve()}:ro" not in joined + + +def test_build_gui_command_mounts_shared_dir_symlinks(monkeypatch, tmp_path: Path) -> None: + """The GUI must see the same shared dirs as a run, or symlinked meshes break inside the container.""" + monkeypatch.delenv("DISPLAY", raising=False) + runs_dir = tmp_path / "RUNS" + case_dir = runs_dir / "case0001" + (case_dir / "DATA").mkdir(parents=True) + (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8") + mesh = tmp_path / "study" / "MESH" + post = tmp_path / "study" / "POST" + mesh.mkdir(parents=True) + post.mkdir(parents=True) + (runs_dir / "MESH").symlink_to(mesh, target_is_directory=True) + (runs_dir / "POST").symlink_to(post, target_is_directory=True) + + cmd = build_gui_command(case_dir, "my_image") + + assert f"{mesh.resolve()}:{mesh.resolve()}:ro" in cmd + assert f"{post.resolve()}:{post.resolve()}" in cmd + + +def test_build_gui_command_adds_no_mount_without_symlinks(monkeypatch, tmp_path: Path) -> None: + monkeypatch.delenv("DISPLAY", raising=False) + runs_dir = tmp_path / "RUNS" + case_dir = runs_dir / "case0001" + (case_dir / "DATA").mkdir(parents=True) + (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8") + (runs_dir / "MESH").mkdir() + + cmd = build_gui_command(case_dir, "my_image") + + assert cmd.count("-v") == 1 diff --git a/tests/unit/test_execution.py b/tests/unit/test_execution.py index 0d1b8bd..706914a 100644 --- a/tests/unit/test_execution.py +++ b/tests/unit/test_execution.py @@ -309,3 +309,29 @@ def test_build_singularity_slurm_script_binds_symlinked_shared_dirs(tmp_path: Pa script = CodeSaturneAdapter().build_slurm_script(case_dir, nprocs=2, nt=1, selection=selection) assert f"--bind {mesh.resolve()}:{mesh.resolve()}:ro" in script + + +def test_build_runtime_gui_command_singularity_mounts_shared_dir_symlinks(tmp_path: Path, monkeypatch) -> None: + """The GUI must see the same shared dirs as a run, or symlinked meshes break inside the container.""" + monkeypatch.delenv("DISPLAY", raising=False) + runs_dir = tmp_path / "RUNS" + case_dir = runs_dir / "case0001" + (case_dir / "DATA").mkdir(parents=True) + (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8") + mesh = tmp_path / "study" / "MESH" + post = tmp_path / "study" / "POST" + mesh.mkdir(parents=True) + post.mkdir(parents=True) + (runs_dir / "MESH").symlink_to(mesh, target_is_directory=True) + (runs_dir / "POST").symlink_to(post, target_is_directory=True) + selection = RuntimeSelection( + runtime=RUNTIME_SINGULARITY, + docker_image="img", + singularity_bin="/usr/bin/apptainer", + singularity_image="/images/code_saturne.sif", + ) + + cmd = build_runtime_gui_command(case_dir, selection) + + assert f"{mesh.resolve()}:{mesh.resolve()}:ro" in cmd + assert f"{post.resolve()}:{post.resolve()}" in cmd