docker-dsl 0.1.0__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.
- docker_dsl/__init__.py +10 -0
- docker_dsl/__main__.py +56 -0
- docker_dsl/_completions.sh +97 -0
- docker_dsl/apt.py +45 -0
- docker_dsl/builder.py +66 -0
- docker_dsl/builder.pyi +6730 -0
- docker_dsl/cli.py +9 -0
- docker_dsl/context.py +41 -0
- docker_dsl/core.py +51 -0
- docker_dsl/instructions.py +88 -0
- docker_dsl/mounts.py +39 -0
- docker_dsl/naming.py +28 -0
- docker_dsl/py.typed +0 -0
- docker_dsl/run.py +76 -0
- docker_dsl/stage.py +220 -0
- docker_dsl/state.py +36 -0
- docker_dsl/stubgen.py +217 -0
- docker_dsl-0.1.0.dist-info/METADATA +291 -0
- docker_dsl-0.1.0.dist-info/RECORD +22 -0
- docker_dsl-0.1.0.dist-info/WHEEL +4 -0
- docker_dsl-0.1.0.dist-info/entry_points.txt +3 -0
- docker_dsl-0.1.0.dist-info/licenses/LICENSE +21 -0
docker_dsl/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from docker_dsl.context import context, rendering
|
|
4
|
+
from docker_dsl.core import Dockerfile
|
|
5
|
+
from docker_dsl.stage import Stage
|
|
6
|
+
from docker_dsl.state import current_stage
|
|
7
|
+
|
|
8
|
+
# Load-bearing for the docs site: without it, great-docs walks every public
|
|
9
|
+
# symbol, including the ~4500 generated methods in builder.pyi.
|
|
10
|
+
__all__ = ["Dockerfile", "Stage", "context", "current_stage", "rendering"]
|
docker_dsl/__main__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from docker_dsl.context import Registry
|
|
11
|
+
from docker_dsl.core import Dockerfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Main:
|
|
15
|
+
@classmethod
|
|
16
|
+
def parse_bool(cls, value: str) -> bool:
|
|
17
|
+
match value.lower():
|
|
18
|
+
case "true" | "1" | "yes":
|
|
19
|
+
return True
|
|
20
|
+
case "false" | "0" | "no":
|
|
21
|
+
return False
|
|
22
|
+
raise argparse.ArgumentTypeError(f"expected true/false, got {value!r}")
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def type_for(cls, type_: type) -> Callable[[str], Any]:
|
|
26
|
+
return cls.parse_bool if type_ is bool else type_
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def run(cls, argv: list[str] | None = None) -> None:
|
|
30
|
+
args = sys.argv[1:] if argv is None else argv
|
|
31
|
+
usage = "usage: docker-dsl <module.path> [--<key>=<value> ...] [--out PATH]"
|
|
32
|
+
if args and args[0] in ("-h", "--help"):
|
|
33
|
+
print(usage)
|
|
34
|
+
return
|
|
35
|
+
if not args:
|
|
36
|
+
raise SystemExit(usage)
|
|
37
|
+
module_path, *rest = args
|
|
38
|
+
module = importlib.import_module(module_path)
|
|
39
|
+
schema = Registry.get(module)
|
|
40
|
+
|
|
41
|
+
parser = argparse.ArgumentParser(prog=f"python -m docker_dsl {module_path}")
|
|
42
|
+
parser.add_argument("--out", type=Path, default=None)
|
|
43
|
+
for name, type_ in schema.items():
|
|
44
|
+
parser.add_argument(f"--{name}", type=cls.type_for(type_), required=True)
|
|
45
|
+
|
|
46
|
+
namespace = parser.parse_args(rest)
|
|
47
|
+
out: Path | None = namespace.out
|
|
48
|
+
config_values = {name: getattr(namespace, name) for name in schema}
|
|
49
|
+
|
|
50
|
+
text = Dockerfile(module).render(path=out, **config_values)
|
|
51
|
+
if out is None:
|
|
52
|
+
sys.stdout.write(text)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
Main.run()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Invoked by stubgen.py via: bash -lic 'source _completions.sh <output.json>'
|
|
3
|
+
# Must run via -c so bash stays fully interactive for completion loading.
|
|
4
|
+
|
|
5
|
+
OUTPUT="${1:?usage: source _completions.sh <output.json>}"
|
|
6
|
+
|
|
7
|
+
# Collect all command names
|
|
8
|
+
mapfile -t ALL_CMDS < <(compgen -c | sort -u)
|
|
9
|
+
|
|
10
|
+
# Trigger lazy loading for common dev tools that might not be registered yet
|
|
11
|
+
PRIORITY_CMDS=(git make cmake curl tar docker uv pip npm cargo gcc g++ clang
|
|
12
|
+
python python3 ruby node rustc go java javac scp ssh rsync sed awk
|
|
13
|
+
find xargs sort uniq wc head tail cat less grep diff patch chmod chown
|
|
14
|
+
cp mv rm mkdir rmdir ln touch kill ps mount umount df du free top htop
|
|
15
|
+
systemctl journalctl apt dpkg yum dnf pacman brew port)
|
|
16
|
+
|
|
17
|
+
for cmd in "${PRIORITY_CMDS[@]}"; do
|
|
18
|
+
_completion_loader "$cmd" 2>/dev/null || true
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
# Collect all function-based completion specs
|
|
22
|
+
declare -A COMP_FUNCS
|
|
23
|
+
while IFS= read -r line; do
|
|
24
|
+
fn=$(echo "$line" | sed -n 's/.*-F \([^ ]*\).*/\1/p')
|
|
25
|
+
cmd=$(echo "$line" | awk '{print $NF}')
|
|
26
|
+
[ -n "$fn" ] && [ -n "$cmd" ] && COMP_FUNCS["$cmd"]="$fn"
|
|
27
|
+
done < <(complete -p 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
extract_completions() {
|
|
30
|
+
local cmd="$1" fn="$2"
|
|
31
|
+
|
|
32
|
+
COMPREPLY=()
|
|
33
|
+
COMP_WORDS=("$cmd" "")
|
|
34
|
+
COMP_CWORD=1
|
|
35
|
+
COMP_LINE="$cmd "
|
|
36
|
+
COMP_POINT=$((${#cmd} + 1))
|
|
37
|
+
"$fn" >/dev/null 2>&1 || true
|
|
38
|
+
|
|
39
|
+
local subs=()
|
|
40
|
+
local is_filesystem=true
|
|
41
|
+
for s in "${COMPREPLY[@]}"; do
|
|
42
|
+
s="${s%% }"
|
|
43
|
+
[ -z "$s" ] && continue
|
|
44
|
+
[[ "$s" == */* ]] && continue
|
|
45
|
+
[[ "$s" == *.* ]] && continue
|
|
46
|
+
[ ! -e "$s" ] && is_filesystem=false
|
|
47
|
+
subs+=("$s")
|
|
48
|
+
done
|
|
49
|
+
$is_filesystem && [ ${#subs[@]} -gt 0 ] && subs=()
|
|
50
|
+
|
|
51
|
+
COMPREPLY=()
|
|
52
|
+
COMP_WORDS=("$cmd" "--")
|
|
53
|
+
COMP_CWORD=1
|
|
54
|
+
COMP_LINE="$cmd --"
|
|
55
|
+
COMP_POINT=$((${#cmd} + 3))
|
|
56
|
+
"$fn" >/dev/null 2>&1 || true
|
|
57
|
+
|
|
58
|
+
local flags=()
|
|
59
|
+
for f in "${COMPREPLY[@]}"; do
|
|
60
|
+
f="${f%% }"
|
|
61
|
+
f="${f%%=}"
|
|
62
|
+
[[ "$f" == --* ]] || continue
|
|
63
|
+
flags+=("$f")
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
# Discard if too many subcommands (dictionary/manpage completion noise)
|
|
67
|
+
[ ${#subs[@]} -gt 500 ] && subs=()
|
|
68
|
+
[ ${#subs[@]} -eq 0 ] && [ ${#flags[@]} -eq 0 ] && return
|
|
69
|
+
|
|
70
|
+
local subs_json flags_json
|
|
71
|
+
subs_json=$(printf '%s\n' "${subs[@]}" | sort -u | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
72
|
+
flags_json=$(printf '%s\n' "${flags[@]}" | sort -u | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
73
|
+
|
|
74
|
+
printf '"%s": {"subcommands": %s, "flags": %s}' "$cmd" "$subs_json" "$flags_json"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
first=true
|
|
79
|
+
printf '{"commands": ['
|
|
80
|
+
for cmd in "${ALL_CMDS[@]}"; do
|
|
81
|
+
$first || printf ','
|
|
82
|
+
printf '"%s"' "$cmd"
|
|
83
|
+
first=false
|
|
84
|
+
done
|
|
85
|
+
printf '], "completions": {'
|
|
86
|
+
|
|
87
|
+
first=true
|
|
88
|
+
for cmd in "${!COMP_FUNCS[@]}"; do
|
|
89
|
+
result=$(extract_completions "$cmd" "${COMP_FUNCS[$cmd]}" 2>/dev/null </dev/null) || true
|
|
90
|
+
[ -z "$result" ] && continue
|
|
91
|
+
$first || printf ','
|
|
92
|
+
printf '%s' "$result"
|
|
93
|
+
first=false
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
printf '}}\n'
|
|
97
|
+
} > "$OUTPUT"
|
docker_dsl/apt.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from docker_dsl.run import ShellCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(kw_only=True)
|
|
11
|
+
class AptMixin:
|
|
12
|
+
apt_updated: bool = False
|
|
13
|
+
apt_dirty: bool = True
|
|
14
|
+
|
|
15
|
+
def append(self, command: ShellCommand) -> None: ...
|
|
16
|
+
|
|
17
|
+
def apt_install(self, *packages: str, fast: bool = False) -> None:
|
|
18
|
+
from docker_dsl.run import ShellCommand
|
|
19
|
+
|
|
20
|
+
if not self.apt_updated or self.apt_dirty:
|
|
21
|
+
self.append(ShellCommand(raw="apt-get update -y"))
|
|
22
|
+
self.apt_updated = True
|
|
23
|
+
self.apt_dirty = False
|
|
24
|
+
cmd = "apt-fast" if fast else "apt-get"
|
|
25
|
+
self.append(ShellCommand(raw=f"{cmd} install -y --no-install-recommends {' '.join(packages)}"))
|
|
26
|
+
|
|
27
|
+
def add_apt_ppa(self, ppa: str) -> None:
|
|
28
|
+
from docker_dsl.run import ShellCommand
|
|
29
|
+
|
|
30
|
+
self.append(ShellCommand(raw=f"add-apt-repository {ppa} -y"))
|
|
31
|
+
self.apt_dirty = True
|
|
32
|
+
|
|
33
|
+
def add_apt_repo(self, key_url: str, repo_url: str, *, name: str) -> None:
|
|
34
|
+
from docker_dsl.run import ShellCommand
|
|
35
|
+
|
|
36
|
+
self.append(
|
|
37
|
+
ShellCommand(raw=f"wget -O- {key_url} | gpg --dearmor | tee /usr/share/keyrings/{name}.gpg > /dev/null")
|
|
38
|
+
)
|
|
39
|
+
self.append(
|
|
40
|
+
ShellCommand(
|
|
41
|
+
raw=f'echo "deb [signed-by=/usr/share/keyrings/{name}.gpg] {repo_url}" '
|
|
42
|
+
f"| tee /etc/apt/sources.list.d/{name}.list"
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
self.apt_dirty = True
|
docker_dsl/builder.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import TYPE_CHECKING, Self
|
|
6
|
+
|
|
7
|
+
from docker_dsl.apt import AptMixin
|
|
8
|
+
from docker_dsl.run import CdScope, RedirectableCmd, ShellCommand
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from docker_dsl.run import CmdInvoker
|
|
12
|
+
from docker_dsl.stage import Stage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RunBuilder(AptMixin):
|
|
17
|
+
stage: Stage
|
|
18
|
+
commands: list[ShellCommand] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
def __enter__(self) -> Self:
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def __exit__(
|
|
24
|
+
self,
|
|
25
|
+
exc_type: type[BaseException] | None,
|
|
26
|
+
exc_val: BaseException | None,
|
|
27
|
+
exc_tb: TracebackType | None,
|
|
28
|
+
) -> None:
|
|
29
|
+
if exc_type is not None or not self.commands:
|
|
30
|
+
return
|
|
31
|
+
self.stage.emit_run(self.build_script())
|
|
32
|
+
|
|
33
|
+
def build_script(self) -> str:
|
|
34
|
+
return " \\\n && ".join(c.render() for c in self.commands)
|
|
35
|
+
|
|
36
|
+
def append(self, command: ShellCommand) -> None:
|
|
37
|
+
self.commands.append(command)
|
|
38
|
+
|
|
39
|
+
# The generated builder.pyi shadows this module for type checkers, so
|
|
40
|
+
# `Self` here is a distinct type from the stub's `RunBuilder` that
|
|
41
|
+
# CmdInvoker & co. are annotated against.
|
|
42
|
+
def __getattr__(self, name: str) -> CmdInvoker:
|
|
43
|
+
if name.startswith("_"):
|
|
44
|
+
raise AttributeError(name)
|
|
45
|
+
from docker_dsl.run import CmdInvoker
|
|
46
|
+
|
|
47
|
+
return CmdInvoker(self, name) # type: ignore
|
|
48
|
+
|
|
49
|
+
def __call__(self, raw: str, *, env: dict[str, str] | None = None) -> None:
|
|
50
|
+
self.append(ShellCommand(raw=raw, env=env or {}))
|
|
51
|
+
|
|
52
|
+
def echo(self, text: str) -> RedirectableCmd:
|
|
53
|
+
return RedirectableCmd(self, text) # type: ignore
|
|
54
|
+
|
|
55
|
+
def cd(self, path: str) -> CdScope:
|
|
56
|
+
return CdScope(self, path) # type: ignore
|
|
57
|
+
|
|
58
|
+
def curl_bash(self, url: str, *, args: tuple[str, ...] = ()) -> None:
|
|
59
|
+
tail = f" -- {' '.join(args)}" if args else ""
|
|
60
|
+
self.append(ShellCommand(raw=f"curl --proto '=https' --tlsv1.2 -sSf {url} | bash -s{tail}"))
|
|
61
|
+
|
|
62
|
+
def install(self, url: str, *, target: str = "/usr/local/bin", strip: int = 1) -> None:
|
|
63
|
+
self.append(ShellCommand(raw=f"curl -fL {url} | tar xz -C {target} --strip-components={strip}"))
|
|
64
|
+
|
|
65
|
+
def fetch_file(self, url: str, dest: str) -> None:
|
|
66
|
+
self.append(ShellCommand(raw=f"mkdir -p $(dirname {dest}) && curl -fsSL {url} -o {dest}"))
|