Skip to content

Commit 02eb6ec

Browse files
committed
may be optimized. work in progress
1 parent 6fd8527 commit 02eb6ec

7 files changed

Lines changed: 263 additions & 146 deletions

File tree

chomp/README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Environment variable `CHOMP_REPO` overrides the repo root (default: `./chomp`).
9393
## Repository layout (auto-created)
9494

9595
```
96-
chomp/
96+
chomp.out/
9797
nupkgs/ ← downloaded .nupkg cache
9898
installers/ ← staged installer binaries
9999
out/
@@ -141,7 +141,42 @@ manifests/
141141

142142
---
143143

144-
## URL filtering
144+
## Error handling
145+
146+
CHOMP distinguishes three error classes:
147+
148+
**Per-package / per-download failures** — logged inline with `` and included in the summary and manifest. The run continues with remaining packages.
149+
150+
**Fatal errors** (bad args, missing mode, unreadable directory) — printed as a clean one-liner to stderr and exit code 1:
151+
152+
```
153+
error: rewrite mode requires --base-url
154+
```
155+
156+
**Interrupted runs** (`Ctrl-C`) — prints a clean abort line and any partial progress before exiting with code 130:
157+
158+
```
159+
interrupted (ctrl-c)
160+
3 installer(s) downloaded before interrupt
161+
```
162+
163+
### Tracebacks
164+
165+
By default, tracebacks are suppressed for clean output. Enable them two ways:
166+
167+
```bash
168+
# via flag (also enables verbose debug output)
169+
chomp internalize googlechrome -v
170+
171+
# via environment variable (traceback only, no extra verbosity)
172+
CHOMP_TRACEBACK=1 chomp internalize googlechrome
173+
```
174+
175+
Exit codes: `0` success, `1` error, `2` bad arguments, `130` interrupted.
176+
177+
---
178+
179+
145180

146181
CHOMP skips URLs that aren't downloadable installer paths:
147182

chomp/src/chomp/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
resolve_with_deps,
99
)
1010
from .manifest import print_summary, write_csv, write_json
11-
from .repack import finalize_nupkg, process_nupkg_phase1
11+
from .repack import build_nupkg, collect_urls
1212

1313
__all__ = [
14-
"process_nupkg_phase1",
15-
"finalize_nupkg",
14+
"collect_urls",
15+
"build_nupkg",
1616
"download_batch",
1717
"installer_path",
1818
"resolve_package",

chomp/src/chomp/cli.py

Lines changed: 92 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,20 @@
1818
from .download import download_batch, installer_path, print_failed
1919
from .fetch import resolve_and_download_packages, resolve_with_deps
2020
from .manifest import print_summary, write_csv, write_json
21-
from .repack import finalize_nupkg, process_nupkg_phase1
22-
from .term import bold, dim, err, info, ok, section, set_verbose, warn
21+
from .repack import build_nupkg, collect_urls
22+
from .term import (
23+
abort,
24+
bold,
25+
dim,
26+
err,
27+
fatal,
28+
info,
29+
ok,
30+
section,
31+
set_verbose,
32+
vlog,
33+
warn,
34+
)
2335

2436

2537
def build_parser() -> argparse.ArgumentParser:
@@ -181,63 +193,69 @@ def main(argv=None) -> int:
181193
unique.append(n)
182194
nupkgs = unique
183195

184-
# ---- Phase 1: rewrite URLs ----
196+
# ---- Collect URLs (read-only scan of all nupkgs) ----
185197
all_mappings = []
186-
processed_nupkgs = [] # only nupkgs written this run
187-
188-
section("Phase 1 — Rewriting URLs")
189-
with tempfile.TemporaryDirectory(prefix="chomp_p1_") as tmp:
190-
for nupkg in nupkgs:
191-
mappings = process_nupkg_phase1(
192-
nupkg=nupkg,
193-
base_url=base_url,
194-
out_dir=out_dir,
195-
work_dir=Path(tmp),
196-
mode=mode,
197-
dry_run=is_dry,
198-
force=force,
199-
)
200-
if mappings is not None:
201-
all_mappings.extend(mappings)
202-
processed_nupkgs.append(out_dir / nupkg.name)
198+
nupkg_mappings: dict[Path, list[dict]] = {} # per-package URL maps
199+
200+
section("Phase 1 — Scanning packages")
201+
for nupkg in nupkgs:
202+
result = collect_urls(
203+
nupkg=nupkg,
204+
base_url=base_url,
205+
out_dir=out_dir,
206+
mode=mode,
207+
force=force,
208+
)
209+
if result is None:
210+
continue # already exists, skip
211+
nupkg_mappings[nupkg] = result
212+
all_mappings.extend(result)
203213

204-
if not all_mappings:
205-
print(warn("No external URLs found."))
214+
if not nupkg_mappings:
215+
print(warn("Nothing to do."))
206216
return 0
207217

208-
# ---- Phase 2: download + finalize ----
209-
if not args.skip_download and not is_dry:
218+
if is_dry:
219+
pass # fall through to summary
220+
221+
# ---- Download installers ----
222+
elif not args.skip_download:
210223
section("Phase 2 — Downloading installers")
211-
all_mappings = download_batch(
212-
items=all_mappings,
213-
installer_dir=installer_dir,
214-
use_pwsh=args.pwsh,
215-
quiet=args.quiet,
216-
interactive=args.interactive,
217-
force=force,
218-
)
224+
try:
225+
all_mappings = download_batch(
226+
items=all_mappings,
227+
installer_dir=installer_dir,
228+
use_pwsh=args.pwsh,
229+
quiet=args.quiet,
230+
interactive=args.interactive,
231+
force=force,
232+
)
233+
except KeyboardInterrupt:
234+
abort()
235+
done = sum(1 for m in all_mappings if m.get("downloaded") == "ok")
236+
print(warn(f" {done} installer(s) downloaded before interrupt"))
237+
raise
219238

220-
section("Phase 2b — Finalizing packages")
239+
# ---- Build final nupkgs ----
240+
section("Phase 3 — Building packages")
221241

222242
def resolve(m):
223243
return installer_path(m, installer_dir)
224244

225-
with tempfile.TemporaryDirectory(prefix="chomp_p2_") as tmp2:
226-
for nupkg in processed_nupkgs:
227-
if not nupkg.exists():
228-
continue
229-
pkg_id = nupkg.stem.split(".")[0]
230-
pkg_maps = [m for m in all_mappings if m["package"] == pkg_id]
231-
if not pkg_maps:
232-
continue
233-
finalize_nupkg(
234-
nupkg=nupkg,
235-
mappings=pkg_maps,
236-
local_file_resolver=resolve,
237-
out_dir=out_dir,
238-
work_dir=Path(tmp2),
239-
mode=mode,
240-
)
245+
with tempfile.TemporaryDirectory(prefix="chomp_") as tmp:
246+
try:
247+
for nupkg, mappings in nupkg_mappings.items():
248+
build_nupkg(
249+
nupkg=nupkg,
250+
mappings=mappings,
251+
local_file_resolver=resolve,
252+
out_dir=out_dir,
253+
work_dir=Path(tmp),
254+
mode=mode,
255+
)
256+
except KeyboardInterrupt:
257+
abort()
258+
raise
241259

242260
# ---- Output ----
243261
if not is_dry:
@@ -257,4 +275,28 @@ def resolve(m):
257275

258276

259277
def entry_point():
260-
sys.exit(main())
278+
import os
279+
import traceback
280+
281+
_show_tb = (
282+
os.environ.get("CHOMP_TRACEBACK") or "--verbose" in sys.argv or "-v" in sys.argv
283+
)
284+
285+
try:
286+
sys.exit(main())
287+
except KeyboardInterrupt:
288+
abort()
289+
sys.exit(130)
290+
except Exception as exc:
291+
if _show_tb:
292+
traceback.print_exc()
293+
else:
294+
# Walk cause chain for a useful one-liner
295+
msg = str(exc).strip() or type(exc).__name__
296+
cause = exc.__cause__ or exc.__context__
297+
if cause and str(cause).strip():
298+
msg = f"{msg}: {str(cause).strip()}"
299+
fatal(
300+
msg, hint="re-run with -v for full traceback, or set CHOMP_TRACEBACK=1"
301+
)
302+
sys.exit(1)

chomp/src/chomp/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def validate_mode(mode: str) -> str:
4747

4848

4949
def get_repo_root() -> Path:
50-
return Path(os.environ.get("CHOMP_REPO", "./chomp")).resolve()
50+
return Path(os.environ.get("CHOMP_REPO", "./chomp.out")).resolve()
5151

5252

5353
def repo_path(root: Path, key: str) -> Path:

chomp/src/chomp/download.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ def _prompt_url(item: dict, dest: Path) -> dict:
6767
print(f" {warn('?')} Enter, s, or e.")
6868

6969

70+
def _item_key(item: dict) -> tuple:
71+
return (item["package"], item.get("version", ""), item["filename"])
72+
73+
7074
def download_batch(
7175
items: list[dict],
7276
installer_dir: Path,
@@ -82,7 +86,7 @@ def download_batch(
8286
print(f"\n{info('── Review URLs (all decisions before downloads start) ──')}")
8387
resolved, seen = [], set()
8488
for item in items:
85-
key = (item["package"], item.get("version", ""), item["filename"])
89+
key = _item_key(item)
8690
dest = installer_path(item, installer_dir)
8791
if key in seen:
8892
resolved.append(
@@ -101,17 +105,16 @@ def download_batch(
101105
seen, results = set(), []
102106
for item in items:
103107
url = item.get("resolved_url", item["old_url"])
104-
key = (item["package"], item.get("version", ""), item["filename"])
108+
key = _item_key(item)
105109
action = item.get("action")
106110
dest = installer_path(item, installer_dir)
107111

108-
# Pre-resolved (interactive)
109-
if action == "duplicate":
112+
if action == "duplicate" or (action is None and key in seen):
110113
results.append(
111114
{**item, "downloaded": "skipped-duplicate", "skip_reason": ""}
112115
)
113116
continue
114-
if action == "exists":
117+
if action == "exists" or (action is None and dest.exists() and not force):
115118
if not quiet:
116119
print(f" {SKIP} {dim('exists:')} {dim(str(dest))}")
117120
results.append({**item, "downloaded": "skipped-exists", "skip_reason": ""})
@@ -126,22 +129,8 @@ def download_batch(
126129
)
127130
continue
128131

129-
# Non-interactive dedup
130-
if key in seen:
131-
results.append(
132-
{**item, "downloaded": "skipped-duplicate", "skip_reason": ""}
133-
)
134-
continue
135132
seen.add(key)
136133

137-
# Skip-if-exists (unless --force)
138-
if dest.exists() and not force:
139-
if not quiet:
140-
print(f" {SKIP} {dim('exists:')} {dim(str(dest))}")
141-
results.append({**item, "downloaded": "skipped-exists", "skip_reason": ""})
142-
continue
143-
144-
# URL classification
145134
should_dl, skip_reason = classify_url(url)
146135
if not should_dl:
147136
if not quiet:
@@ -151,7 +140,14 @@ def download_batch(
151140
)
152141
continue
153142

154-
dest.parent.mkdir(parents=True, exist_ok=True)
143+
try:
144+
dest.parent.mkdir(parents=True, exist_ok=True)
145+
except OSError as e:
146+
print(f" {CROSS} {err('mkdir failed:')} {dim(str(e))}")
147+
results.append(
148+
{**item, "downloaded": "failed", "skip_reason": f"mkdir: {e}"}
149+
)
150+
continue
155151
success, err_msg = _download_one(url, dest, use_pwsh)
156152
results.append(
157153
{

0 commit comments

Comments
 (0)