Skip to content

Commit bd20adc

Browse files
committed
Update config.py
1 parent c46c790 commit bd20adc

1 file changed

Lines changed: 88 additions & 43 deletions

File tree

pkg/src/controlman/file_gen/config.py

Lines changed: 88 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from __future__ import annotations as _annotations
22

3+
from typing import TYPE_CHECKING as _TYPE_CHECKING
34
from pathlib import Path as _Path
45
from xml.etree import ElementTree as _ElementTree
56
import copy as _copy
67
import os as _os
8+
import re as _re
9+
import shlex as _shlex
710

811
from loggerman import logger
912
import pyserials as _ps
@@ -13,6 +16,8 @@
1316
from controlman.file_gen import unit as _unit
1417
from controlman import const as _const
1518

19+
if _TYPE_CHECKING:
20+
from typing import Literal
1621

1722
class ConfigFileGenerator:
1823
def __init__(
@@ -98,6 +103,7 @@ def dynamic_file(self, key: str, file: dict, file_before: dict | None):
98103
file_info = {
99104
"type": DynamicFileType.CUSTOM,
100105
"subtype": (key, file.get("name", key)),
106+
"executable": file["type"] == "exec",
101107
}
102108
if file["status"] == "delete":
103109
file_info["path_before"] = file["path"]
@@ -214,18 +220,65 @@ def create_docker_compose():
214220
out.append(docker_compose_file)
215221
return
216222

217-
def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) -> str:
223+
def quote_shell_arguments(args: list[str] | None) -> list[str]:
224+
"""Quotes arguments safely for Bash while preserving special shell parameters."""
225+
return [
226+
f'"{arg}"' if unquoted_task_process_patterns.match(arg) else _shlex.quote(arg)
227+
for arg in (args or [])
228+
]
229+
230+
def resolve_task_settings(
231+
devcontainer: dict,
232+
typ: Literal["local", "global"],
233+
environment: dict | None = None
234+
):
235+
typ2 = "environment" if environment else "root"
236+
jsonpath = f"default.task_setting.{typ}.{typ2}"
237+
settings = self._data.get(jsonpath, {})
238+
settings_filled = _unit.fill_jinja_templates(
239+
templates=settings,
240+
jsonpath=jsonpath,
241+
env_vars={"devcontainer": devcontainer, "environment": environment or {}}
242+
)
243+
out = _copy.deepcopy(devcontainer.get("task_setting", {}).get(typ, {}).get(typ2, {}))
244+
_ps.update.recursive_update(
245+
source=out,
246+
addon=settings_filled,
247+
)
248+
return out
249+
250+
def create_task_function(
251+
task: dict,
252+
task_setting: dict,
253+
) -> str:
254+
def add_lines(content: str):
255+
lines.extend([(f"{indent}{line}" if line else "") for line in content.splitlines()])
256+
return
257+
218258
lines = [f"{task["alias"]}() {{"]
219259
indent = 4 * " "
220260
if "script" in task:
221-
lines.extend([f"{indent}{line}" for line in task["script"].strip().splitlines()])
261+
settings = task_setting.get("script", {})
262+
add_lines(settings.get("prepend", ""))
263+
add_lines(task["script"])
264+
add_lines(settings.get("append", ""))
222265
else:
223-
cmd_prefix = task_in_env_prefix.format(env_name=env_name).strip()
224-
cmd = f"{cmd_prefix} {" ".join(task["process"])}"
225-
lines.append(f"{indent}{cmd}")
266+
settings = task_setting.get("process", {})
267+
cmd = settings.get("prepend", [])
268+
cmd.extend(task["process"])
269+
cmd.extend(settings.get("append", []))
270+
cmd_quoted = quote_shell_arguments(cmd)
271+
add_lines(" ".join(cmd_quoted))
226272
lines.append("}")
227273
return "\n".join(lines)
228274

