rocket-engine 0.0.2b1__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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Durable revisions, persistence, and rewind snapshots."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import uuid
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ...interfaces.paths import join_contained
|
|
9
|
+
from ..errors import ProjectError
|
|
10
|
+
|
|
11
|
+
from .shared import (
|
|
12
|
+
_EVENTS_FILENAME,
|
|
13
|
+
_LEDGER_FILENAME,
|
|
14
|
+
_LINKED_CARVEOUT,
|
|
15
|
+
_SNAPSHOTS_DIRNAME,
|
|
16
|
+
_atomic_write_bytes,
|
|
17
|
+
_atomic_write_text,
|
|
18
|
+
_dump_yaml,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class _SnapshotMixin:
|
|
22
|
+
"""Durable revisions and snapshots for ProjectService."""
|
|
23
|
+
|
|
24
|
+
def _read_ledger_file(self, path):
|
|
25
|
+
"""Parse one durable file; missing reads as None, corrupt refuses."""
|
|
26
|
+
try:
|
|
27
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
return None
|
|
30
|
+
except OSError as exc:
|
|
31
|
+
raise ProjectError(
|
|
32
|
+
"invalid_ledger", f"Cannot read ledger file {path}: {exc}."
|
|
33
|
+
) from exc
|
|
34
|
+
try:
|
|
35
|
+
data = self._yaml.load(text)
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
raise ProjectError(
|
|
38
|
+
"invalid_ledger",
|
|
39
|
+
f"Cannot parse ledger file {path}: {exc}.",
|
|
40
|
+
) from exc
|
|
41
|
+
if data is None:
|
|
42
|
+
raise ProjectError(
|
|
43
|
+
"invalid_ledger", f"Ledger file is empty: {path}."
|
|
44
|
+
)
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
def _load(self):
|
|
48
|
+
"""Refresh head + log from durable files (absent files read empty)."""
|
|
49
|
+
ledger = self._read_ledger_file(self._dir / _LEDGER_FILENAME)
|
|
50
|
+
if ledger is not None:
|
|
51
|
+
if not isinstance(ledger, dict):
|
|
52
|
+
raise ProjectError(
|
|
53
|
+
"invalid_ledger",
|
|
54
|
+
f"Ledger head must be a mapping: {self._dir}.",
|
|
55
|
+
)
|
|
56
|
+
revision = ledger.get("current_revision")
|
|
57
|
+
requests = ledger.get("requests")
|
|
58
|
+
if not isinstance(revision, str) or not revision:
|
|
59
|
+
raise ProjectError(
|
|
60
|
+
"invalid_ledger",
|
|
61
|
+
f"Ledger head carries no revision: {self._dir}.",
|
|
62
|
+
)
|
|
63
|
+
if not isinstance(requests, dict):
|
|
64
|
+
raise ProjectError(
|
|
65
|
+
"invalid_ledger",
|
|
66
|
+
f"Ledger head carries no request map: {self._dir}.",
|
|
67
|
+
)
|
|
68
|
+
for request_id, entry in requests.items():
|
|
69
|
+
if (
|
|
70
|
+
not isinstance(entry, dict)
|
|
71
|
+
or not isinstance(entry.get("command_text"), str)
|
|
72
|
+
or not isinstance(entry.get("ack"), dict)
|
|
73
|
+
):
|
|
74
|
+
raise ProjectError(
|
|
75
|
+
"invalid_ledger",
|
|
76
|
+
f"Ledger request is malformed: {request_id}.",
|
|
77
|
+
)
|
|
78
|
+
owner = ledger.get("project_id")
|
|
79
|
+
if owner is not None and owner != self._project_id:
|
|
80
|
+
raise ProjectError(
|
|
81
|
+
"invalid_ledger",
|
|
82
|
+
f"Ledger belongs to {owner}, not {self._project_id}.",
|
|
83
|
+
)
|
|
84
|
+
self._current_revision = revision
|
|
85
|
+
self._requests = {
|
|
86
|
+
request_id: (entry["command_text"], entry["ack"])
|
|
87
|
+
for request_id, entry in requests.items()
|
|
88
|
+
}
|
|
89
|
+
events = self._read_ledger_file(self._dir / _EVENTS_FILENAME)
|
|
90
|
+
if events is not None:
|
|
91
|
+
if not isinstance(events, list):
|
|
92
|
+
raise ProjectError(
|
|
93
|
+
"invalid_ledger",
|
|
94
|
+
f"Event log must be a sequence: {self._dir}.",
|
|
95
|
+
)
|
|
96
|
+
for entry in events:
|
|
97
|
+
if (
|
|
98
|
+
not isinstance(entry, dict)
|
|
99
|
+
or not isinstance(entry.get("seq"), int)
|
|
100
|
+
or isinstance(entry.get("seq"), bool)
|
|
101
|
+
or not isinstance(entry.get("event"), str)
|
|
102
|
+
):
|
|
103
|
+
raise ProjectError(
|
|
104
|
+
"invalid_ledger",
|
|
105
|
+
f"Event record is malformed in {self._dir}.",
|
|
106
|
+
)
|
|
107
|
+
self._events = events
|
|
108
|
+
self._trim()
|
|
109
|
+
|
|
110
|
+
def _persist(self):
|
|
111
|
+
"""Rewrite both durable files; events first, then the head."""
|
|
112
|
+
try:
|
|
113
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
(self._dir / "runs").mkdir(parents=True, exist_ok=True)
|
|
115
|
+
except OSError as exc:
|
|
116
|
+
raise ProjectError(
|
|
117
|
+
"invalid_ledger",
|
|
118
|
+
f"Cannot create ledger dir {self._dir}: {exc}.",
|
|
119
|
+
) from exc
|
|
120
|
+
try:
|
|
121
|
+
_atomic_write_text(
|
|
122
|
+
self._dir / _EVENTS_FILENAME, _dump_yaml(self._events)
|
|
123
|
+
)
|
|
124
|
+
_atomic_write_text(
|
|
125
|
+
self._dir / _LEDGER_FILENAME,
|
|
126
|
+
_dump_yaml(
|
|
127
|
+
{
|
|
128
|
+
"project_id": self._project_id,
|
|
129
|
+
"current_revision": self._current_revision,
|
|
130
|
+
"requests": {
|
|
131
|
+
request_id: {
|
|
132
|
+
"command_text": command_text,
|
|
133
|
+
"ack": ack,
|
|
134
|
+
}
|
|
135
|
+
for request_id, (command_text, ack)
|
|
136
|
+
in self._requests.items()
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
except OSError as exc:
|
|
142
|
+
raise ProjectError(
|
|
143
|
+
"invalid_ledger",
|
|
144
|
+
f"Cannot persist ledger under {self._dir}: {exc}.",
|
|
145
|
+
) from exc
|
|
146
|
+
|
|
147
|
+
def _refresh_durable(self):
|
|
148
|
+
if self._dir is not None:
|
|
149
|
+
self._load()
|
|
150
|
+
|
|
151
|
+
def _persist_after_mutation(self):
|
|
152
|
+
if self._dir is not None:
|
|
153
|
+
self._persist()
|
|
154
|
+
|
|
155
|
+
def _snapshot_exists(self, revision):
|
|
156
|
+
"""True when a restorable snapshot exists for a revision."""
|
|
157
|
+
if self._dir is None:
|
|
158
|
+
return revision in self._snapshots
|
|
159
|
+
try:
|
|
160
|
+
path = join_contained(
|
|
161
|
+
self._dir, _SNAPSHOTS_DIRNAME, revision
|
|
162
|
+
)
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
return path.is_dir()
|
|
166
|
+
|
|
167
|
+
def _capture_snapshot(self, revision):
|
|
168
|
+
"""Store the current managed bytes under one revision id.
|
|
169
|
+
|
|
170
|
+
Overwrites any previous capture for that id; empty trees
|
|
171
|
+
leave an empty marker dir so the revision stays restorable.
|
|
172
|
+
The copy lands in a sibling temp dir and renames into
|
|
173
|
+
place, so a crash never leaves a partial snapshot behind.
|
|
174
|
+
"""
|
|
175
|
+
if self._dir is None:
|
|
176
|
+
self._snapshots[revision] = dict(self._docs)
|
|
177
|
+
return
|
|
178
|
+
try:
|
|
179
|
+
dest = join_contained(
|
|
180
|
+
self._dir, _SNAPSHOTS_DIRNAME, revision
|
|
181
|
+
)
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
raise ProjectError(
|
|
184
|
+
"invalid_ledger",
|
|
185
|
+
f"Cannot snapshot revision {revision}: {exc}.",
|
|
186
|
+
) from exc
|
|
187
|
+
base = self._dir / _SNAPSHOTS_DIRNAME
|
|
188
|
+
try:
|
|
189
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
except OSError as exc:
|
|
191
|
+
raise ProjectError(
|
|
192
|
+
"invalid_ledger",
|
|
193
|
+
f"Cannot snapshot revision {revision}: {exc}.",
|
|
194
|
+
) from exc
|
|
195
|
+
for stale in base.glob(".cap.*"):
|
|
196
|
+
try:
|
|
197
|
+
if stale.is_symlink() or stale.is_file():
|
|
198
|
+
stale.unlink()
|
|
199
|
+
elif stale.is_dir():
|
|
200
|
+
shutil.rmtree(stale)
|
|
201
|
+
except OSError:
|
|
202
|
+
pass
|
|
203
|
+
tmp = base / f".cap.{uuid.uuid4().hex}.tmp"
|
|
204
|
+
try:
|
|
205
|
+
tmp.mkdir(parents=True, exist_ok=False)
|
|
206
|
+
except OSError as exc:
|
|
207
|
+
raise ProjectError(
|
|
208
|
+
"invalid_ledger",
|
|
209
|
+
f"Cannot snapshot revision {revision}: {exc}.",
|
|
210
|
+
) from exc
|
|
211
|
+
try:
|
|
212
|
+
for rel, _size in self._iter_doc_files():
|
|
213
|
+
try:
|
|
214
|
+
data = self._doc_path(rel).read_bytes()
|
|
215
|
+
except OSError as exc:
|
|
216
|
+
raise ProjectError(
|
|
217
|
+
"invalid_ledger",
|
|
218
|
+
f"Cannot snapshot {rel}: {exc}.",
|
|
219
|
+
) from exc
|
|
220
|
+
target = tmp / rel
|
|
221
|
+
try:
|
|
222
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
_atomic_write_bytes(target, data)
|
|
224
|
+
except OSError as exc:
|
|
225
|
+
raise ProjectError(
|
|
226
|
+
"invalid_ledger",
|
|
227
|
+
f"Cannot snapshot {rel}: {exc}.",
|
|
228
|
+
) from exc
|
|
229
|
+
if dest.is_symlink() or dest.is_file():
|
|
230
|
+
dest.unlink()
|
|
231
|
+
elif dest.is_dir():
|
|
232
|
+
shutil.rmtree(dest)
|
|
233
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
os.replace(tmp, dest)
|
|
235
|
+
except ProjectError:
|
|
236
|
+
try:
|
|
237
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
238
|
+
except OSError:
|
|
239
|
+
pass
|
|
240
|
+
raise
|
|
241
|
+
except OSError as exc:
|
|
242
|
+
try:
|
|
243
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
244
|
+
except OSError:
|
|
245
|
+
pass
|
|
246
|
+
raise ProjectError(
|
|
247
|
+
"invalid_ledger",
|
|
248
|
+
f"Cannot snapshot revision {revision}: {exc}.",
|
|
249
|
+
) from exc
|
|
250
|
+
|
|
251
|
+
def _ensure_snapshot(self, revision):
|
|
252
|
+
if not self._snapshot_exists(revision):
|
|
253
|
+
self._capture_snapshot(revision)
|
|
254
|
+
|
|
255
|
+
def _load_snapshot(self, revision):
|
|
256
|
+
"""Current snapshot bytes, or None when unrestorable."""
|
|
257
|
+
if self._dir is None:
|
|
258
|
+
snap = self._snapshots.get(revision)
|
|
259
|
+
return dict(snap) if snap is not None else None
|
|
260
|
+
try:
|
|
261
|
+
src_dir = join_contained(
|
|
262
|
+
self._dir, _SNAPSHOTS_DIRNAME, revision
|
|
263
|
+
)
|
|
264
|
+
except Exception:
|
|
265
|
+
return None
|
|
266
|
+
if not src_dir.is_dir():
|
|
267
|
+
return None
|
|
268
|
+
found = {}
|
|
269
|
+
for target in sorted(src_dir.rglob("*")):
|
|
270
|
+
if not target.is_file():
|
|
271
|
+
continue
|
|
272
|
+
try:
|
|
273
|
+
rel = target.relative_to(src_dir).as_posix()
|
|
274
|
+
except ValueError:
|
|
275
|
+
continue
|
|
276
|
+
try:
|
|
277
|
+
found[rel] = target.read_bytes()
|
|
278
|
+
except OSError as exc:
|
|
279
|
+
raise ProjectError(
|
|
280
|
+
"invalid_ledger",
|
|
281
|
+
f"Cannot read snapshot {revision}/{rel}: {exc}.",
|
|
282
|
+
) from exc
|
|
283
|
+
return found
|
|
284
|
+
|
|
285
|
+
def _current_bytes(self):
|
|
286
|
+
"""All managed bytes for rollback staging."""
|
|
287
|
+
found = {}
|
|
288
|
+
for rel, _size in self._iter_doc_files():
|
|
289
|
+
try:
|
|
290
|
+
found[rel] = self._doc_path(rel).read_bytes()
|
|
291
|
+
except OSError as exc:
|
|
292
|
+
raise ProjectError(
|
|
293
|
+
"invalid_ledger",
|
|
294
|
+
f"Cannot stage {rel}: {exc}.",
|
|
295
|
+
) from exc
|
|
296
|
+
return found
|
|
297
|
+
|
|
298
|
+
def _clear_docs(self):
|
|
299
|
+
"""Delete every managed file, then prune emptied doc dirs."""
|
|
300
|
+
for rel, _size in self._iter_doc_files():
|
|
301
|
+
target = self._doc_path(rel)
|
|
302
|
+
try:
|
|
303
|
+
target.unlink()
|
|
304
|
+
except FileNotFoundError:
|
|
305
|
+
pass
|
|
306
|
+
except OSError as exc:
|
|
307
|
+
raise ProjectError(
|
|
308
|
+
"invalid_ledger",
|
|
309
|
+
f"Cannot clear {rel}: {exc}.",
|
|
310
|
+
) from exc
|
|
311
|
+
for target in sorted(self._dir.rglob("*"), reverse=True):
|
|
312
|
+
if not target.is_dir() or target == self._dir:
|
|
313
|
+
continue
|
|
314
|
+
try:
|
|
315
|
+
rel = target.relative_to(self._dir).as_posix()
|
|
316
|
+
except ValueError:
|
|
317
|
+
continue
|
|
318
|
+
if rel == "runs" or rel.startswith("runs/"):
|
|
319
|
+
continue
|
|
320
|
+
if rel == _SNAPSHOTS_DIRNAME or rel.startswith(
|
|
321
|
+
_SNAPSHOTS_DIRNAME + "/"
|
|
322
|
+
):
|
|
323
|
+
continue
|
|
324
|
+
try:
|
|
325
|
+
target.rmdir()
|
|
326
|
+
except OSError:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
def _write_bytes_map(self, mapping):
|
|
330
|
+
for rel, data in mapping.items():
|
|
331
|
+
target = self._doc_path(rel)
|
|
332
|
+
try:
|
|
333
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
334
|
+
_atomic_write_bytes(target, data)
|
|
335
|
+
except OSError as exc:
|
|
336
|
+
raise ProjectError(
|
|
337
|
+
"invalid_ledger",
|
|
338
|
+
f"Cannot restore {rel}: {exc}.",
|
|
339
|
+
) from exc
|
|
340
|
+
|
|
341
|
+
def _restore_snapshot_bytes(self, snapshot, pre):
|
|
342
|
+
"""Replace managed bytes; best-effort rollback to pre."""
|
|
343
|
+
try:
|
|
344
|
+
self._clear_docs()
|
|
345
|
+
self._write_bytes_map(snapshot)
|
|
346
|
+
except ProjectError:
|
|
347
|
+
try:
|
|
348
|
+
self._clear_docs()
|
|
349
|
+
self._write_bytes_map(pre)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
raise
|
|
353
|
+
|
|
354
|
+
def _linked_external_files(self):
|
|
355
|
+
"""Caller files outside `.rocket/`, sorted, never touched."""
|
|
356
|
+
if self._linked_root is None:
|
|
357
|
+
return []
|
|
358
|
+
root = self._linked_root
|
|
359
|
+
try:
|
|
360
|
+
if not root.is_dir():
|
|
361
|
+
return []
|
|
362
|
+
except OSError:
|
|
363
|
+
return []
|
|
364
|
+
found = []
|
|
365
|
+
try:
|
|
366
|
+
targets = sorted(root.rglob("*"))
|
|
367
|
+
except OSError:
|
|
368
|
+
return []
|
|
369
|
+
for target in targets:
|
|
370
|
+
try:
|
|
371
|
+
if not target.is_file():
|
|
372
|
+
continue
|
|
373
|
+
rel = target.relative_to(root).as_posix()
|
|
374
|
+
except (OSError, ValueError):
|
|
375
|
+
continue
|
|
376
|
+
if rel == _LINKED_CARVEOUT or rel.startswith(
|
|
377
|
+
_LINKED_CARVEOUT + "/"
|
|
378
|
+
):
|
|
379
|
+
continue
|
|
380
|
+
try:
|
|
381
|
+
size = target.stat().st_size
|
|
382
|
+
except OSError:
|
|
383
|
+
continue
|
|
384
|
+
found.append({"path": rel, "bytes": size})
|
|
385
|
+
return sorted(found, key=lambda e: e["path"])
|