[object Object]

← back to Cli Printing Press

fix(cli): skip Windows Python Store stub in verify-skill (#1086)

27ad2dda618d56431cf3fcd826017c3a760c3cc2 · 2026-05-11 10:22:37 -0700 · Trevin Chow

* fix(cli): skip Windows Python Store stub in verify-skill

Windows verify-skill crashed before any check ran because
exec.LookPath("python3") resolves to the Microsoft Store launcher stub
(.../WindowsApps/python3.exe) by default. The stub is on PATH but exits
with "Python was not found" instead of executing the script.

Add a resolvePython() fallback chain that's a no-op on non-Windows
(returns "python3") and on Windows skips the stub via path-based
detection, preferring the Python Launcher ("py") and "python" before
falling back. isWindowsStorePython matches WindowsApps + python in the
resolved path; LookPath cannot distinguish the stub any other way.

Sub-bugs 2 (UTF-8 file decode) and 3 (UTF-8 stdout) from #876 were
already resolved by #985 (read_utf8 helper + pythonUTF8Env env vars).

Closes #876

* test(cli): pin versioned Microsoft Store stub case for isWindowsStorePython

Locks in the existing substring match against a future refactor that
might tighten basename detection to strict python.exe / python3.exe
matches and silently reintroduce the bug on Windows 11 where the Store
launcher exposes python3.13.exe under the same WindowsApps directory.

Files touched

Diff

commit 27ad2dda618d56431cf3fcd826017c3a760c3cc2
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Mon May 11 10:22:37 2026 -0700

    fix(cli): skip Windows Python Store stub in verify-skill (#1086)
    
    * fix(cli): skip Windows Python Store stub in verify-skill
    
    Windows verify-skill crashed before any check ran because
    exec.LookPath("python3") resolves to the Microsoft Store launcher stub
    (.../WindowsApps/python3.exe) by default. The stub is on PATH but exits
    with "Python was not found" instead of executing the script.
    
    Add a resolvePython() fallback chain that's a no-op on non-Windows
    (returns "python3") and on Windows skips the stub via path-based
    detection, preferring the Python Launcher ("py") and "python" before
    falling back. isWindowsStorePython matches WindowsApps + python in the
    resolved path; LookPath cannot distinguish the stub any other way.
    
    Sub-bugs 2 (UTF-8 file decode) and 3 (UTF-8 stdout) from #876 were
    already resolved by #985 (read_utf8 helper + pythonUTF8Env env vars).
    
    Closes #876
    
    * test(cli): pin versioned Microsoft Store stub case for isWindowsStorePython
    
    Locks in the existing substring match against a future refactor that
    might tighten basename detection to strict python.exe / python3.exe
    matches and silently reintroduce the bug on Windows 11 where the Store
    launcher exposes python3.13.exe under the same WindowsApps directory.
---
 internal/cli/verify_skill.go               | 40 +++++++++++++++++++++++++++++-
 internal/cli/verify_skill_internal_test.go | 30 ++++++++++++++++++++++
 2 files changed, 69 insertions(+), 1 deletion(-)

diff --git a/internal/cli/verify_skill.go b/internal/cli/verify_skill.go
index 2f4e05d4..e2df6258 100644
--- a/internal/cli/verify_skill.go
+++ b/internal/cli/verify_skill.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"runtime"
 	"strings"
 
 	"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
@@ -25,6 +26,12 @@ var verifySkillScript string
 
 const canonicalSectionsCheckName = "canonical-sections"
 
+const (
+	pythonPython3 = "python3"
+	pythonPy      = "py"
+	pythonPython  = "python"
+)
+
 type verifySkillRunResult struct {
 	Stdout   string
 	Stderr   string
@@ -71,7 +78,7 @@ func runVerifySkillScript(dir string, only []string, asJSON bool, strict bool) (
 		pyArgs = append(pyArgs, "--strict")
 	}
 
-	py := exec.Command("python3", pyArgs...)
+	py := exec.Command(resolvePython(), pyArgs...)
 	py.Stdin = os.Stdin
 	py.Env = pythonUTF8Env(os.Environ())
 	var stdout, stderr bytes.Buffer
@@ -335,6 +342,37 @@ func emitMergedJSON(pyResult verifySkillRunResult, runCanonical, hasCanonicalFin
 	return nil
 }
 
+// resolvePython picks the interpreter name to pass to exec.Command for the
+// embedded verify-skill script. The Windows fallback chain exists because
+// exec.LookPath("python3") on Windows often resolves to the Microsoft Store
+// launcher stub, which is on PATH but exits with a "Python was not found"
+// Store-redirect message instead of running the script.
+func resolvePython() string {
+	if runtime.GOOS != "windows" {
+		return pythonPython3
+	}
+	if path, err := exec.LookPath(pythonPython3); err == nil && !isWindowsStorePython(path) {
+		return pythonPython3
+	}
+	if _, err := exec.LookPath(pythonPy); err == nil {
+		return pythonPy
+	}
+	if path, err := exec.LookPath(pythonPython); err == nil && !isWindowsStorePython(path) {
+		return pythonPython
+	}
+	return pythonPython3
+}
+
+// LookPath succeeds against the Microsoft Store Python launcher stub at
+// .../WindowsApps/python*.exe, so the only pre-exec signal is the path itself.
+func isWindowsStorePython(path string) bool {
+	if path == "" {
+		return false
+	}
+	lower := strings.ToLower(path)
+	return strings.Contains(lower, "windowsapps") && strings.Contains(lower, "python")
+}
+
 // pythonUTF8Env returns base with PYTHONIOENCODING and PYTHONUTF8 forced to
 // UTF-8. Windows consoles default to cp1252, which cannot encode the ✓/✘
 // glyphs the Python script prints; without these env vars the subprocess
diff --git a/internal/cli/verify_skill_internal_test.go b/internal/cli/verify_skill_internal_test.go
index 7302cb86..c12585be 100644
--- a/internal/cli/verify_skill_internal_test.go
+++ b/internal/cli/verify_skill_internal_test.go
@@ -64,3 +64,33 @@ func TestPythonUTF8Env_EmptyBase(t *testing.T) {
 		t.Errorf("expected both UTF-8 entries, got %v", got)
 	}
 }
+
+func TestIsWindowsStorePython(t *testing.T) {
+	t.Parallel()
+
+	cases := []struct {
+		name string
+		path string
+		want bool
+	}{
+		{"store stub python3", `C:\Users\alice\AppData\Local\Microsoft\WindowsApps\python3.exe`, true},
+		{"store stub python", `C:\Users\alice\AppData\Local\Microsoft\WindowsApps\python.exe`, true},
+		{"store stub versioned python3.13", `C:\Users\alice\AppData\Local\Microsoft\WindowsApps\python3.13.exe`, true},
+		{"store stub mixed case", `C:\Users\alice\AppData\Local\Microsoft\WINDOWSAPPS\Python3.exe`, true},
+		{"store stub forward slashes", `C:/Users/alice/AppData/Local/Microsoft/WindowsApps/python3.exe`, true},
+		{"real python install", `C:\Python314\python.exe`, false},
+		{"py launcher", `C:\Windows\py.exe`, false},
+		{"unix path", "/usr/bin/python3", false},
+		{"unrelated windowsapps binary", `C:\Users\alice\AppData\Local\Microsoft\WindowsApps\winget.exe`, false},
+		{"empty path", "", false},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			if got := isWindowsStorePython(tc.path); got != tc.want {
+				t.Errorf("isWindowsStorePython(%q) = %v, want %v", tc.path, got, tc.want)
+			}
+		})
+	}
+}

← 46481718 docs(skills): clarify wrapper-only catalog entries have no g  ·  back to Cli Printing Press  ·  fix(catalog): refresh google-flights entry to reflect fli na 5c4ef3a7 →