From a761b0c0865a1528c237cf8effe486ba74b52c88 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Thu, 16 Jul 2026 23:55:29 +0200 Subject: [PATCH] fix(scripts): resolve go-installed binaries via GOPATH/bin fallback make install-gup failed with 'Installation via go completed but binary not found' whenever ${GOPATH:-$HOME/go}/bin was not on PATH: go install succeeds, but resolve_global_bin only checked PATH and npm's global bin. Add a GOPATH/bin fallback so the verify finds the binary and the existing off-PATH warning fires instead of a false error. Signed-off-by: Sebastian Mendel --- scripts/lib/common.sh | 7 ++- tests/test_resolve_global_bin.py | 89 ++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/test_resolve_global_bin.py diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index d38cf16..b2deef2 100755 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -115,7 +115,8 @@ npm_global_bin_dir() { return 0 } -# Resolve a global CLI binary path: prefer PATH, fall back to npm's global bin. +# Resolve a global CLI binary path: prefer PATH, fall back to npm's global +# bin, then GOPATH/bin (go install lands there even when it is off PATH). resolve_global_bin() { local bin="$1" p p="$(command -v "$bin" 2>/dev/null || true)" @@ -124,6 +125,10 @@ resolve_global_bin() { gdir="$(npm_global_bin_dir)" [[ -n "$gdir" ]] && [[ -x "$gdir/$bin" ]] && p="$gdir/$bin" fi + if [[ -z "$p" ]]; then + local go_bin="${GOPATH:-$HOME/go}/bin/$bin" + [[ -x "$go_bin" ]] && p="$go_bin" + fi printf '%s' "$p" } diff --git a/tests/test_resolve_global_bin.py b/tests/test_resolve_global_bin.py new file mode 100644 index 0000000..c053737 --- /dev/null +++ b/tests/test_resolve_global_bin.py @@ -0,0 +1,89 @@ +"""Tests for resolve_global_bin's GOPATH fallback. + +A successful `go install` lands in ${GOPATH:-$HOME/go}/bin; when that dir is +not on PATH, resolve_global_bin previously only fell back to npm's global bin +dir, so the install verify reported a false "binary not found" error. +""" + +from __future__ import annotations + +import subprocess +import sys +import tempfile +from pathlib import Path + +import pytest + +skip_on_windows = pytest.mark.skipif( + sys.platform == "win32", reason="Shell script tests require POSIX shell" +) + +SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" + + +@skip_on_windows +class TestResolveGlobalBinGopath: + def _resolve(self, env_setup: str, binary: str) -> str: + full_code = f""" +set -euo pipefail +source "{SCRIPTS_DIR}/lib/common.sh" +{env_setup} +resolve_global_bin "{binary}" +""" + result = subprocess.run( + ["bash", "-c", full_code], + capture_output=True, text=True, timeout=10, + ) + assert result.returncode == 0, result.stderr + return result.stdout.strip() + + @staticmethod + def _make_tool(bin_dir: Path, name: str) -> Path: + bin_dir.mkdir(parents=True, exist_ok=True) + tool = bin_dir / name + tool.write_text("#!/bin/sh\necho tool\n") + tool.chmod(0o755) + return tool + + def test_finds_binary_in_gopath_bin_off_path(self): + with tempfile.TemporaryDirectory() as tmpdir: + gopath = Path(tmpdir) / "gopath" + tool = self._make_tool(gopath / "bin", "faketool_xyz") + + resolved = self._resolve( + f'export GOPATH="{gopath}"; export PATH="/usr/bin:/bin"', + "faketool_xyz", + ) + assert resolved == str(tool) + + def test_defaults_to_home_go_bin_without_gopath(self): + with tempfile.TemporaryDirectory() as tmpdir: + home = Path(tmpdir) / "home" + tool = self._make_tool(home / "go" / "bin", "faketool_xyz") + + resolved = self._resolve( + f'export HOME="{home}"; unset GOPATH; export PATH="/usr/bin:/bin"', + "faketool_xyz", + ) + assert resolved == str(tool) + + def test_path_hit_wins_over_gopath(self): + with tempfile.TemporaryDirectory() as tmpdir: + gopath = Path(tmpdir) / "gopath" + self._make_tool(gopath / "bin", "faketool_xyz") + path_dir = Path(tmpdir) / "onpath" + on_path = self._make_tool(path_dir, "faketool_xyz") + + resolved = self._resolve( + f'export GOPATH="{gopath}"; export PATH="{path_dir}:/usr/bin:/bin"', + "faketool_xyz", + ) + assert resolved == str(on_path) + + def test_missing_binary_resolves_empty(self): + with tempfile.TemporaryDirectory() as tmpdir: + resolved = self._resolve( + f'export GOPATH="{tmpdir}"; export PATH="/usr/bin:/bin"', + "faketool_definitely_missing_xyz", + ) + assert resolved == ""