diff --git a/.nextchanges/cli/labs-venv-missing-interpreter.md b/.nextchanges/cli/labs-venv-missing-interpreter.md new file mode 100644 index 00000000000..1dea04bba9b --- /dev/null +++ b/.nextchanges/cli/labs-venv-missing-interpreter.md @@ -0,0 +1 @@ +Recreate a labs virtualenv when its Python interpreter is missing so `databricks labs install` recovers after the previous interpreter is removed. diff --git a/cmd/labs/project/installer.go b/cmd/labs/project/installer.go index 32a74b6808f..bb5e97b804b 100644 --- a/cmd/labs/project/installer.go +++ b/cmd/labs/project/installer.go @@ -239,7 +239,15 @@ func (i *installer) setupPythonVirtualEnvironment(ctx context.Context, w *databr venvPath := i.virtualEnvPath(ctx) log.Debugf(ctx, "Creating Python Virtual Environment at: %s", venvPath) sp.Update("Creating Virtual Environment with Python " + py.Version) - _, err = process.Background(ctx, []string{py.Path, "-m", "venv", venvPath}) + venvArgs := []string{py.Path, "-m", "venv", venvPath} + // python -m venv does not replace existing interpreter symlinks unless --clear + // is passed. A leftover venv after Homebrew retires its Python then makes + // every later labs install fail on a dangling bin/python3. + if shouldRecreateVenv(venvPath, i.virtualEnvPython(ctx)) { + log.Infof(ctx, "Existing virtualenv at %s has a missing interpreter, recreating it", venvPath) + venvArgs = []string{py.Path, "-m", "venv", "--clear", venvPath} + } + _, err = process.Background(ctx, venvArgs) if err != nil { return fmt.Errorf("create venv: %w", err) } @@ -270,6 +278,14 @@ func (i *installer) setupPythonVirtualEnvironment(ctx context.Context, w *databr return i.installPythonDependencies(ctx, ".") } +func shouldRecreateVenv(venvPath, pythonBin string) bool { + if _, err := os.Stat(venvPath); err != nil { + return false + } + _, err := os.Stat(pythonBin) + return err != nil +} + func (i *installer) installPythonDependencies(ctx context.Context, spec string) error { if !i.IsPythonProject() { return nil diff --git a/cmd/labs/project/installer_venv_test.go b/cmd/labs/project/installer_venv_test.go new file mode 100644 index 00000000000..94a0347c215 --- /dev/null +++ b/cmd/labs/project/installer_venv_test.go @@ -0,0 +1,41 @@ +package project + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestShouldRecreateVenv(t *testing.T) { + dir := t.TempDir() + venvPath := filepath.Join(dir, "venv") + pythonBin := filepath.Join(venvPath, "bin", "python3") + if runtime.GOOS == "windows" { + pythonBin = filepath.Join(venvPath, "Scripts", "python.exe") + } + + require.False(t, shouldRecreateVenv(venvPath, pythonBin)) + + require.NoError(t, os.MkdirAll(filepath.Dir(pythonBin), 0o755)) + require.True(t, shouldRecreateVenv(venvPath, pythonBin)) + + require.NoError(t, os.WriteFile(pythonBin, []byte("python"), 0o755)) + require.False(t, shouldRecreateVenv(venvPath, pythonBin)) +} + +func TestShouldRecreateVenvDanglingInterpreter(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink dangling interpreter check is unix specific") + } + + dir := t.TempDir() + venvPath := filepath.Join(dir, "venv") + binDir := filepath.Join(venvPath, "bin") + pythonBin := filepath.Join(binDir, "python3") + require.NoError(t, os.MkdirAll(binDir, 0o755)) + require.NoError(t, os.Symlink(filepath.Join(dir, "missing-python"), pythonBin)) + require.True(t, shouldRecreateVenv(venvPath, pythonBin)) +}