From 248025cdf937025f149d0cd78ed3d372655adfc8 Mon Sep 17 00:00:00 2001 From: Suriyakumarvijayanayagam Date: Wed, 30 Sep 2026 17:11:47 +0530 Subject: [PATCH] The Python ceiling was defeated by the venv the failure left behind makeVenv reused any environment already on disk, whatever Python built it. The machine that found the version bug already had a runtime built by 3.14, left there by the run that failed - so with the ceiling in place setup would choose a good interpreter, reach makeVenv, find the 3.14 environment, keep it, and die in the same clang error as before. A fix a user cannot reach because the bug's own debris is in the way is not a fix, and it would have read as the release not working. It now asks the interpreter inside an existing environment what it is and rebuilds when the answer is unsupported, saying so. Rebuilding costs a re-download of the libraries and nothing else - the models live in the state root. An environment that cannot be asked counts as unusable too: a half-created one answers nothing, and reusing it fails later in pip with an error about a package rather than about the environment. Tested against real environments rather than a fake, because what is under test is what an interpreter on disk reports about itself. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj --- CLAUDE.md | 16 ++++++++ agent/cmd/behavision-setup/main.go | 40 ++++++++++++++++++- agent/cmd/behavision-setup/python_test.go | 48 ++++++++++++++++++++++- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4d53379..ad564cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3495,6 +3495,22 @@ an uncapped `>=4.8.1` means every NEW install silently gets a major release this project has never run a real camera through while every existing one keeps 4.11. +### And the fix was defeated by the wreckage of the bug + +`makeVenv` reused any environment already on disk, whatever Python built it. +That machine had a runtime built by **3.14**, left behind by the run that +failed — so with the ceiling in place setup would choose a good interpreter, +reach `makeVenv`, find the 3.14 environment, keep it, and die in the same clang +error as before. A fix a user cannot reach because the bug's own debris is in +the way is not a fix, and it would have read as the release not working. + +It now asks the interpreter inside an existing environment what it is and +rebuilds when the answer is unsupported, saying so. Rebuilding costs a +re-download of the libraries and nothing else — the models live in the state +root, not in there. An environment that cannot be asked counts as unusable +too: a half-created one answers nothing, and reusing it fails later in pip +with an error about a package rather than about the environment. + ### One MQTT client id for a whole shop, so two PCs fought over it `behavision--` is the same string on every computer claimed to diff --git a/agent/cmd/behavision-setup/main.go b/agent/cmd/behavision-setup/main.go index 2ab5f22..4253fc1 100644 --- a/agent/cmd/behavision-setup/main.go +++ b/agent/cmd/behavision-setup/main.go @@ -473,14 +473,52 @@ func venvPython(venv string) string { // engine requires - inside a shared interpreter is how you break the other // thing months later, silently. func makeVenv(py, venv string) error { + // An existing environment is reused - but only if the Python inside it is + // one this build supports. + // + // It used to be reused unconditionally, and that would have made the + // version ceiling above look like it did not work. The machine this was + // all found on already had a runtime built by Python 3.14, from the run + // that failed: with the ceiling in place setup would choose a good + // interpreter, reach here, find the 3.14 environment, keep it, and die in + // the same clang error as before. A fix that is defeated by the wreckage + // of the bug it fixes is not one. + // + // Rebuilding costs a re-download of the libraries and nothing else. The + // models are in the state root, not in here, so they survive. if _, err := os.Stat(venvPython(venv)); err == nil { - return nil // already built; pip below brings it up to date + ok, ver := venvUsable(venv) + if ok { + return nil // pip below brings it up to date + } + fmt.Printf(" [..] %-24s %s\n", "Rebuilding environment", + "the existing one uses "+ver+", which is not supported") + if err := os.RemoveAll(venv); err != nil { + return fmt.Errorf("removing the old environment at %s: %w", venv, err) + } } exe, args := splitLauncher(py) args = append(args, "-m", "venv", venv) return stream(exec.Command(exe, args...), "creating the virtual environment") } +// venvUsable reports whether the interpreter already inside an environment is +// one this build supports, and what it is when it is not. +// +// An environment that cannot be asked counts as unusable: a half-created or +// truncated one answers nothing, and reusing it fails later in pip with an +// error about a package rather than about the environment. +func venvUsable(venv string) (bool, string) { + out, err := exec.Command(venvPython(venv), "-c", + "import sys;print('%d.%d'%sys.version_info[:2])").Output() + if err != nil { + return false, "an interpreter that will not run" + } + ver := strings.TrimSpace(string(out)) + major, minor, parsed := parseVer(ver) + return pythonVerdict(major, minor, parsed) == verdictOK, "Python " + ver +} + func pipInstall(vpy, src string) error { fmt.Println(" Installing the engine and its libraries. This downloads a few") fmt.Println(" hundred megabytes and takes a while on a slow connection.") diff --git a/agent/cmd/behavision-setup/python_test.go b/agent/cmd/behavision-setup/python_test.go index e2395aa..e056c3a 100644 --- a/agent/cmd/behavision-setup/python_test.go +++ b/agent/cmd/behavision-setup/python_test.go @@ -1,6 +1,10 @@ package main -import "testing" +import ( + "os" + "path/filepath" + "testing" +) // The choice this program makes silently, and got wrong. // @@ -64,3 +68,45 @@ func TestUnknownVersionsAreNotAccepted(t *testing.T) { } } } + +// An environment already on disk is reused, and that is right until the Python +// inside it is one this build cannot use. +// +// It was reused unconditionally, which would have defeated the ceiling above +// on the exact machine that found the bug: that Mac already had a runtime +// built by Python 3.14, left behind by the run that failed. Setup would pick a +// good interpreter, find the 3.14 environment, keep it, and die in the same +// clang error as before - a fix defeated by the wreckage of the bug it fixes. +// +// Real environments, not a fake: the thing under test is what an interpreter +// on disk reports about itself. +func TestAnUnsupportedEnvironmentIsNotReused(t *testing.T) { + py, _, err := findPython() + if err != nil { + t.Skipf("no supported Python on this machine: %v", err) + } + venv := filepath.Join(t.TempDir(), "runtime") + if err := makeVenv(py, venv); err != nil { + t.Fatalf("makeVenv: %v", err) + } + if ok, ver := venvUsable(venv); !ok { + t.Fatalf("an environment built from the interpreter setup just chose "+ + "reported itself unusable (%s)", ver) + } + + // The two states that must not be confused with a working one. + empty := filepath.Join(t.TempDir(), "gone") + if ok, _ := venvUsable(empty); ok { + t.Error("a missing environment was reported usable") + } + broken := filepath.Join(t.TempDir(), "broken") + if err := os.MkdirAll(filepath.Dir(venvPython(broken)), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(venvPython(broken), []byte("not an interpreter"), 0o755); err != nil { + t.Fatal(err) + } + if ok, ver := venvUsable(broken); ok { + t.Errorf("a half-created environment was reported usable (%s)", ver) + } +}