275+
unquoted_task_process_patterns = _re.compile(
276+
r'^\$(\d+|\*|@)$' # Matches $1, $2, ..., $@, $*
277+
r'|^\$\{[^}]+\}$' # Matches ${VAR}, ${ARRAY[@]}, ${1}, etc.
278+
r'|^\$\w+$' # Matches $VAR
279+
r'|^\$\(.+\)$' # Matches $(command)
280+
)
281+
229282
out = []
230283
docker_compose_data = self._data["devcontainer.docker-compose"]
231284
docker_compose_path = docker_compose_data["path"]
@@ -234,15 +287,6 @@ def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) ->
234287
for k, v in self._data.items() if k.startswith("devcontainer_")
235288
}
236289
create_docker_compose()
237-
env_dirname = self._data["devcontainer.containers.rel_path.environment"]
238-
apt_path = self._data["devcontainer.containers.rel_path.apt"]
239-
conda_path = self._data["devcontainer.containers.rel_path.conda"]
240-
tasks_path = self._data["devcontainer.containers.rel_path.tasks"]
241-
242-
env_dirname_before = self._data_before["devcontainer.containers.rel_path.environment"] or env_dirname
243-
apt_path_before = self._data_before["devcontainer.containers.rel_path.apt"] or apt_path
244-
conda_path_before = self._data_before["devcontainer.containers.rel_path.conda"] or conda_path
245-
tasks_path_before = self._data_before["devcontainer.containers.rel_path.tasks"] or tasks_path
246290

247291
for container_id, container in devcontainers.items():
248292
container_before = self._data_before.get(f"devcontainer_{container_id}", {})
@@ -255,8 +299,8 @@ def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) ->
255299
file_type="txt",
256300
content=container["dockerfile"],
257301
),
258-
path=f"{dir_path}/Dockerfile",
259-
path_before=f"{dir_path_before}/Dockerfile",
302+
path=f"{container["path"]["dockerfile"]}",
303+
path_before=f"{container_before.get("path", {}).get("dockerfile")}",
260304
)
261305
out.append(dockerfile)
262306
# devcontainer.json file
@@ -284,8 +328,8 @@ def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) ->
284328
file_type="txt",
285329
content=[pkg["spec"]["full"] for pkg in container["apt"].values()],
286330
) if container.get("apt") else None,
287-
path=f"{dir_path}/{env_dirname}/{apt_path}",
288-
path_before=f"{dir_path_before}/{env_dirname_before}/{apt_path_before}",
331+
path=f"{container["path"]["apt"]}",
332+
path_before=f"{container_before.get("path", {}).get("apt")}",
289333
)
290334
out.append(apt_file)
291335
# conda environment files
@@ -307,36 +351,37 @@ def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) ->
307351
)
308352
out.append(env_file)
309353
# bash task file
310-
tasks = []
354+
tasks = {"local": [], "global": []}
311355
for task in container.get("task", {}).values():
312-
tasks.append(
313-
create_task_function(
314-
task=task,
315-
env_name="base",
316-
task_in_env_prefix=container["task_in_env_prefix"]
317-
)
318-
)
319-
for environment in container.get("environment", {}).values():
320-
for task in environment.get("task", {}).values():
321-
tasks.append(
356+
for typ in tasks.keys():
357+
tasks[typ].append(
322358
create_task_function(
323359
task=task,
324-
env_name=environment["name"],
325-
task_in_env_prefix=container["task_in_env_prefix"]
360+
task_setting=resolve_task_settings(devcontainer=container, typ=typ),
326361
)
327362
)
328-
task_file = DynamicFile(
329-
type=DynamicFileType.DEVCONTAINER_TASK,
330-
subtype=(container_id, container.get("name", container_id)),
331-
content=_unit.create_dynamic_file(
332-
file_type="txt",
333-
content=tasks,
334-
content_item_separator="\n\n",
335-
) if tasks else None,
336-
path=f"{dir_path}/{tasks_path}",
337-
path_before=f"{dir_path_before}/{tasks_path_before}",
338-
)
339-
out.append(task_file)
363+
for environment in container.get("environment", {}).values():
364+
for task in environment.get("task", {}).values():
365+
for typ in tasks.keys():
366+
tasks[typ].append(
367+
create_task_function(
368+
task=task,
369+
task_setting=resolve_task_settings(devcontainer=container, typ=typ, environment=environment),
370+
)
371+
)
372+
for typ in tasks.keys():
373+
task_file = DynamicFile(
374+
type=DynamicFileType.DEVCONTAINER_TASK,
375+
subtype=(container_id, container.get("name", container_id)),
376+
content=_unit.create_dynamic_file(
377+
file_type="txt",
378+
content=tasks[typ],
379+
content_item_separator="\n\n",
380+
) if tasks[typ] else None,
381+
path=f"{container["path"][f"tasks_{typ}"]}",
382+
path_before=f"{container_before.get("path", {}).get(f"tasks_{typ}")}",
383+
)
384+
out.append(task_file)
340385
return out
341386

342387
def devcontainer_features(self) -> list[DynamicFile]:

0 commit comments

Comments
 (0)