agent-bios 0.19.1 → 0.19.3
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.
- package/DEPENDENCIES.md +58 -30
- package/INSTALL.md +4 -4
- package/README.md +105 -28
- package/claude/CLAUDE.md +1 -1
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +4 -4
- package/claude/guides/coding-staged-workflow.md +17 -0
- package/claude/guides/documentation-hygiene.md +3 -0
- package/claude/guides/gpt-prompting.md +1 -1
- package/claude/guides/korean-writing.md +153 -0
- package/claude/guides/learning-flow.md +4 -4
- package/claude/guides/llm-capability-boundary.md +7 -1
- package/claude/guides/session-distill-workflow.md +8 -8
- package/claude/guides/slide-writing/RUNBOOK.md +5 -5
- package/claude/guides/tooling-gotchas.md +20 -1
- package/claude/guides/ui-design/visual-direction.md +88 -0
- package/claude/guides/ui-design.md +90 -0
- package/claude/guides/verification-discipline.md +10 -1
- package/claude/hooks/tooling-gotchas-hook.py +41 -0
- package/claude/skills/repo-charter/SKILL.md +3 -3
- package/claude/skills/understand/SKILL.md +5 -5
- package/codex/AGENTS.md +1 -1
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +4 -4
- package/codex/guides/coding-staged-workflow.md +17 -0
- package/codex/guides/documentation-hygiene.md +3 -0
- package/codex/guides/gpt-prompting.md +1 -1
- package/codex/guides/korean-writing.md +153 -0
- package/codex/guides/learning-flow.md +4 -4
- package/codex/guides/llm-capability-boundary.md +7 -1
- package/codex/guides/session-distill-workflow.md +8 -8
- package/codex/guides/slide-writing/RUNBOOK.md +5 -5
- package/codex/guides/tooling-gotchas.md +20 -1
- package/codex/guides/ui-design/visual-direction.md +88 -0
- package/codex/guides/ui-design.md +90 -0
- package/codex/guides/verification-discipline.md +10 -1
- package/compose/app_bridge/SKILL.md +12 -12
- package/compose/app_bridge/scripts/bridge.py +35 -10
- package/compose/app_desktop/server.py +250 -0
- package/compose/assemble.py +5 -5
- package/compose/bootstrap/SKILL.md +18 -18
- package/compose/canary.sh +4 -4
- package/compose/check-domains.py +6 -6
- package/compose/corpus-state.py +16 -1168
- package/compose/corpus.py +13 -402
- package/compose/corpus_app.py +14 -450
- package/compose/corpus_catalog.py +15 -926
- package/compose/corpus_import.py +14 -523
- package/compose/corpus_install.py +14 -1847
- package/compose/corpus_session.py +16 -848
- package/compose/corpus_setup.py +16 -672
- package/compose/corpus_setup_cli.py +15 -580
- package/compose/corpus_setup_i18n.py +20 -324
- package/compose/corpus_setup_ui.py +18 -645
- package/compose/corpus_store.py +16 -1664
- package/compose/corpus_transaction.py +15 -284
- package/compose/corpus_ui.py +17 -972
- package/compose/corpus_ui_runtime.py +16 -274
- package/compose/corpus_understand.py +13 -671
- package/compose/domains.json +3 -1
- package/compose/host_platform.py +121 -0
- package/compose/instructions-state.py +1178 -0
- package/compose/instructions.py +409 -0
- package/compose/instructions_app.py +697 -0
- package/compose/instructions_catalog.py +931 -0
- package/compose/instructions_import.py +537 -0
- package/compose/instructions_install.py +1932 -0
- package/compose/instructions_session.py +852 -0
- package/compose/instructions_setup.py +713 -0
- package/compose/instructions_setup_cli.py +607 -0
- package/compose/instructions_setup_i18n.py +327 -0
- package/compose/instructions_setup_ui.py +647 -0
- package/compose/instructions_store.py +1668 -0
- package/compose/instructions_transaction.py +308 -0
- package/compose/instructions_ui.py +975 -0
- package/compose/instructions_ui_runtime.py +279 -0
- package/compose/instructions_understand.py +678 -0
- package/compose/native_cli.py +52 -0
- package/compose/register-hooks.py +1 -1
- package/compose/runtime_entry.py +58 -0
- package/compose/setup/START.md +11 -11
- package/compose/windows_deploy.py +719 -0
- package/docs/advanced-launch.md +11 -11
- package/docs/instructions-compatibility.md +86 -0
- package/docs/{corpus.md → instructions.md} +36 -8
- package/docs/recovery.md +10 -10
- package/docs/releases/0.19.2.md +38 -0
- package/docs/releases/0.19.3.md +107 -0
- package/docs/session-model.md +31 -20
- package/docs/setup.md +63 -26
- package/docs/understand.md +6 -6
- package/docs/windows.md +99 -0
- package/install.sh +71 -69
- package/launch/agent-launch.py +309 -293
- package/launch/agent-launch.toml +2 -2
- package/launch/agent-launch.zsh +11 -1
- package/launch/i18n/en.toml +55 -55
- package/launch/i18n/ja.toml +56 -56
- package/launch/i18n/ko.toml +56 -56
- package/launch/shell_integration.py +4 -4
- package/learn/collect-learning.py +10 -10
- package/learn/learning.schema.json +1 -1
- package/learn/migrate-learnings.py +51 -51
- package/package.json +33 -12
- package/provenance.json +1 -1
- /package/docs/assets/{corpus-studio.svg → instructions-studio.svg} +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Load the shipped pure-Python UI wheels offline into one process-owned directory."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import atexit
|
|
5
|
+
from email.parser import BytesParser
|
|
6
|
+
import hashlib
|
|
7
|
+
import importlib
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path, PurePosixPath
|
|
12
|
+
import re
|
|
13
|
+
import stat
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
import threading
|
|
17
|
+
from typing import Any
|
|
18
|
+
import zipfile
|
|
19
|
+
import zlib
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SCHEMA_VERSION = 1
|
|
23
|
+
MAX_ARCHIVE_BYTES = 32 * 1024 * 1024
|
|
24
|
+
MAX_EXPANDED_BYTES = 128 * 1024 * 1024
|
|
25
|
+
_LOCK = threading.RLock()
|
|
26
|
+
_ACTIVE: tuple[str, tempfile.TemporaryDirectory[str]] | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UiRuntimeError(RuntimeError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _canonical(value: Any) -> bytes:
|
|
34
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _bundle_id(manifest: dict[str, Any]) -> str:
|
|
38
|
+
return hashlib.sha256(_canonical({key: value for key, value in manifest.items() if key != "bundle_id"})).hexdigest()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _normalized_name(name: str) -> str:
|
|
42
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _members(archive: zipfile.ZipFile) -> list[zipfile.ZipInfo]:
|
|
46
|
+
result = []
|
|
47
|
+
seen = set()
|
|
48
|
+
expanded = 0
|
|
49
|
+
for info in archive.infolist():
|
|
50
|
+
raw = info.filename.rstrip("/")
|
|
51
|
+
path = PurePosixPath(raw)
|
|
52
|
+
if not raw or path.is_absolute() or "\\" in raw or path.as_posix() != raw \
|
|
53
|
+
or any(part in {".", ".."} for part in path.parts) \
|
|
54
|
+
or any(ord(character) < 32 or ord(character) == 127 for character in raw):
|
|
55
|
+
raise UiRuntimeError(f"unsafe UI wheel member: {info.filename!r}")
|
|
56
|
+
if raw in seen:
|
|
57
|
+
raise UiRuntimeError(f"duplicate UI wheel member: {raw}")
|
|
58
|
+
seen.add(raw)
|
|
59
|
+
mode = info.external_attr >> 16
|
|
60
|
+
if stat.S_ISLNK(mode) or (stat.S_IFMT(mode) not in {0, stat.S_IFREG, stat.S_IFDIR}):
|
|
61
|
+
raise UiRuntimeError(f"non-regular UI wheel member: {raw}")
|
|
62
|
+
if info.is_dir():
|
|
63
|
+
continue
|
|
64
|
+
if info.flag_bits & 1:
|
|
65
|
+
raise UiRuntimeError(f"encrypted UI wheel member: {raw}")
|
|
66
|
+
if path.suffix.lower() in {".so", ".pyd", ".dll", ".dylib", ".pth", ".pyc"} \
|
|
67
|
+
or path.parts[0].endswith(".data"):
|
|
68
|
+
raise UiRuntimeError(f"UI wheel requires unsupported installation behavior: {raw}")
|
|
69
|
+
expanded += info.file_size
|
|
70
|
+
if expanded > MAX_EXPANDED_BYTES:
|
|
71
|
+
raise UiRuntimeError("UI wheel exceeds the extraction size limit")
|
|
72
|
+
result.append(info)
|
|
73
|
+
if not result:
|
|
74
|
+
raise UiRuntimeError("UI wheel has no files")
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def wheel_metadata(filename: str, data: bytes) -> dict[str, Any]:
|
|
79
|
+
"""Read wheel metadata and licensing without importing its Python code."""
|
|
80
|
+
if Path(filename).name != filename or not re.fullmatch(r"[A-Za-z0-9_.!+-]+-py[0-9.]+-none-any\.whl", filename):
|
|
81
|
+
raise UiRuntimeError(f"UI runtime requires a pure Python universal wheel: {filename}")
|
|
82
|
+
if not data or len(data) > MAX_ARCHIVE_BYTES:
|
|
83
|
+
raise UiRuntimeError(f"invalid UI wheel size: {filename}")
|
|
84
|
+
try:
|
|
85
|
+
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
|
86
|
+
members = _members(archive)
|
|
87
|
+
names = {member.filename for member in members}
|
|
88
|
+
metadata_paths = [name for name in names if name.endswith(".dist-info/METADATA")]
|
|
89
|
+
if len(metadata_paths) != 1:
|
|
90
|
+
raise UiRuntimeError(f"UI wheel needs exactly one distribution metadata file: {filename}")
|
|
91
|
+
metadata_path = metadata_paths[0]
|
|
92
|
+
info_root = metadata_path.rsplit("/", 1)[0]
|
|
93
|
+
wheel_path = info_root + "/WHEEL"
|
|
94
|
+
if wheel_path not in names:
|
|
95
|
+
raise UiRuntimeError(f"UI wheel lacks WHEEL metadata: {filename}")
|
|
96
|
+
metadata = BytesParser().parsebytes(archive.read(metadata_path))
|
|
97
|
+
wheel = BytesParser().parsebytes(archive.read(wheel_path))
|
|
98
|
+
tags = wheel.get_all("Tag", [])
|
|
99
|
+
if wheel.get("Root-Is-Purelib", "").lower() != "true" or not tags \
|
|
100
|
+
or any(not re.fullmatch(r"py[0-9.]+-none-any", tag) for tag in tags):
|
|
101
|
+
raise UiRuntimeError(f"UI wheel is not pure Python: {filename}")
|
|
102
|
+
license_files = sorted(name for name in names if name.startswith(info_root + "/") and (
|
|
103
|
+
"/licenses/" in name.lower() or PurePosixPath(name).name.lower().startswith(("license", "licence", "copying"))))
|
|
104
|
+
if not license_files or any(not archive.read(name).strip() for name in license_files):
|
|
105
|
+
raise UiRuntimeError(f"UI wheel lacks an upstream license: {filename}")
|
|
106
|
+
modules = set()
|
|
107
|
+
for name in names:
|
|
108
|
+
parts = PurePosixPath(name).parts
|
|
109
|
+
if parts[0].endswith(".dist-info"):
|
|
110
|
+
continue
|
|
111
|
+
root = parts[0][:-3] if len(parts) == 1 and parts[0].endswith(".py") else parts[0]
|
|
112
|
+
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", root):
|
|
113
|
+
modules.add(root)
|
|
114
|
+
name, version = metadata.get("Name"), metadata.get("Version")
|
|
115
|
+
if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9_.-]+", name) \
|
|
116
|
+
or not isinstance(version, str) or not re.fullmatch(r"[A-Za-z0-9_.!+]+", version) or not modules:
|
|
117
|
+
raise UiRuntimeError(f"invalid UI distribution metadata: {filename}")
|
|
118
|
+
return {"name": _normalized_name(name), "version": version, "filename": filename,
|
|
119
|
+
"sha256": hashlib.sha256(data).hexdigest(), "size_bytes": len(data),
|
|
120
|
+
"expanded_bytes": sum(member.file_size for member in members),
|
|
121
|
+
"modules": sorted(modules), "license_files": license_files,
|
|
122
|
+
"requires_python": metadata.get("Requires-Python", ""),
|
|
123
|
+
"requires_dist": metadata.get_all("Requires-Dist", [])}
|
|
124
|
+
except (zipfile.BadZipFile, KeyError, OSError, UnicodeError, RuntimeError, ValueError, EOFError, zlib.error) as exc:
|
|
125
|
+
if isinstance(exc, UiRuntimeError):
|
|
126
|
+
raise
|
|
127
|
+
raise UiRuntimeError(f"cannot read UI wheel {filename}: {exc}") from exc
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _read_bundle(repo: Path) -> tuple[dict[str, Any], list[tuple[dict[str, Any], bytes]]]:
|
|
131
|
+
bundle = Path(repo) / "compose/ui_runtime"
|
|
132
|
+
manifest_path = bundle / "manifest.json"
|
|
133
|
+
if bundle.is_symlink() or manifest_path.is_symlink() or not manifest_path.is_file():
|
|
134
|
+
raise UiRuntimeError("bundled UI runtime manifest is missing or unsafe; reinstall the agent-bios package")
|
|
135
|
+
try:
|
|
136
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
137
|
+
except (OSError, ValueError) as exc:
|
|
138
|
+
raise UiRuntimeError(f"cannot read UI runtime manifest: {exc}") from exc
|
|
139
|
+
if not isinstance(manifest, dict) or manifest.get("schema_version") != SCHEMA_VERSION \
|
|
140
|
+
or manifest.get("python_requirement") != ">=3.11" or manifest.get("source") != "pypi":
|
|
141
|
+
raise UiRuntimeError("unsupported UI runtime manifest")
|
|
142
|
+
if manifest.get("bundle_id") != _bundle_id(manifest):
|
|
143
|
+
raise UiRuntimeError("UI runtime manifest fingerprint mismatch")
|
|
144
|
+
requirements = manifest.get("root_requirement")
|
|
145
|
+
rows = manifest.get("packages")
|
|
146
|
+
if not isinstance(requirements, str) or not re.fullmatch(r"textual==[0-9][A-Za-z0-9_.!+]*", requirements) \
|
|
147
|
+
or not isinstance(rows, list) or not rows:
|
|
148
|
+
raise UiRuntimeError("UI runtime manifest has no valid package inventory")
|
|
149
|
+
supplied = set()
|
|
150
|
+
filenames = set()
|
|
151
|
+
modules = set()
|
|
152
|
+
files = set()
|
|
153
|
+
expanded = 0
|
|
154
|
+
archives = []
|
|
155
|
+
for row in rows:
|
|
156
|
+
if not isinstance(row, dict) or not isinstance(row.get("filename"), str):
|
|
157
|
+
raise UiRuntimeError("UI runtime manifest has an invalid package row")
|
|
158
|
+
filename = row["filename"]
|
|
159
|
+
if Path(filename).name != filename or filename in filenames:
|
|
160
|
+
raise UiRuntimeError(f"invalid or duplicate UI wheel name: {filename}")
|
|
161
|
+
path = bundle / filename
|
|
162
|
+
if path.is_symlink() or not path.is_file():
|
|
163
|
+
raise UiRuntimeError(f"bundled UI wheel missing or unsafe: {filename}")
|
|
164
|
+
if path.stat().st_size != row.get("size_bytes") or path.stat().st_size > MAX_ARCHIVE_BYTES:
|
|
165
|
+
raise UiRuntimeError(f"UI wheel size mismatch: {filename}")
|
|
166
|
+
data = path.read_bytes()
|
|
167
|
+
if hashlib.sha256(data).hexdigest() != row.get("sha256"):
|
|
168
|
+
raise UiRuntimeError(f"UI wheel SHA256 mismatch: {filename}")
|
|
169
|
+
actual = wheel_metadata(filename, data)
|
|
170
|
+
if actual != row:
|
|
171
|
+
raise UiRuntimeError(f"UI wheel metadata differs from its manifest: {filename}")
|
|
172
|
+
if row["name"] in supplied or modules.intersection(row["modules"]):
|
|
173
|
+
raise UiRuntimeError(f"UI distributions overlap: {filename}")
|
|
174
|
+
supplied.add(row["name"])
|
|
175
|
+
filenames.add(filename)
|
|
176
|
+
modules.update(row["modules"])
|
|
177
|
+
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
|
178
|
+
names = {member.filename for member in _members(archive)}
|
|
179
|
+
if files.intersection(names):
|
|
180
|
+
raise UiRuntimeError(f"UI wheel paths overlap: {filename}")
|
|
181
|
+
files.update(names)
|
|
182
|
+
expanded += row["expanded_bytes"]
|
|
183
|
+
if expanded > MAX_EXPANDED_BYTES:
|
|
184
|
+
raise UiRuntimeError("UI runtime exceeds the extraction size limit")
|
|
185
|
+
archives.append((row, data))
|
|
186
|
+
if {path.name for path in bundle.iterdir() if path.name != "manifest.json"} != filenames:
|
|
187
|
+
raise UiRuntimeError("UI runtime directory differs from its exact wheel inventory")
|
|
188
|
+
if not any(f"{row['name']}=={row['version']}" == requirements for row in rows):
|
|
189
|
+
raise UiRuntimeError("UI runtime root Textual requirement is not present")
|
|
190
|
+
return manifest, archives
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def runtime_inventory(repo: Path) -> dict[str, Any]:
|
|
194
|
+
"""Report bundled hashes, purity, and licenses without extraction or UI imports."""
|
|
195
|
+
try:
|
|
196
|
+
manifest, _archives = _read_bundle(repo)
|
|
197
|
+
except (UiRuntimeError, OSError) as exc:
|
|
198
|
+
return {"schema_version": SCHEMA_VERSION, "status": "unavailable", "issues": [str(exc)], "packages": []}
|
|
199
|
+
return {**manifest, "status": "available", "issues": [],
|
|
200
|
+
"package_count": len(manifest["packages"]),
|
|
201
|
+
"size_bytes": sum(row["size_bytes"] for row in manifest["packages"]),
|
|
202
|
+
"expanded_bytes": sum(row["expanded_bytes"] for row in manifest["packages"]),
|
|
203
|
+
"license_file_count": sum(len(row["license_files"]) for row in manifest["packages"]),
|
|
204
|
+
"pure_python": True}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _preloaded_modules(modules: set[str], active: Path | None) -> list[str]:
|
|
208
|
+
conflicts = []
|
|
209
|
+
for name, module in tuple(sys.modules.items()):
|
|
210
|
+
if name.split(".", 1)[0] not in modules:
|
|
211
|
+
continue
|
|
212
|
+
filename = getattr(module, "__file__", None)
|
|
213
|
+
if active is not None and isinstance(filename, str):
|
|
214
|
+
try:
|
|
215
|
+
Path(filename).resolve().relative_to(active)
|
|
216
|
+
continue
|
|
217
|
+
except (ValueError, OSError):
|
|
218
|
+
pass
|
|
219
|
+
conflicts.append(name)
|
|
220
|
+
return sorted(conflicts)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _cleanup_runtime() -> None:
|
|
224
|
+
global _ACTIVE
|
|
225
|
+
if _ACTIVE is not None:
|
|
226
|
+
_bundle, temporary = _ACTIVE
|
|
227
|
+
sys.path[:] = [entry for entry in sys.path if entry != temporary.name]
|
|
228
|
+
temporary.cleanup()
|
|
229
|
+
_ACTIVE = None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def release_ui_runtime() -> None:
|
|
233
|
+
"""Release owned files immediately before process handoff; UI reuse needs a fresh process."""
|
|
234
|
+
with _LOCK:
|
|
235
|
+
_cleanup_runtime()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def activate_ui_runtime(repo: Path) -> Path:
|
|
239
|
+
"""Activate only verified shipped modules; imports must follow this call."""
|
|
240
|
+
global _ACTIVE
|
|
241
|
+
if sys.version_info < (3, 11):
|
|
242
|
+
raise UiRuntimeError("the bundled UI runtime requires Python 3.11 or newer")
|
|
243
|
+
with _LOCK:
|
|
244
|
+
manifest, archives = _read_bundle(repo)
|
|
245
|
+
if _ACTIVE is not None and _ACTIVE[0] != manifest["bundle_id"]:
|
|
246
|
+
raise UiRuntimeError("another UI bundle is active; start a fresh process for this agent-bios package")
|
|
247
|
+
active = Path(_ACTIVE[1].name).resolve() if _ACTIVE is not None else None
|
|
248
|
+
names = {name for row, _data in archives for name in row["modules"]}
|
|
249
|
+
conflicts = _preloaded_modules(names, active)
|
|
250
|
+
if conflicts:
|
|
251
|
+
raise UiRuntimeError("UI dependencies were imported before bundled activation: " + ", ".join(conflicts[:8])
|
|
252
|
+
+ "; start a fresh process and activate the bundle before importing UI modules")
|
|
253
|
+
if active is not None:
|
|
254
|
+
if not active.is_dir():
|
|
255
|
+
raise UiRuntimeError("the active UI runtime directory is missing; start a fresh process")
|
|
256
|
+
return active
|
|
257
|
+
parent = Path(tempfile.gettempdir()).resolve()
|
|
258
|
+
home = Path.home().resolve()
|
|
259
|
+
if os.name != "nt" and (parent == home or home in parent.parents):
|
|
260
|
+
parent = Path("/tmp").resolve()
|
|
261
|
+
temporary = tempfile.TemporaryDirectory(prefix="agent-bios-ui-", dir=parent)
|
|
262
|
+
target = Path(temporary.name)
|
|
263
|
+
try:
|
|
264
|
+
for _row, data in archives:
|
|
265
|
+
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
|
266
|
+
for member in _members(archive):
|
|
267
|
+
path = target.joinpath(*PurePosixPath(member.filename).parts)
|
|
268
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
269
|
+
with path.open("xb") as output:
|
|
270
|
+
output.write(archive.read(member))
|
|
271
|
+
path.chmod(0o600)
|
|
272
|
+
sys.path.insert(0, str(target))
|
|
273
|
+
importlib.invalidate_caches()
|
|
274
|
+
_ACTIVE = (manifest["bundle_id"], temporary)
|
|
275
|
+
atexit.register(_cleanup_runtime)
|
|
276
|
+
return target
|
|
277
|
+
except BaseException:
|
|
278
|
+
temporary.cleanup()
|
|
279
|
+
raise
|