project-sandbox 0.1.2__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.
@@ -0,0 +1,10 @@
1
+ """project-sandbox package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ __all__ = ["__version__"]
6
+
7
+ try:
8
+ __version__ = version("project-sandbox")
9
+ except PackageNotFoundError:
10
+ __version__ = "unknown"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,78 @@
1
+ """Input fingerprinting so a matching image can be reused without rebuilding.
2
+
3
+ The CLI rebuilds the container image on every run by default. When the build
4
+ inputs have not changed and the image still exists, that work is wasted. This
5
+ module computes a deterministic fingerprint of the build inputs and records it,
6
+ alongside the image tag, in a sidecar JSON file under the generated
7
+ ``.project-sandbox`` directory. The caller compares the current fingerprint with
8
+ the recorded one (and confirms the image still exists) to decide whether the
9
+ build can be skipped.
10
+
11
+ Skipping is correctness-safe: it only happens on an exact fingerprint+tag match,
12
+ and any read/parse problem degrades to "not cached" so the build runs.
13
+ """
14
+
15
+ import hashlib
16
+ import json
17
+ from pathlib import Path
18
+
19
+ # Top-level files in the generated context that fully determine the CLI image.
20
+ # The CLI always builds context_dir/"Dockerfile"; entrypoint.sh, init-firewall.sh
21
+ # and project-sandbox-devcontainer-init are COPY'd into it.
22
+ _BUILD_INPUT_FILES = (
23
+ "Dockerfile",
24
+ "entrypoint.sh",
25
+ "init-firewall.sh",
26
+ "project-sandbox-devcontainer-init",
27
+ )
28
+
29
+ _STATE_FILENAME = ".build-state.json"
30
+
31
+
32
+ def state_path(context_dir: Path) -> Path:
33
+ return context_dir / _STATE_FILENAME
34
+
35
+
36
+ def compute_fingerprint(context_dir: Path, *, extra: dict[str, str]) -> str:
37
+ """Return a SHA256 over the build-input files plus the ``extra`` mapping.
38
+
39
+ ``extra`` carries inputs that are not files in ``context_dir`` (the resolved
40
+ base image and the image tag). Missing input files are simply skipped, so
41
+ the fingerprint still reflects which files are present.
42
+ """
43
+ h = hashlib.sha256()
44
+ for name in sorted(_BUILD_INPUT_FILES):
45
+ path = context_dir / name
46
+ if not path.is_file():
47
+ continue
48
+ h.update(name.encode("utf-8"))
49
+ h.update(b"\0")
50
+ h.update(hashlib.sha256(path.read_bytes()).hexdigest().encode("ascii"))
51
+ h.update(b"\0")
52
+ h.update(b"extra\0")
53
+ h.update(json.dumps(extra, sort_keys=True, separators=(",", ":")).encode("utf-8"))
54
+ return h.hexdigest()
55
+
56
+
57
+ def read_state(context_dir: Path) -> dict | None:
58
+ try:
59
+ data = json.loads(state_path(context_dir).read_text(encoding="utf-8"))
60
+ except (OSError, ValueError):
61
+ return None
62
+ return data if isinstance(data, dict) else None
63
+
64
+
65
+ def write_state(context_dir: Path, *, image_tag: str, fingerprint: str) -> None:
66
+ state_path(context_dir).write_text(
67
+ json.dumps({"image_tag": image_tag, "fingerprint": fingerprint}) + "\n",
68
+ encoding="utf-8",
69
+ )
70
+
71
+
72
+ def is_cache_valid(context_dir: Path, *, image_tag: str, fingerprint: str) -> bool:
73
+ state = read_state(context_dir)
74
+ return (
75
+ state is not None
76
+ and state.get("image_tag") == image_tag
77
+ and state.get("fingerprint") == fingerprint
78
+ )
@@ -0,0 +1,12 @@
1
+ from pathlib import Path
2
+
3
+ from . import templating
4
+
5
+
6
+ def render(context_dir: Path) -> Path:
7
+ out = context_dir / "chroot-run.sh"
8
+ out.write_text(
9
+ templating.get_template("chroot-run.sh.j2").render() + "\n", encoding="utf-8"
10
+ )
11
+ out.chmod(0o700)
12
+ return out