Commit c0f89be
Add PEP 723 inline script environment creation (PEP 723 PR 5c/16) (#1656)
> Part of #1602 (PEP 723 inline script env support). Design doc: #1601.
> **Split for review (3 PRs).** Reviewers flagged the original PR 5 as
too large, so it is split into three PRs grouped by dependency layer:
> - **5a — generic env-creation utilities — #1651.** Merged.
> - **5b — inline-script cache + interpreter utilities — #1655.**
Merged.
> - **5c — `create()` happy path (manager + wiring) — this PR (#1656).**
Rebased on `main`.
>
> #1651 and #1655 have merged and this branch has been rebased. The diff
now contains only this PR's five files.
### Roadmap context
This is the final slice of **PR 5 of 16** — the actual `create()` happy
path. See #1651 for the full roadmap table.
| Phase 2: Manager | PR | Status |
|---|---|---|
| | PR 4: `InlineScriptEnvManager` skeleton | merged (#1610) |
| | PR 5a: generic env-creation utilities | merged (#1651) |
| | PR 5b: inline-script cache + interpreter utilities | merged (#1655)
|
| | **PR 5c: `create()` happy path (manager + wiring)** | **this PR
(#1656)** |
| | PR 6: `create()` uv-install fallback | not started (needs 3, 5) |
### Why this PR
`InlineScriptEnvManager.create()` was a deliberately empty no-op after
PR 4. This PR implements its **happy path**: the case where the machine
already has a base interpreter that satisfies the script's
`requires-python`, so no uv Python install is required. Given a PEP 723
script, it builds — or reuses — a dependency-keyed virtual environment
under the extension's global storage, following the pipx-style cache
design from Q4 of #1601. The uv-install fallback (no compatible
interpreter present) is deferred to PR 6.
It composes the primitives from 5a (#1651) and the inline-script
utilities from 5b (#1655); this PR adds only the manager and its wiring.
### What this PR does
**Wires the manager's collaborators** (`extension.ts`,
`inlineScriptMain.ts`): `registerInlineScriptFeatures` and the
`InlineScriptEnvManager` constructor now receive the
`NativePythonFinder`, the `PythonEnvironmentApi`, the base (system)
environment manager, and `globalStorageUri`.
**Implements `create(scope)`** (`inlineScriptEnvManager.ts`):
- Accepts exactly one local `file:` URI (a bare `Uri` or single-element
array). Anything else — `'global'`, a folder, or multiple URIs — logs a
warning and returns `undefined`.
- Reads PEP 723 metadata from the script; missing or invalid metadata
returns `undefined`.
- Merges `metadata.dependencies` with `options.additionalPackages`,
trims each, and rejects empty entries.
- Selects a base interpreter, computes the dependency + interpreter
cache key, and de-duplicates concurrent in-process `create()` calls for
the same key via a `pendingCreations` map.
**Base-interpreter selection** (`selectBaseInterpreter`): starts from
`getEnvironments('global')`, keeps only true base managers (system,
pyenv, conda `base`), and excludes derived environments by rejecting a
non-absolute `sysPrefix` or the presence of `pyvenv.cfg`. It then picks
the newest compatible interpreter with `pickCompatibleInterpreter` and
resolves the executable through `fs.realpath` so the cache key is
canonical. If a candidate cannot be resolved it falls through to the
next.
**Create-or-reuse under a cross-process lock**
(`createOrReuseEnvironment`): acquires a directory lock (5a), inspects
the existing cache entry, and reuses / rebuilds / preserves accordingly,
always releasing the lock in `finally`.
**Fail-closed cache inspection** (`inspectCacheEntry`) returns `absent |
stale | uncertain | reusable`:
- Rejects symlinks and non-directories; verifies the entry is contained
under the cache root (`resolveCacheEntryPath`).
- Reads and validates the `.meta.json` sidecar and confirms the recorded
base-interpreter path and version still match the selected base.
- Confirms the base interpreter is still present on disk
(`getBaseInterpreterStatus`).
- Resolves the cached venv to a real `PythonEnvironment`, confirms it is
genuinely ours via realpath containment (`inspectOwnedCacheEntry`),
compares Python release segments, and re-checks `requires-python` with
the existing `matchesPythonVersion`.
- Only conclusive evidence marks an entry `stale` (rebuild); any doubt
yields `uncertain`, and an uncertain entry is **preserved, never
deleted**. A reused entry has its `lastUsedAt` refreshed.
**Environment build** (`buildCacheEntry`): delegates to the existing
`createWithProgress` venv flow with `trackUvEnvironment` set to `false`
so cached script environments are not registered as workspace venvs. On
success it writes the sidecar and re-validates that the built
environment matches the requested release and is owned by this entry. On
failure it removes the directory and returns empty. On **cancellation**
it retains the lock so a half-built environment is not silently reused
later.
### Tests
- **`inlineScriptEnvManager.unit.test.ts`** — 40 tests across
scope/metadata validation, base-interpreter selection, cache creation,
cache reuse, transaction rollback, and events/disposal.
- **`inlineScriptMain.unit.test.ts`** — updated for the new
`registerInlineScriptFeatures` signature.
On this rebased branch `npm run compile-tests` is clean and `npm run
unittest` reports **1491 passing, 0 failing, 5 pending**.
### User impact
**None on the default path.** The manager is still registered only when
the undeclared `python-envs.inlineScripts.enabled` flag is on, so
default users see nothing.
`create()` is now a declared method (PR 4 omitted it), so with the flag
on the inline manager can appear as a create target. But it acts only on
a single local script URI and no-ops on every other scope, and **nothing
in the extension routes a script URI to it yet**. Wiring the trigger is
later work: routing in PR 9, and the "Set up env for this script" picker
item and bulk command in PR 11/12.
### Merge order
#1651 and #1655 have merged. This PR is the remaining final slice.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 39dcc6a3-0fbd-4f36-9d0f-68677de49c271 parent 58f231d commit c0f89be
10 files changed
Lines changed: 1246 additions & 118 deletions
File tree
- src
- common
- utils
- managers
- builtin
- pyenv
- test
- common
- managers/builtin
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
273 | 274 | | |
274 | 275 | | |
275 | 276 | | |
276 | | - | |
277 | | - | |
278 | | - | |
279 | | - | |
280 | 277 | | |
281 | 278 | | |
282 | 279 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
657 | 657 | | |
658 | 658 | | |
659 | 659 | | |
660 | | - | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
661 | 670 | | |
662 | 671 | | |
663 | 672 | | |
| |||
0 commit comments