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.
- project_sandbox/__init__.py +10 -0
- project_sandbox/__main__.py +3 -0
- project_sandbox/build_cache.py +78 -0
- project_sandbox/chroot.py +12 -0
- project_sandbox/cli.py +1680 -0
- project_sandbox/config_agents.py +577 -0
- project_sandbox/container_cli.py +481 -0
- project_sandbox/devcontainer.py +288 -0
- project_sandbox/dockerfile.py +826 -0
- project_sandbox/dockerfile_checksum.py +90 -0
- project_sandbox/firewall.py +75 -0
- project_sandbox/git_identity.py +19 -0
- project_sandbox/jj_workspace.py +330 -0
- project_sandbox/oauth_refresh.py +120 -0
- project_sandbox/ollama_network.py +232 -0
- project_sandbox/paths.py +62 -0
- project_sandbox/session.py +167 -0
- project_sandbox/templates/Dockerfile.j2 +137 -0
- project_sandbox/templates/_provision.sh.j2 +58 -0
- project_sandbox/templates/chroot-run.sh.j2 +59 -0
- project_sandbox/templates/devcontainer-entrypoint.sh.j2 +11 -0
- project_sandbox/templates/entrypoint.sh.j2 +179 -0
- project_sandbox/templates/init-firewall.sh.j2 +262 -0
- project_sandbox/templating.py +18 -0
- project_sandbox/token_expiry.py +149 -0
- project_sandbox/transcript.py +307 -0
- project_sandbox/worktree.py +174 -0
- project_sandbox-0.1.2.dist-info/METADATA +96 -0
- project_sandbox-0.1.2.dist-info/RECORD +32 -0
- project_sandbox-0.1.2.dist-info/WHEEL +4 -0
- project_sandbox-0.1.2.dist-info/entry_points.txt +2 -0
- project_sandbox-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|