fix(install): the shared completion tail runs from a source slice
- build_update_products builds only the frontends the checkout carries; a python-only slice (the installer's acceptance fixture) publishes commands and runs maintenance without asking PM for node. - stderr_timestamp.py is a launcher boot file copied into published commands; it inlines the EX_CONFIG code instead of importing gateway.restart. - Tests: the stamp-writer slice gains hermes_cli/release_channels.py and pm/paths.py (the modules update_channel now imports); the source-launch fixture records the source_completion hand-off (--finish-update) instead of building products; the stdlib recovery probe blocks PM's engine modules, not the pm.environments boot leaf; the memory-provider restart test selects a generation the running interpreter has not activated; the warm-path installer stage is `products`.
This commit is contained in:
@@ -80,6 +80,13 @@ def build_source_web(project_root: Path, *, env: dict, icons: Path | None = None
|
||||
"--icons", str(icons), "--out", str(project_root / "hermes_cli/web_dist"), env=env)
|
||||
|
||||
|
||||
def source_frontends(project_root: Path) -> tuple[str, ...]:
|
||||
"""The frontend workspaces this checkout carries. A source slice without them
|
||||
(python-only installs, the installer's acceptance fixture) has no products
|
||||
to build; it still publishes commands and runs the maintenance tail."""
|
||||
return tuple(name for name in ("ui-tui", "web") if (project_root / name / "package.json").is_file())
|
||||
|
||||
|
||||
def build_update_products(project_root: Path, *, desktop: bool) -> None:
|
||||
"""Prepare the selected union once; a failed product aborts the update."""
|
||||
# Both current updates and historical takeover reach this in a fresh target
|
||||
@@ -88,14 +95,19 @@ def build_update_products(project_root: Path, *, desktop: bool) -> None:
|
||||
from hermes_cli.update_stage import publish_stage
|
||||
|
||||
_warn_configured_features_missing_deps()
|
||||
frontends = source_frontends(project_root)
|
||||
if not frontends:
|
||||
return
|
||||
env = source_build_env(explicit=True)
|
||||
workspaces = ("ui-tui", "web") + (("apps/desktop",) if desktop else ())
|
||||
workspaces = frontends + (("apps/desktop",) if desktop else ())
|
||||
publish_stage("Updating Node dependencies")
|
||||
prepare_source_dependencies(project_root, workspaces, env=env, explicit=True)
|
||||
publish_stage("Building the TUI")
|
||||
build_source_tui(project_root, env=env)
|
||||
publish_stage("Building the web UI")
|
||||
build_source_web(project_root, env=env, explicit=True)
|
||||
if "ui-tui" in frontends:
|
||||
publish_stage("Building the TUI")
|
||||
build_source_tui(project_root, env=env)
|
||||
if "web" in frontends:
|
||||
publish_stage("Building the web UI")
|
||||
build_source_web(project_root, env=env, explicit=True)
|
||||
if desktop:
|
||||
from hermes_cli.main_desktop import _install_rebuilt_desktop_app, build_prepared_desktop
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ from pathlib import Path
|
||||
from typing import BinaryIO, Sequence, TextIO
|
||||
|
||||
EXTERNAL_SUPERVISOR_FLAG = "--external-supervisor"
|
||||
# gateway.restart.GATEWAY_FATAL_CONFIG_EXIT_CODE. This wrapper is a launcher boot
|
||||
# file: it runs from a source slice and stays stdlib-only.
|
||||
_GATEWAY_FATAL_CONFIG_EXIT_CODE = 78
|
||||
|
||||
_TIMESTAMP_PREFIX = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}(?:\s|$)")
|
||||
|
||||
@@ -97,10 +100,8 @@ def _child_returncode_for_supervisor(command: Sequence[str], returncode: int) ->
|
||||
"""
|
||||
if returncode < 0:
|
||||
return 128 + abs(returncode)
|
||||
from gateway.restart import GATEWAY_FATAL_CONFIG_EXIT_CODE, map_fatal_config_exit_for_launchd
|
||||
|
||||
if returncode == GATEWAY_FATAL_CONFIG_EXIT_CODE and _is_hermes_gateway_run_argv(command):
|
||||
return map_fatal_config_exit_for_launchd(returncode)
|
||||
if returncode == _GATEWAY_FATAL_CONFIG_EXIT_CODE and _is_hermes_gateway_run_argv(command):
|
||||
return 0
|
||||
return returncode
|
||||
|
||||
|
||||
|
||||
@@ -97,8 +97,9 @@ import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
sys.path.insert(0, sys.argv[1])
|
||||
# No selected application environment, no PM, and no CLI config/UI reader.
|
||||
for module in ('pm', 'hermes_cli.config', 'hermes_cli.plugins_cmd'):
|
||||
# No selected application environment, no PM engine, and no CLI config/UI reader.
|
||||
# pm.environments/pm.paths are stdlib boot leaves the recovery owner may use.
|
||||
for module in ('pm.client', 'pm.install', 'pm.store', 'pm.workspace', 'hermes_cli.config', 'hermes_cli.plugins_cmd'):
|
||||
sys.modules[module] = None
|
||||
from hermes_cli import plugins_transaction
|
||||
row = json.loads(sys.stdin.read())
|
||||
|
||||
@@ -123,6 +123,8 @@ def test_setup_reports_restart_and_preserves_external_steps(tmp_path, monkeypatc
|
||||
if python_failure:
|
||||
raise RuntimeError("Python preparation refused")
|
||||
monkeypatch.setattr("pm.sync_venv", sync)
|
||||
# A sync selects a new generation the running interpreter has not activated.
|
||||
monkeypatch.setattr("pm.environments.selected_venv", lambda root: tmp_path / "next-generation")
|
||||
result = mp._install_memory_provider_setup("provider")
|
||||
assert result["ok"] is not python_failure
|
||||
assert result["results"][0]["status"] == ("failed" if python_failure else "restart_required")
|
||||
|
||||
@@ -61,6 +61,15 @@ def source_launch(tmp_path, monkeypatch, isolated_python):
|
||||
(root / "install-stamp.json").write_text(
|
||||
json.dumps({"updateMechanism": "self"}), encoding="utf-8",
|
||||
)
|
||||
# The startup heal hands the shared completion tail (launchers, products,
|
||||
# maintenance) to the checkout's own hermes_cli/source_completion.py. This
|
||||
# source slice has no products; record the hand-off instead of running it.
|
||||
(root / "hermes_cli").mkdir()
|
||||
(root / "hermes_cli" / "source_completion.py").write_text(
|
||||
"import json, sys\n"
|
||||
f"open({str(tmp_path / 'completion-calls')!r}, 'a').write(json.dumps(sys.argv[1:]) + '\\n')\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "pyproject.toml").write_text(
|
||||
'[project]\nname = "launch-proof"\nversion = "1"\nrequires-python = ">=3.11"\n'
|
||||
'[project.optional-dependencies]\nall = []\nlaunch-extra = []\n'
|
||||
@@ -136,6 +145,9 @@ def test_source_python_pin_update_survives_real_gc(source_launch, tmp_path, monk
|
||||
previous = selected_venv(root)
|
||||
if update == "launch":
|
||||
assert venv_sync.prepare_launch(root, []) == new_python
|
||||
# The heal finished the whole tail, with update wording, through the checkout's own completion.
|
||||
calls = [json.loads(line) for line in (tmp_path / "completion-calls").read_text().splitlines()]
|
||||
assert calls == [["--source", str(root), "--finish-update"]]
|
||||
elif update == "sync":
|
||||
# This is the PM sync -> launcher publication sequence now split across
|
||||
# update_completion._prepare and _complete_selected. The former checkout
|
||||
|
||||
@@ -15,7 +15,7 @@ def test_stamp_uses_built_commit_even_with_dispatch_sha_and_refuses_mismatch(tmp
|
||||
repo.mkdir()
|
||||
for relative in ('scripts/write_install_stamp.py', 'scripts/releases/commit_build.py',
|
||||
'hermes_cli/__init__.py', 'hermes_cli/update_channel.py', 'hermes_cli/release_channels.py',
|
||||
'pm/environments.py', 'hermes_cli/steward.py', 'hermes_constants.py'):
|
||||
'pm/paths.py', 'pm/environments.py', 'hermes_cli/steward.py', 'hermes_constants.py'):
|
||||
dest = repo / relative
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ROOT / relative, dest)
|
||||
|
||||
@@ -146,7 +146,7 @@ def test_current_installer_publishes_real_dependencies_and_warm_path(tmp_path, s
|
||||
marker.chmod(0o444)
|
||||
before = facts.read_bytes()
|
||||
try:
|
||||
run([*command, "--stage", "path"])
|
||||
run([*command, "--stage", "products"])
|
||||
finally:
|
||||
marker.chmod(0o644)
|
||||
assert facts.read_bytes() == before
|
||||
|
||||
@@ -35,7 +35,7 @@ def fixture_repo(tmp_path):
|
||||
'scripts/releases/r2.py', 'scripts/releases/r2_scope.py', 'scripts/release-content-types.json',
|
||||
'hermes_cli/__init__.py', 'hermes_cli/update_channel.py',
|
||||
'hermes_cli/release_channels.py',
|
||||
'pm/environments.py', 'hermes_constants.py'):
|
||||
'pm/paths.py', 'pm/environments.py', 'hermes_constants.py'):
|
||||
dest = repo / relative
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ROOT / relative, dest)
|
||||
|
||||
@@ -17,7 +17,8 @@ def _stamp_probe(tmp_path, shell):
|
||||
repo = tmp_path / "installed source"
|
||||
for relative in (
|
||||
"scripts/write_install_stamp.py", "hermes_cli/__init__.py",
|
||||
"hermes_cli/update_channel.py", "pm/environments.py",
|
||||
"hermes_cli/update_channel.py", "hermes_cli/release_channels.py",
|
||||
"pm/paths.py", "pm/environments.py",
|
||||
"hermes_cli/steward.py", "hermes_constants.py",
|
||||
):
|
||||
dest = repo / relative
|
||||
|
||||
Reference in New Issue
Block a user