Skip to content

Commit 953b88e

Browse files
committed
fix: lpb self-update (404 + engine clobber) + --dev/--main short flags
- self_update: py_url built with str.replace hit '/lpb' inside '/lpb-stack' → raw.githubusercontent.com/lpb.py-stack/... → HTTP 404 on every update - self_update: used sys.argv[0] (which is lpb.py when exec'd via the bash wrapper) as the 'launcher' path → overwrote lpb.py with the bash wrapper, corrupting the install. Now resolves engine via __file__ and treats 'lpb' in the same dir as the optional wrapper - self_update: branch follows the pipeline tag — dev tags (--tag dev, versioned *-dev) pull from dev, stable tags from main; a main-installed launcher updates from dev (and vice versa) via the tag - atomic staging cleanup moved to finally; engine updated before wrapper - warn()/err(): removed duplicate print — one warning produced two lines - new flags: --dev / --main as shorthand for --tag dev / --tag main (explicit --tag/--version still win) - tests: real self_update coverage (branch selection, file replacement, engine-only layout, single warning on network failure) + flag tests
1 parent 673d2a3 commit 953b88e

2 files changed

Lines changed: 243 additions & 66 deletions

File tree

‎scripts/lpb.py‎

Lines changed: 69 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@
1010
lpb --stop Stop the container
1111
lpb --remove Stop + remove container + state dirs
1212
lpb --logs Stream container logs
13-
lpb --update Pull latest image(s)
14-
lpb --config Show config file location
15-
lpb --help Show usage
1613
lpb --tag dev|main|latest Select image pipeline (dev/main/latest/<custom>)
14+
lpb --dev / lpb --main Shorthand for --tag dev / --tag main
15+
lpb --update Pull latest image(s) (+ self-update launcher)
1716
1817
Positional command aliases (no -- needed):
1918
lpb logs → lpb --logs
@@ -29,8 +28,13 @@
2928
lpb --tag main Use latest stable image (0.0.x-lpb-cli)
3029
lpb --tag latest Same as main
3130
lpb --tag 0.0.9-lpb-dev Pin to specific version image
31+
lpb --dev / lpb --main Shorthand for --tag dev / --tag main
3232
LPB_IMAGE_TAG=0.0.9-lpb-dev Or set env var for persistent override
3333
34+
Self-update (lpb --update):
35+
Updates the installed lpb/lpb.py from the pipeline branch matching the
36+
tag (dev tags → dev branch, stable tags → main branch), then pulls images.
37+
3438
Pi passthrough (after "--"):
3539
lpb /myproject -- -p "summarize this repo"
3640
lpb /myproject -- --session abc123
@@ -310,64 +314,68 @@ class Config:
310314
def err(msg: str, hint: str = "") -> None:
311315
"""Print an error (red) to stderr; optionally with a yellow hint line."""
312316
logger.error("%sError: %s%s", _ERR, msg, _RSV)
313-
print(f"{_ERR}Error: {msg}{_RSV}", file=sys.stderr)
314317
if hint:
315318
logger.warning(" %s%s%s", _WRN, hint, _RSV)
316-
print(f" {_WRN}{hint}{_RSV}", file=sys.stderr)
317319

318320

319321
def warn(msg: str) -> None:
320-
"""Print a warning (yellow) to stderr."""
322+
"""Print a warning (yellow) to stderr (single line via logger)."""
321323
logger.warning("%sWarning: %s%s", _WRN, msg, _RSV)
322-
print(f"{_WRN}Warning: {msg}{_RSV}", file=sys.stderr)
323324

324325

325326
# ── Output helpers (stdout) ───────────────────────────────────────────────────
326327

328+
# Engine file path — resolved from __file__, NOT sys.argv[0]: the bash wrapper
329+
# execs `python3 lpb.py`, so argv[0] is the engine and the wrapper is its
330+
# sibling "lpb" in the same directory. (Tests may override this.)
331+
LPB_ENGINE_PATH = Path(__file__).resolve()
332+
333+
334+
def _fetch_file(url: str, dest: Path, staging: Path) -> None:
335+
"""Download url and atomically replace dest if the content changed."""
336+
with urllib.request.urlopen(url, timeout=10) as resp:
337+
new_data = resp.read()
338+
with open(dest, "rb") as f:
339+
old_data = f.read()
340+
if new_data == old_data:
341+
return
342+
info(f"Updating {dest.name}...")
343+
with open(staging, "wb") as f:
344+
f.write(new_data)
345+
staging.rename(dest)
346+
dest.chmod(0o755)
347+
348+
349+
# ── Self-update (lpb --update) ───────────────────────────────────────────────────
350+
327351
def self_update() -> None:
328-
"""Update lpb itself from the GitHub repo if a newer version is available."""
329-
lpb_path = Path(sys.argv[0]).resolve()
330-
lpb_dir = lpb_path.parent
331-
new_path = lpb_dir / "lpb.new"
332-
if not lpb_path.is_file():
352+
"""Update lpb (wrapper) + lpb.py (engine) from the GitHub repo.
353+
354+
Source branch follows the pipeline tag: --tag dev (or a versioned
355+
*-dev tag) pulls from the dev branch, everything else from main — so a
356+
launcher installed from main can be updated from dev (and vice versa)
357+
simply by choosing the tag. Network/IO failures never break startup.
358+
"""
359+
engine_path = LPB_ENGINE_PATH
360+
if not engine_path.is_file():
333361
return
334-
# Select branch based on cfg.image_tag (set by --tag)
335-
branch = "dev" if cfg.image_tag == "dev" else "main"
336-
script_url = f"https://raw.githubusercontent.com/lpb-stack/devstack/{branch}/scripts/lpb"
337-
py_url = script_url.replace("/lpb", "/lpb.py")
362+
base_dir = engine_path.parent
363+
wrapper_path = base_dir / "lpb"
364+
tag = (cfg.image_tag or "").strip().lower()
365+
branch = "dev" if (tag == "dev" or tag.endswith("-dev")) else "main"
366+
base_url = f"https://raw.githubusercontent.com/lpb-stack/devstack/{branch}/scripts/"
367+
staging = base_dir / "lpb.py.new"
338368
try:
339-
with urllib.request.urlopen(script_url, timeout=10) as resp:
340-
new_wrapper = resp.read()
341-
with open(lpb_path, "rb") as f:
342-
old_wrapper = f.read()
343-
if new_wrapper != old_wrapper:
344-
info(f"Updating lpb launcher...")
345-
with open(new_path, "wb") as f:
346-
f.write(new_wrapper)
347-
new_path.rename(lpb_path)
348-
lpb_path.chmod(0o755)
349-
with urllib.request.urlopen(py_url, timeout=10) as resp:
350-
new_py = resp.read()
351-
py_path = lpb_dir / "lpb.py"
352-
if py_path.is_file():
353-
with open(py_path, "rb") as f:
354-
old_py = f.read()
355-
if new_py != old_py:
356-
info(f"Updating lpb.py...")
357-
with open(new_path, "wb") as f:
358-
f.write(new_py)
359-
new_path.rename(py_path)
360-
elif not py_path.is_file():
361-
info(f"Installing missing lpb.py...")
362-
with open(new_path, "wb") as f:
363-
f.write(new_py)
364-
new_path.rename(py_path)
365-
py_path.chmod(0o755)
366-
for _f in lpb_dir.iterdir():
367-
if _f.name.endswith(".new"):
368-
_f.unlink()
369-
except Exception as exc: # network/IO failures should never break startup
369+
# Engine first (this running script itself)
370+
_fetch_file(base_url + "lpb.py", engine_path, staging)
371+
# Wrapper (optional — only present in install.sh installs)
372+
if wrapper_path.is_file():
373+
_fetch_file(base_url + "lpb", wrapper_path, staging)
374+
except Exception as exc:
370375
warn(f"self-update skipped: {exc}")
376+
finally:
377+
if staging.exists():
378+
staging.unlink()
371379

372380
def info(msg):
373381
"""Print an informational message to stdout (no color)."""
@@ -739,9 +747,10 @@ def apply_overrides(project_dir: str | None = None, project_name: str | None = N
739747
" lpb --stop Stop the container\n"
740748
" lpb --remove Stop + remove container + state dirs\n"
741749
" lpb --logs Stream container logs\n"
742-
" lpb --update Pull latest image(s)\n"
750+
" lpb --update Pull latest image(s) (+ self-update launcher)\n"
743751
" lpb --config Show config file location\n"
744-
" lpb --help Show this help\n\n"
752+
" lpb --help Show this help\n"
753+
" lpb --tag dev|main|latest Select image pipeline (or --dev / --main)\n\n"
745754
"Pi passthrough (after \"--\"):\n"
746755
' lpb /myproject -- -p "summarize" # Non-interactive, process & exit\n'
747756
' lpb /myproject -- --session abc123 # Resume specific session\n'
@@ -785,6 +794,10 @@ def _build_parser() -> argparse.ArgumentParser:
785794
parser.add_argument("--web", action="store_true")
786795
parser.add_argument("--tag", default=None,
787796
help="Select image pipeline (dev|main|latest|<custom>)")
797+
parser.add_argument("--dev", action="store_true",
798+
help="Shorthand for --tag dev (dev pipeline)")
799+
parser.add_argument("--main", action="store_true",
800+
help="Shorthand for --tag main (stable pipeline)")
788801
parser.add_argument("--version", nargs="?", const="_show",
789802
help="Pin to version tag (e.g. 0.0.9-lpb) or show stack version")
790803
parser.add_argument("--stop", "-s", action="store_true")
@@ -844,8 +857,14 @@ def parse_cli(args: list[str]) -> None:
844857
cfg.shell_mode = True
845858
if known.web:
846859
cfg.web_mode = True
860+
# Pipeline short flags: --dev / --main (only used when --tag/--version absent;
861+
# explicit --tag or --version always win)
862+
if known.dev:
863+
cfg.image_tag = "dev"
864+
if known.main:
865+
cfg.image_tag = "main"
847866
if known.tag is not None:
848-
cfg.image_tag = known.tag
867+
cfg.image_tag, _cli_overrides["image_tag"] = known.tag, True
849868
if known.version:
850869
if known.version == "_show" or known.version == "":
851870
# --version without value → show version and exit

0 commit comments

Comments
 (0)