Repository navigation
Honor UV_PROJECT_ENVIRONMENT in environments setup-local #7026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
446e187
d6e297b
0bc8887
aeff535
adb855e
9c9db49
6153b59
5131bd3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| * `databricks environments setup-local` now uses the environment in `UV_PROJECT_ENVIRONMENT` when it is set, as `uv sync` does. ([#7026](https://github.com/databricks/cli/pull/7026)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| [project] | ||
| name = "a" | ||
| version = "0.1.0" | ||
| requires-python = ">=3.10" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| #!/bin/bash | ||
| # The workspace root is the test folder, where this script is installed as uv. | ||
| # Like real uv, `uv sync` creates the environment at UV_PROJECT_ENVIRONMENT | ||
| # (relative to the workspace root), with a fake interpreter that answers the | ||
| # validate probe (prefixes from libs/localenv/uv.go). | ||
| root="$(cd "$(dirname "$0")" && pwd)" | ||
| venv="$root/${UV_PROJECT_ENVIRONMENT:-.venv}" | ||
| case "$1 $2" in | ||
| "--version ") echo "uv 0.12.9 (fake)"; exit 0 ;; | ||
| "workspace dir") echo "$root"; exit 0 ;; | ||
| "python install") exit 0 ;; | ||
| "pip install") exit 0 ;; | ||
| "sync --python") | ||
| mkdir -p "$venv/bin" | ||
| cat > "$venv/bin/python" <<'PY' | ||
| #!/bin/bash | ||
| echo "PYVER:3.12" | ||
| echo "DBCVER:17.2.0" | ||
| echo "PYSPARKVER:" | ||
| echo "DBCIMPORT:" | ||
| PY | ||
| chmod +x "$venv/bin/python" | ||
| exit 0 ;; | ||
| esac | ||
| echo "fake uv: unexpected call: $*" >&2 | ||
| exit 1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Cloud = false | ||
| GOOS.windows = false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
|
|
||
| >>> [CLI] environments setup-local --serverless-version 4 | ||
| warning: requires-python ">=3.10" is replaced by the environment's ">=3.12" | ||
| ✔ Local environment ready | ||
|
|
||
| Compute target serverless 4 | ||
| Python 3.12 | ||
| databricks-connect 17.2.0 | ||
| Virtual env ../../custom env | ||
| pyproject.toml updated (backup: [TEST_TMP_DIR]/bundles/a/pyproject.toml.bak) | ||
| uv workspace root [TEST_TMP_DIR] | ||
| Root pyproject.toml constraints updated (backup: [TEST_TMP_DIR]/pyproject.toml.bak) | ||
|
|
||
| Next steps: | ||
| • Activate it: source '../../custom env/bin/activate' | ||
| • Or select ../../custom env as the Python interpreter in VS Code / Cursor |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| [project] | ||
| name = "root" | ||
| version = "0.1.0" | ||
| requires-python = ">=3.10" | ||
|
|
||
| [tool.uv.workspace] | ||
| members = ["bundles/*"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # With UV_PROJECT_ENVIRONMENT set, the shared environment is that folder | ||
| # (relative to the workspace root), not .venv. The name holds a space, so the | ||
| # activate hint quotes it. A fake uv on PATH stands in for the | ||
| # network-dependent sync (see acceptance/cmd/psql). | ||
| mv fake-uv.sh uv | ||
| chmod +x uv | ||
| cleanup() { | ||
| rm -f uv | ||
| } | ||
| trap cleanup EXIT | ||
| export PATH="$(pwd):$PATH" | ||
| export UV_PROJECT_ENVIRONMENT="custom env" | ||
|
|
||
| withdir bundles/a trace $CLI environments setup-local --serverless-version 4 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # The fake uv is a bash script, so this test cannot run on Windows runners | ||
| # (mirrors acceptance/cmd/psql). | ||
| GOOS.windows = false | ||
|
|
||
| # The run writes both pyproject.toml files, their backups, and the shared | ||
| # environment "custom env" at the workspace root. | ||
| Ignore = ["custom env", "pyproject.toml.bak", "bundles/a/pyproject.toml.bak"] | ||
|
|
||
| Env.DATABRICKS_LOCALENV_CONSTRAINT_SOURCE_URL_TEST_OVERRIDE = "$DATABRICKS_HOST" | ||
|
|
||
| [[Server]] | ||
| Pattern = "GET /serverless/serverless-v4/pyproject.toml" | ||
| Response.Body = ''' | ||
| [project] | ||
| requires-python = ">=3.12" | ||
|
|
||
| [dependency-groups] | ||
| dev = ["databricks-connect~=17.2.0"] | ||
|
|
||
| [tool.uv] | ||
| constraint-dependencies = ["pyarrow<19", "pandas<3"] | ||
| ''' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package localenv | ||
|
|
||
| import ( | ||
| "os" | ||
| "testing" | ||
| ) | ||
|
|
||
| // TestMain clears uv's UV_PROJECT_ENVIRONMENT from the host shell: it moves the | ||
| // venv the pipeline uses, and most tests expect the default .venv. Tests that | ||
| // need it set it on the context. | ||
| func TestMain(m *testing.M) { | ||
| os.Unsetenv(EnvUvProjectEnvironment) //nolint:usetesting // t.Setenv cannot unset | ||
| m.Run() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,10 +7,12 @@ import ( | |
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "runtime" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/databricks/cli/libs/atomicfile" | ||
| "github.com/databricks/cli/libs/env" | ||
| "github.com/databricks/cli/libs/log" | ||
| "github.com/hexops/gotextdiff" | ||
| "github.com/hexops/gotextdiff/myers" | ||
|
|
@@ -77,8 +79,9 @@ type Pipeline struct { | |
| // inspects. Set in preflight. | ||
| venv string | ||
|
|
||
| // venvRel is the venv folder relative to ProjectDir, reported as | ||
| // Result.VenvPath. Set in preflight. | ||
| // venvRel is the venv folder relative to ProjectDir (absolute when | ||
| // UV_PROJECT_ENVIRONMENT is absolute), reported as Result.VenvPath. Set in | ||
| // preflight. | ||
| venvRel string | ||
|
|
||
| // workspaceRoot is the uv workspace root when ProjectDir is a workspace | ||
|
|
@@ -217,18 +220,18 @@ func (p *Pipeline) run(ctx context.Context) error { | |
| return p.fail(PhasePreflight, false, NewError(ErrCanceled, err, "interrupted")) | ||
| } | ||
| venvBase := p.ProjectDir | ||
| p.venvRel = venvDir | ||
| if p.workspaceRoot != "" { | ||
| venvBase = p.workspaceRoot | ||
| p.venvRel = filepath.Join(rootRel, venvDir) | ||
| p.res.UvWorkspaceRoot = filepath.ToSlash(p.workspaceRoot) | ||
| if !p.Check { | ||
| if err := ensureWritable(p.workspaceRoot); err != nil { | ||
| return p.fail(PhasePreflight, false, NewError(ErrNotWritable, err, "uv workspace root %s is not writable", filepath.ToSlash(p.workspaceRoot))) | ||
| } | ||
| } | ||
| } | ||
| p.venv = filepath.Join(venvBase, venvDir) | ||
| if err := p.setVenv(ctx, venvBase, rootRel); err != nil { | ||
| return err | ||
| } | ||
| p.markOK(PhasePreflight, detail) | ||
|
|
||
| // Phase: resolve — compute target → environment key. | ||
|
|
@@ -326,6 +329,49 @@ func (p *Pipeline) run(ctx context.Context) error { | |
| return p.validate(ctx, pyMinor, c.DatabricksConnect, opts) | ||
| } | ||
|
|
||
| // setVenv sets the venv folder where uv sync puts the project environment: | ||
| // UV_PROJECT_ENVIRONMENT when set, else .venv in venvBase (the workspace root | ||
| // or ProjectDir). rootRel is venvBase relative to the symlink-resolved | ||
| // ProjectDir ("" outside a workspace; see discoverWorkspaceRoot). | ||
| func (p *Pipeline) setVenv(ctx context.Context, venvBase, rootRel string) error { | ||
| name := env.Get(ctx, EnvUvProjectEnvironment) | ||
| switch { | ||
| case name == "": | ||
| p.venv = filepath.Join(venvBase, venvDir) | ||
| p.venvRel = filepath.Join(rootRel, venvDir) | ||
| case filepath.IsAbs(name): | ||
| // uv uses an absolute value as is. It is reported as is too: a relative | ||
| // path cannot cross drives. | ||
| p.venv = filepath.Clean(name) | ||
| p.venvRel = p.venv | ||
| case filepath.VolumeName(name) != "": | ||
| // A Windows drive-relative value such as C:envs\x names a folder on that | ||
| // drive; uv uses it as is. | ||
| p.venv = filepath.Clean(name) | ||
| p.venvRel = p.venv | ||
| case strings.HasPrefix(filepath.ToSlash(name), "/"): | ||
| // A Windows rooted path such as \envs\x: uv keeps only the drive of the | ||
| // root for it. | ||
| p.venv = filepath.Clean(filepath.VolumeName(venvBase) + name) | ||
| p.venvRel = p.venv | ||
| default: | ||
| // uv joins a relative value to its working folder. On Unix that folder is | ||
| // symlink-resolved, so ".." in the value climbs the real folders; Windows | ||
| // keeps the path as given and resolves ".." by text. | ||
| base := venvBase | ||
| if runtime.GOOS != "windows" { | ||
| resolved, err := filepath.EvalSymlinks(venvBase) | ||
| if err != nil { | ||
| return p.fail(PhasePreflight, false, NewError(ErrNotWritable, err, "cannot resolve %s to place %s=%s", filepath.ToSlash(venvBase), EnvUvProjectEnvironment, filepath.ToSlash(name))) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] A relative
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping |
||
| } | ||
| base = resolved | ||
| } | ||
| p.venv = filepath.Join(base, name) | ||
| p.venvRel = filepath.Join(rootRel, name) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // resolve runs ResolveCompute and records the resolve phase. | ||
| func (p *Pipeline) resolve(ctx context.Context) (*ComputeInfo, error) { | ||
| compute, err := ResolveCompute(ctx, p.Flags, p.Compute, p.Bundle) | ||
|
|
@@ -751,11 +797,11 @@ func (p *Pipeline) validate(ctx context.Context, expectedPyMinor, dbcPin string, | |
| } | ||
| p.markOK(PhaseValidate, detail) | ||
|
|
||
| // venvPath is reported relative to the project root (spec §6.1), not as an | ||
| // absolute path: the VS Code consumer already knows the project root (it sets | ||
| // the working directory when it shells out). For a standalone project this is | ||
| // ".venv"; for a workspace member it is computed between the symlink-resolved | ||
| // folders (see discoverWorkspaceRoot), for example "../../.venv". | ||
| // venvPath is reported relative to the project root (spec §6.1): the VS Code | ||
| // consumer already knows the project root (it sets the working directory | ||
| // when it shells out). By default it is ".venv", or a path such as | ||
| // "../../.venv" for a workspace member (see discoverWorkspaceRoot); it is | ||
| // absolute only when UV_PROJECT_ENVIRONMENT is absolute. | ||
| p.res.VenvPath = filepath.ToSlash(p.venvRel) | ||
| if p.res.Resolved != nil { | ||
| if managed { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[medium]
ensureWritable(workspaceRoot)can reject a run that never writes to the root.For a workspace-member run with
--no-constraintsand an out-of-rootUV_PROJECT_ENVIRONMENT,planRootMergereturns nil (SkipConstraints) so the rootpyproject.tomlis never written, anduv synccreates the venv at the out-of-root path — yet a read-only workspace root fails preflight withErrNotWritablethough nothing would be written there. Consider gating this check on whether the root is actually going to be written.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The root must stay writable in this case too:
uv synccreates or updatesuv.lockat the workspace root, also with--no-constraintsand an out-of-root venv. So the preflight check catches a failure thatuv syncwould hit later.