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) + } +}