ros2docker 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ros2docker/__init__.py +19 -0
- ros2docker/__main__.py +8 -0
- ros2docker/api.py +152 -0
- ros2docker/cli.py +115 -0
- ros2docker/commands.py +191 -0
- ros2docker/config.py +316 -0
- ros2docker/resources/build/Dockerfile +192 -0
- ros2docker/resources/build/entrypoint.sh +72 -0
- ros2docker/resources/examples/ros2docker.json +51 -0
- ros2docker/resources/schema/ros2docker.schema.json +118 -0
- ros2docker-0.1.1.dist-info/METADATA +152 -0
- ros2docker-0.1.1.dist-info/RECORD +16 -0
- ros2docker-0.1.1.dist-info/WHEEL +5 -0
- ros2docker-0.1.1.dist-info/entry_points.txt +2 -0
- ros2docker-0.1.1.dist-info/licenses/LICENSE +28 -0
- ros2docker-0.1.1.dist-info/top_level.txt +1 -0
ros2docker/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""ros2docker package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
_DISTRIBUTION_NAME = "ros2docker"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _package_version() -> str:
|
|
11
|
+
try:
|
|
12
|
+
return version(_DISTRIBUTION_NAME)
|
|
13
|
+
except PackageNotFoundError:
|
|
14
|
+
return "0+unknown"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__version__ = _package_version()
|
|
18
|
+
|
|
19
|
+
__all__ = ["__version__"]
|
ros2docker/__main__.py
ADDED
ros2docker/api.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Public Python API for ros2docker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from importlib import resources
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .commands import (
|
|
16
|
+
make_build_command,
|
|
17
|
+
make_exec_shell_command,
|
|
18
|
+
make_run_command,
|
|
19
|
+
make_stop_command,
|
|
20
|
+
)
|
|
21
|
+
from .config import get_config_dir, load_config
|
|
22
|
+
|
|
23
|
+
RunResult = list[str] | subprocess.CompletedProcess[bytes]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build(
|
|
27
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
28
|
+
override: str | Mapping[str, object] | None = None,
|
|
29
|
+
*,
|
|
30
|
+
dry_run: bool = False,
|
|
31
|
+
) -> RunResult:
|
|
32
|
+
with build_context(config_file, override) as context_dir:
|
|
33
|
+
command = make_build_command(config_file, override, context_dir=context_dir)
|
|
34
|
+
return _run(command, dry_run=dry_run)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(
|
|
38
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
39
|
+
override: str | Mapping[str, object] | None = None,
|
|
40
|
+
*,
|
|
41
|
+
mount: str | os.PathLike[str] | None = None,
|
|
42
|
+
extra_run_args: Sequence[str] | None = None,
|
|
43
|
+
dry_run: bool = False,
|
|
44
|
+
) -> RunResult:
|
|
45
|
+
command = make_run_command(
|
|
46
|
+
config_file,
|
|
47
|
+
override,
|
|
48
|
+
mount=mount,
|
|
49
|
+
extra_run_args=extra_run_args,
|
|
50
|
+
)
|
|
51
|
+
return _run(command, dry_run=dry_run, cwd=get_config_dir(config_file))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_run(
|
|
55
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
56
|
+
override: str | Mapping[str, object] | None = None,
|
|
57
|
+
*,
|
|
58
|
+
mount: str | os.PathLike[str] | None = None,
|
|
59
|
+
extra_run_args: Sequence[str] | None = None,
|
|
60
|
+
dry_run: bool = False,
|
|
61
|
+
) -> tuple[RunResult, RunResult]:
|
|
62
|
+
build_result = build(config_file, override, dry_run=dry_run)
|
|
63
|
+
run_result = run(
|
|
64
|
+
config_file,
|
|
65
|
+
override,
|
|
66
|
+
mount=mount,
|
|
67
|
+
extra_run_args=extra_run_args,
|
|
68
|
+
dry_run=dry_run,
|
|
69
|
+
)
|
|
70
|
+
return build_result, run_result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def stop(
|
|
74
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
75
|
+
override: str | Mapping[str, object] | None = None,
|
|
76
|
+
*,
|
|
77
|
+
dry_run: bool = False,
|
|
78
|
+
) -> RunResult:
|
|
79
|
+
command = make_stop_command(config_file, override)
|
|
80
|
+
return _run(command, dry_run=dry_run, check=False)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def exec_shell(
|
|
84
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
85
|
+
override: str | Mapping[str, object] | None = None,
|
|
86
|
+
*,
|
|
87
|
+
command: Sequence[str] | None = None,
|
|
88
|
+
interactive: bool = True,
|
|
89
|
+
dry_run: bool = False,
|
|
90
|
+
) -> RunResult:
|
|
91
|
+
docker_command = make_exec_shell_command(
|
|
92
|
+
config_file,
|
|
93
|
+
override,
|
|
94
|
+
command=command,
|
|
95
|
+
interactive=interactive,
|
|
96
|
+
)
|
|
97
|
+
return _run(docker_command, dry_run=dry_run)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@contextmanager
|
|
101
|
+
def build_context(
|
|
102
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
103
|
+
override: str | Mapping[str, object] | None = None,
|
|
104
|
+
) -> Iterator[Path]:
|
|
105
|
+
config = load_config(config_file, override, resolve_run_args=False)
|
|
106
|
+
with tempfile.TemporaryDirectory(prefix="ros2docker-build-") as temp_dir:
|
|
107
|
+
context_dir = Path(temp_dir)
|
|
108
|
+
_copy_build_resources(context_dir)
|
|
109
|
+
_stage_bake_packages(config, context_dir / "bake_packages")
|
|
110
|
+
yield context_dir
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _copy_build_resources(context_dir: Path) -> None:
|
|
114
|
+
build_resource = resources.files("ros2docker").joinpath("resources").joinpath("build")
|
|
115
|
+
for item in build_resource.iterdir():
|
|
116
|
+
destination = context_dir / item.name
|
|
117
|
+
with resources.as_file(item) as source:
|
|
118
|
+
if source.is_dir():
|
|
119
|
+
shutil.copytree(source, destination)
|
|
120
|
+
else:
|
|
121
|
+
shutil.copy2(source, destination)
|
|
122
|
+
(context_dir / "bake_packages").mkdir(exist_ok=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _stage_bake_packages(config: Mapping[str, object], bake_dir: Path) -> None:
|
|
126
|
+
seen_names: set[str] = set()
|
|
127
|
+
package_paths = config.get("bake_ros_packages", [])
|
|
128
|
+
if not isinstance(package_paths, list):
|
|
129
|
+
raise ValueError("'bake_ros_packages' must be a list.")
|
|
130
|
+
|
|
131
|
+
for package_path in package_paths:
|
|
132
|
+
if not isinstance(package_path, str | os.PathLike):
|
|
133
|
+
raise TypeError(f"bake package path must be path-like, got {type(package_path).__name__}.")
|
|
134
|
+
source = Path(package_path)
|
|
135
|
+
package_name = source.name
|
|
136
|
+
if package_name in seen_names:
|
|
137
|
+
raise ValueError(f"Duplicate bake package directory name: {package_name}")
|
|
138
|
+
seen_names.add(package_name)
|
|
139
|
+
shutil.copytree(source, bake_dir / package_name, symlinks=True)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _run(
|
|
143
|
+
command: Sequence[str],
|
|
144
|
+
*,
|
|
145
|
+
dry_run: bool,
|
|
146
|
+
cwd: str | os.PathLike[str] | None = None,
|
|
147
|
+
check: bool = True,
|
|
148
|
+
) -> RunResult:
|
|
149
|
+
print(shlex.join(list(command)), flush=True)
|
|
150
|
+
if dry_run:
|
|
151
|
+
return list(command)
|
|
152
|
+
return subprocess.run(command, check=check, cwd=cwd)
|
ros2docker/cli.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Command line interface for ros2docker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .api import build, build_run, exec_shell, run, stop
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
parser = _make_parser()
|
|
14
|
+
args = parser.parse_args(argv)
|
|
15
|
+
|
|
16
|
+
if not hasattr(args, "func"):
|
|
17
|
+
parser.print_help()
|
|
18
|
+
return 0
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
args.func(args)
|
|
22
|
+
except Exception as exc: # noqa: BLE001 - CLI should report concise user errors.
|
|
23
|
+
print(f"ros2docker: error: {exc}", file=sys.stderr)
|
|
24
|
+
return 1
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _make_parser() -> argparse.ArgumentParser:
|
|
29
|
+
parser = argparse.ArgumentParser(prog="ros2docker")
|
|
30
|
+
parser.add_argument("--version", action="version", version=f"ros2docker {__version__}")
|
|
31
|
+
subparsers = parser.add_subparsers(dest="command_name")
|
|
32
|
+
|
|
33
|
+
build_parser = subparsers.add_parser("build", help="Build the configured Docker image.")
|
|
34
|
+
_add_config_options(build_parser)
|
|
35
|
+
_add_dry_run(build_parser)
|
|
36
|
+
build_parser.set_defaults(func=_build)
|
|
37
|
+
|
|
38
|
+
run_parser = subparsers.add_parser("run", help="Build and run the configured Docker container.")
|
|
39
|
+
_add_config_options(run_parser)
|
|
40
|
+
_add_dry_run(run_parser)
|
|
41
|
+
run_parser.add_argument("-m", "--mount", metavar="PATH", help="Mount PATH into /ws.")
|
|
42
|
+
run_parser.add_argument("--no-build", action="store_true", help="Run without building first.")
|
|
43
|
+
run_parser.add_argument("extra_run_args", nargs=argparse.REMAINDER, help="Extra docker run args after --.")
|
|
44
|
+
run_parser.set_defaults(func=_run)
|
|
45
|
+
|
|
46
|
+
stop_parser = subparsers.add_parser("stop", help="Stop the configured Docker container.")
|
|
47
|
+
_add_config_options(stop_parser)
|
|
48
|
+
_add_dry_run(stop_parser)
|
|
49
|
+
stop_parser.set_defaults(func=_stop)
|
|
50
|
+
|
|
51
|
+
exec_parser = subparsers.add_parser("exec", help="Execute a command in the configured Docker container.")
|
|
52
|
+
_add_config_options(exec_parser)
|
|
53
|
+
_add_dry_run(exec_parser)
|
|
54
|
+
exec_parser.add_argument("command", nargs=argparse.REMAINDER, help="Command after --, defaults to bash.")
|
|
55
|
+
exec_parser.set_defaults(func=_exec)
|
|
56
|
+
|
|
57
|
+
return parser
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _add_config_options(parser: argparse.ArgumentParser) -> None:
|
|
61
|
+
parser.add_argument("-f", "--config", metavar="CONFIG", help="Path to ros2docker config.")
|
|
62
|
+
parser.add_argument("-o", "--override", metavar="JSON", help="JSON object overriding config values.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _add_dry_run(parser: argparse.ArgumentParser) -> None:
|
|
66
|
+
parser.add_argument("--dry-run", action="store_true", help="Print Docker argv without running Docker.")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _strip_separator(args: list[str]) -> list[str]:
|
|
70
|
+
if args and args[0] == "--":
|
|
71
|
+
return args[1:]
|
|
72
|
+
return args
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _build(args: argparse.Namespace) -> None:
|
|
76
|
+
build(args.config, args.override, dry_run=args.dry_run)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run(args: argparse.Namespace) -> None:
|
|
80
|
+
extra_run_args = _strip_separator(args.extra_run_args)
|
|
81
|
+
if args.no_build:
|
|
82
|
+
run(
|
|
83
|
+
args.config,
|
|
84
|
+
args.override,
|
|
85
|
+
mount=args.mount,
|
|
86
|
+
extra_run_args=extra_run_args,
|
|
87
|
+
dry_run=args.dry_run,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
build_run(
|
|
91
|
+
args.config,
|
|
92
|
+
args.override,
|
|
93
|
+
mount=args.mount,
|
|
94
|
+
extra_run_args=extra_run_args,
|
|
95
|
+
dry_run=args.dry_run,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _stop(args: argparse.Namespace) -> None:
|
|
100
|
+
stop(args.config, args.override, dry_run=args.dry_run)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _exec(args: argparse.Namespace) -> None:
|
|
104
|
+
command = _strip_separator(args.command)
|
|
105
|
+
exec_shell(
|
|
106
|
+
args.config,
|
|
107
|
+
args.override,
|
|
108
|
+
command=command or None,
|
|
109
|
+
interactive=not command,
|
|
110
|
+
dry_run=args.dry_run,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
raise SystemExit(main())
|
ros2docker/commands.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Docker command rendering for ros2docker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .config import ConfigError, get_config_dir, load_config, normalize_docker_host_paths, resolve_host_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def make_build_command(
|
|
14
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
15
|
+
override: str | Mapping[str, object] | None = None,
|
|
16
|
+
*,
|
|
17
|
+
context_dir: str | os.PathLike[str],
|
|
18
|
+
) -> list[str]:
|
|
19
|
+
config = load_config(config_file, override, resolve_run_args=False)
|
|
20
|
+
build_args = [
|
|
21
|
+
"-t",
|
|
22
|
+
_image_name(config),
|
|
23
|
+
"--build-arg",
|
|
24
|
+
f"USER_UID={os.getuid()}",
|
|
25
|
+
"--build-arg",
|
|
26
|
+
f"USER_GID={os.getgid()}",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
for key, value in config.get("build_args", {}).items():
|
|
30
|
+
build_args.extend(["--build-arg", f"{key}={value}"])
|
|
31
|
+
|
|
32
|
+
return ["docker", "build", *build_args, str(Path(context_dir))]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_run_command(
|
|
36
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
37
|
+
override: str | Mapping[str, object] | None = None,
|
|
38
|
+
*,
|
|
39
|
+
mount: str | os.PathLike[str] | None = None,
|
|
40
|
+
extra_run_args: Sequence[str] | None = None,
|
|
41
|
+
) -> list[str]:
|
|
42
|
+
config = load_config(config_file, override)
|
|
43
|
+
run_args = [
|
|
44
|
+
"docker",
|
|
45
|
+
"run",
|
|
46
|
+
*_core_run_args(config),
|
|
47
|
+
*_local_run_args(config),
|
|
48
|
+
*_workspace_mount_args(config_file, config, mount),
|
|
49
|
+
*normalize_docker_host_paths(extra_run_args or [], Path.cwd()),
|
|
50
|
+
*_run_type_args(config),
|
|
51
|
+
_image_name(config),
|
|
52
|
+
*_run_command(config),
|
|
53
|
+
]
|
|
54
|
+
return run_args
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_stop_command(
|
|
58
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
59
|
+
override: str | Mapping[str, object] | None = None,
|
|
60
|
+
) -> list[str]:
|
|
61
|
+
config = load_config(config_file, override, resolve_run_args=False)
|
|
62
|
+
return ["docker", "stop", _container_name(config)]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def make_exec_shell_command(
|
|
66
|
+
config_file: str | os.PathLike[str] | None = None,
|
|
67
|
+
override: str | Mapping[str, object] | None = None,
|
|
68
|
+
*,
|
|
69
|
+
command: Sequence[str] | None = None,
|
|
70
|
+
interactive: bool = True,
|
|
71
|
+
) -> list[str]:
|
|
72
|
+
config = load_config(config_file, override, resolve_run_args=False)
|
|
73
|
+
exec_args = ["docker", "exec"]
|
|
74
|
+
if interactive:
|
|
75
|
+
exec_args.append("-it")
|
|
76
|
+
exec_args.append(_container_name(config))
|
|
77
|
+
exec_args.extend(command or ["bash"])
|
|
78
|
+
return exec_args
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _image_name(config: Mapping[str, object]) -> str:
|
|
82
|
+
return str(config.get("image_name") or "ros2docker")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _container_name(config: Mapping[str, object]) -> str:
|
|
86
|
+
return str(config.get("container_name") or config.get("image_name") or "ros2docker")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _core_run_args(config: Mapping[str, object]) -> list[str]:
|
|
90
|
+
return [
|
|
91
|
+
"--name",
|
|
92
|
+
_container_name(config),
|
|
93
|
+
"--user",
|
|
94
|
+
f"{os.getuid()}:{os.getgid()}",
|
|
95
|
+
"--rm",
|
|
96
|
+
"-e",
|
|
97
|
+
"LIBGL_ALWAYS_SOFTWARE=1",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _local_run_args(config: Mapping[str, object]) -> list[str]:
|
|
102
|
+
args: list[str] = []
|
|
103
|
+
|
|
104
|
+
if config.get("enable_gui_forwarding"):
|
|
105
|
+
x11_socket = Path("/tmp/.X11-unix")
|
|
106
|
+
if not x11_socket.exists():
|
|
107
|
+
raise FileNotFoundError("GUI forwarding requested but /tmp/.X11-unix does not exist.")
|
|
108
|
+
args.extend(["-v", "/tmp/.X11-unix:/tmp/.X11-unix", "-e", "DISPLAY"])
|
|
109
|
+
|
|
110
|
+
if config.get("forward_ssh_agent"):
|
|
111
|
+
ssh_auth_sock = os.environ.get("SSH_AUTH_SOCK")
|
|
112
|
+
if not ssh_auth_sock:
|
|
113
|
+
raise ConfigError("forward_ssh_agent is true but SSH_AUTH_SOCK is not set.")
|
|
114
|
+
sock_path = Path(ssh_auth_sock)
|
|
115
|
+
if not sock_path.exists():
|
|
116
|
+
raise FileNotFoundError(f"forward_ssh_agent is true but SSH_AUTH_SOCK does not exist: {ssh_auth_sock}")
|
|
117
|
+
args.extend(["-e", "SSH_AUTH_SOCK", "-v", f"{ssh_auth_sock}:{ssh_auth_sock}"])
|
|
118
|
+
|
|
119
|
+
args.extend(_string_list(config, "run_args"))
|
|
120
|
+
args.extend(_string_list(config, "extra_run_args"))
|
|
121
|
+
return args
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _workspace_mount_args(
|
|
125
|
+
config_file: str | os.PathLike[str] | None,
|
|
126
|
+
config: Mapping[str, object],
|
|
127
|
+
mount: str | os.PathLike[str] | None,
|
|
128
|
+
) -> list[str]:
|
|
129
|
+
if mount is not None:
|
|
130
|
+
mount_path = resolve_host_path(os.fspath(mount), Path.cwd())
|
|
131
|
+
return ["-v", f"{mount_path}:/ws", "-w", "/ws"]
|
|
132
|
+
|
|
133
|
+
if config.get("mount_ws"):
|
|
134
|
+
ws_host = Path(get_config_dir(config_file)) / "ws"
|
|
135
|
+
if not ws_host.exists():
|
|
136
|
+
raise FileNotFoundError(f"mount_ws is true but workspace directory does not exist: {ws_host}")
|
|
137
|
+
return ["-v", f"{ws_host.resolve()}:/ws", "-w", "/ws"]
|
|
138
|
+
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _run_type_args(config: Mapping[str, object]) -> list[str]:
|
|
143
|
+
run_type = str(config.get("run_type") or "bash")
|
|
144
|
+
if run_type in {"bash", "catmux"}:
|
|
145
|
+
return ["-it"]
|
|
146
|
+
if run_type == "command":
|
|
147
|
+
return []
|
|
148
|
+
if run_type == "up":
|
|
149
|
+
return ["-d"]
|
|
150
|
+
raise ConfigError(f"Unsupported run_type: {run_type!r}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _run_command(config: Mapping[str, object]) -> list[str]:
|
|
154
|
+
run_type = str(config.get("run_type") or "bash")
|
|
155
|
+
|
|
156
|
+
if run_type == "catmux":
|
|
157
|
+
catmux_file = str(config["catmux_file"])
|
|
158
|
+
catmux_command = [
|
|
159
|
+
"catmux_create_session",
|
|
160
|
+
catmux_file,
|
|
161
|
+
"--session_name",
|
|
162
|
+
_container_name(config),
|
|
163
|
+
]
|
|
164
|
+
catmux_params = config.get("catmux_params")
|
|
165
|
+
if isinstance(catmux_params, Mapping) and catmux_params:
|
|
166
|
+
params = ",".join(f"{key}={value}" for key, value in catmux_params.items())
|
|
167
|
+
catmux_command.extend(["--overwrite", params])
|
|
168
|
+
return catmux_command
|
|
169
|
+
|
|
170
|
+
if run_type == "bash":
|
|
171
|
+
return ["bash"]
|
|
172
|
+
|
|
173
|
+
if run_type == "up":
|
|
174
|
+
return ["tail", "-f", "/dev/null"]
|
|
175
|
+
|
|
176
|
+
if run_type == "command":
|
|
177
|
+
raw_command = config["command"]
|
|
178
|
+
if isinstance(raw_command, str):
|
|
179
|
+
return shlex.split(raw_command)
|
|
180
|
+
if isinstance(raw_command, list):
|
|
181
|
+
return [str(part) for part in raw_command]
|
|
182
|
+
raise ConfigError("'command' must be a string or list.")
|
|
183
|
+
|
|
184
|
+
raise ConfigError(f"Unsupported run_type: {run_type!r}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _string_list(config: Mapping[str, object], key: str) -> list[str]:
|
|
188
|
+
value = config.get(key, [])
|
|
189
|
+
if not isinstance(value, list):
|
|
190
|
+
raise ConfigError(f"{key!r} must be a list.")
|
|
191
|
+
return [str(item) for item in value]
|