diff --git a/scripts/install.sh b/scripts/install.sh index 1a15964b7080..df8739d62cd5 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1620,7 +1620,8 @@ setup_path() { log_info "Setting up hermes command..." if [ "$USE_VENV" = true ]; then - HERMES_BIN="$INSTALL_DIR/venv/bin/hermes" + HERMES_BIN="$INSTALL_DIR/venv/bin/python" + HERMES_ENTRYPOINT="$INSTALL_DIR/hermes" else HERMES_BIN="$(which hermes 2>/dev/null || echo "")" if [ -z "$HERMES_BIN" ]; then @@ -1629,10 +1630,10 @@ setup_path() { fi fi - # Verify the entry point script was actually generated - if [ ! -x "$HERMES_BIN" ]; then - log_warn "hermes entry point not found at $HERMES_BIN" - log_info "This usually means the pip install didn't complete successfully." + # Verify the interpreter and the checked-in entrypoint needed by the launcher. + if [ ! -x "$HERMES_BIN" ] || { [ "$USE_VENV" = true ] && [ ! -f "$HERMES_ENTRYPOINT" ]; }; then + log_warn "Hermes launcher prerequisites not found" + log_info "This usually means the Python package install didn't complete successfully." if [ "$DISTRO" = "termux" ]; then log_info "Try: cd $INSTALL_DIR && python -m pip install -e '.[termux-all]' -c constraints-termux.txt" else @@ -1654,12 +1655,25 @@ setup_path() { # the rm, `cat >` follows the symlink and overwrites the venv pip entry # point with this shim — making `exec "$HERMES_BIN"` self-recurse. (#21454) rm -f "$command_link_dir/hermes" - cat > "$command_link_dir/hermes" < "$command_link_dir/hermes" < "$command_link_dir/hermes" < None: + path.write_text(content, encoding="utf-8") + path.chmod(path.stat().st_mode | stat.S_IXUSR) + + +def _setup_path_function() -> str: + match = re.search( + r"^setup_path\(\) \{\n.*?^}\n", + INSTALL_SH.read_text(encoding="utf-8"), + re.MULTILINE | re.DOTALL, + ) + assert match is not None, "setup_path function not found in scripts/install.sh" + return match.group(0) + + +def test_venv_launcher_bypasses_uv_console_script_that_requires_realpath(tmp_path: Path) -> None: + """Stock macOS must start Hermes even when its uv console script needs realpath.""" + install_dir = tmp_path / "install" + venv_bin = install_dir / "venv" / "bin" + command_dir = tmp_path / "command" + minimal_path = tmp_path / "minimal-path" + result = tmp_path / "launch-result" + venv_bin.mkdir(parents=True) + minimal_path.mkdir() + + dirname = shutil.which("dirname") + assert dirname is not None + (minimal_path / "dirname").symlink_to(dirname) + + _make_executable( + venv_bin / "python", + '#!/bin/sh\nprintf "%s\\n" "$@" > "$LAUNCH_RESULT"\n', + ) + (install_dir / "hermes").write_text("# source entrypoint\n", encoding="utf-8") + _make_executable( + venv_bin / "hermes", + "#!/bin/sh\n" + f'PATH="{minimal_path}"\n' + "'''exec' \"$(dirname -- \"$(realpath -- \"$0\")\")\"/'python3' \"$0\" \"$@\"\n" + "' '''\n", + ) + + harness = "\n".join( + [ + "set -e", + 'get_command_link_dir() { printf "%s" "$COMMAND_LINK_DIR"; }', + 'get_command_link_display_dir() { printf "%s" "$COMMAND_LINK_DIR"; }', + "log_info() { :; }", + "log_success() { :; }", + _setup_path_function(), + "setup_path", + ] + ) + env = os.environ | { + "USE_VENV": "true", + "INSTALL_DIR": str(install_dir), + "DISTRO": "macos", + "COMMAND_LINK_DIR": str(command_dir), + } + subprocess.run(["/bin/bash", "-c", harness], env=env, check=True) + + completed = subprocess.run( + [command_dir / "hermes", "--version"], + env=os.environ | {"LAUNCH_RESULT": str(result)}, + text=True, + capture_output=True, + ) + + assert completed.returncode == 0, completed.stderr + assert result.read_text(encoding="utf-8").splitlines() == [ + str(install_dir / "hermes"), + "--version", + ]