Add inline-script cache and interpreter utilities (PEP 723 PR 5b/16) - #1655
Conversation
09b7664 to
7363231
Compare
> 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 stacked PRs grouped by dependency layer: > - **5a — generic env-creation utilities — this PR (#1651).** Based on `main`; independent; merges first. > - **5b — inline-script cache + interpreter utilities — #1655.** Stacked on 5a. > - **5c — `create()` happy path (manager + wiring) — #1656.** Stacked on 5b. > > Applied together the three PRs are byte-for-byte identical to the original single change. **Merge order: 5a → 5b → 5c.** ### Roadmap context This is the first slice of **PR 5 of 16** in the PEP 723 inline-script roadmap. The full plan lives in #1602. | Phase | PR | Status | |---|---|---| | **Phase 1: Foundation** | PR 1: cache key hash utility | merged (#1634) | | | PR 2: cache layout + `meta.json` sidecar | merged (#1635) | | | PR 3: `requires-python` to interpreter selection | merged (#1636) | | **Phase 2: Manager** | PR 4: `InlineScriptEnvManager` skeleton | merged (#1610) | | | **PR 5a: generic env-creation utilities** | **this PR (#1651)** | | | **PR 5b: inline-script cache + interpreter utilities** | **#1655** | | | **PR 5c: `create()` happy path (manager + wiring)** | **#1656** | | | PR 6: `create()` uv-install fallback | not started (needs 3, 5) | | | PR 7: persistence with `get`, `set`, and Memento | not started (needs 4) | | | PR 8: activation-time discovery | not started (needs 2, 4, 7) | | **Phase 3: Routing** | PR 9: route PEP 723 scripts to the inline manager | not started (needs 4, 7) | | | PR 10: per-script project registration | not started (needs 9) | | **Phase 4+: UX / lifecycle** | PRs 11-16 | not started | ### Why this PR PR 5c implements `InlineScriptEnvManager.create()`. Before touching the manager, this PR lands the **generic, reusable primitives** it relies on — a cross-process file lock, a venv Python-path helper, a cancellation-hardened process runner, and two small `createWithProgress` options. None of this code is inline-script-specific, so it is reviewed on its own. ### What this PR adds **Cross-process file lock** (`src/common/lockfile.apis.ts`, new): `acquireFileLock` uses an atomic `mkdir` of a `<path>.lock` directory plus a per-owner marker file, returning `AcquiredFileLock { release, retain }`. `retain()` writes a `retained` marker so a later acquirer **fails fast with `ELOCKRETAINED`** instead of waiting out the 5-minute timeout — used when a build is cancelled mid-flight. Distinct error codes (`ELOCKED`, `ELOCKRETAINED`, `ELOCKORPHANED`, `ECOMPROMISED`, `ERETAINFAILED`) separate contention from corruption. **Shared `getVenvPythonPath`** (`src/common/utils/virtualEnvironment.ts`, new): returns `Scripts\python.exe` on Windows, else `bin/python`. Replaces an inline copy in `venvUtils` and is reused by 5b/5c. **Hardened process helper** (`src/managers/builtin/helpers.ts`): `runUV` and `runPython` now share one `runProcess` implementation whose cancellation guards `kill()` in `try/catch` and still emits a clean `CancellationError` if the process errors after a cancel. Per-caller options preserve existing behavior (`collectStderr`, `logPrefix`). **`venvUtils.ts`:** `createWithProgress` gains `CreateWithProgressOptions { trackUvEnvironment }`, and `CreateEnvironmentResult` gains `pkgInstallationCancelled` so a caller can tell cancellation apart from a real install failure. Existing callers are unaffected (both are optional / additive). ### Tests - **`lockfile.apis.unit.test.ts`** — 9 tests: contention, retain/fail-fast, orphaned and compromised locks, and timeout. - **`virtualEnvironment.unit.test.ts`** — 2 tests for `getVenvPythonPath` on Windows and POSIX. - **`helpers.cancellation.unit.test.ts`** — 4 tests for `runProcess` cancellation safety. - **`venvUtils.createWithProgress.unit.test.ts`** — 3 tests for `trackUvEnvironment` and `pkgInstallationCancelled`. On this branch alone `npm run compile-tests` is clean and `npm run unittest` reports **1447 passing, 0 failing, 4 pending**. ### User impact **None.** These are internal primitives with no new user-visible behavior. The refactors to `helpers.ts` and `venvUtils.ts` are behavior-preserving for existing callers. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39dcc6a3-0fbd-4f36-9d0f-68677de49c27
7363231 to
2bbc6a5
Compare
2bbc6a5 to
a98295d
Compare
Test verification reportMust fix
Summary
Verification evidence
|
Cache-key tail normalization, cache-layout ownership/status checks with typed sidecar reads, and interpreter-constraint trimming that inline-script environment creation builds on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39dcc6a3-0fbd-4f36-9d0f-68677de49c27
d9a6511 to
a64dbff
Compare
edvilme
left a comment
There was a problem hiding this comment.
2 comments/questions but otherwise, looks good to me ;)
| } | ||
|
|
||
| /** ` >= 2 ; python_version < "3.13"` becomes `>=2 ; python_version<"3.13"`. */ | ||
| function normalizeRequirementTail(value: string): string { |
There was a problem hiding this comment.
I am not entirely sure, but I think this could be further simplified with some regexes rather than iterating over the characters. If not, this looks good to me :)
There was a problem hiding this comment.
Looking to address the feedback while getting 5c pr ready. I considered this, but the loop is intended to be quotes aware. It normalizes whitespace outside marker literals while preserving it inside quoted values, and include quotes. It's difficult to get regex to do that. I'd keep it this way for now.
| function isDescendantPath(rootPath: string, candidatePath: string): boolean { | ||
| const relative = path.relative(rootPath, candidatePath); | ||
| return ( | ||
| relative.length > 0 && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Perhaps we could see if there are other places we could use this in, and put it on an utils file or something similar?
There was a problem hiding this comment.
venv and pip utils have something similar, but for different purposes and different behavior. This one rejects the root itself on purpose because we don't want to accidentally touch the root of the cached envs and modify every cached envs.
Roadmap context
This is the second slice of PR 5 of 16. See #1651 for the full roadmap table.
InlineScriptEnvManagerskeletoncreate()happy path (manager + wiring)Why this PR
With the generic primitives from 5a in place, this PR lands the inline-script-specific utilities that
create()(5c) composes: a normalized dependency cache key, cache-layout ownership/status checks, and interpreter-constraint handling. These are pure functions with no manager wiring yet, so they are reviewed on their own.What this PR adds
Cache-key tail normalization (
src/common/inlineScriptCacheKey.ts): addsnormalizeRequirementTail, a quote-aware scanner that collapses whitespace and tightens comparator spacing (>= 1.0→>=1.0) in a requirement's version/marker tail while preserving quoted PEP 508 marker literals verbatim (e.g.python_version >= "3.11"). Direct-reference requirements (pkg @ https://…) are kept verbatim after the name and extras. The effect is that semantically identical dependency strings normalize to the same cache key, so they reuse the same cached environment.Cache-layout additions (
src/common/inlineScriptCacheLayout.ts):resolveCacheEntryPath(containment under the cache root),inspectOwnedCacheEntry(realpath ownership),getBaseInterpreterStatus(available | missing | unavailable),inspectMetaJson(typed sidecar read), and a strictervalidateMeta. The.meta.jsonsidecar schema is{ schemaVersion, baseInterpreterPath, baseInterpreterVersion, lastUsedAt }. UsesgetVenvPythonPathfrom merged PR #1651.Interpreter-constraint trimming (
src/common/inlineScriptInterpreter.ts):pickCompatibleInterpreternow trimsrequires-python, so a whitespace-only constraint is treated as no constraint.Manager ID constants (
src/common/constants.ts): centralizes the conda and inline-script manager IDs used by interpreter filtering.Tests
inlineScriptCacheKey.unit.test.ts— canonicalization cases including marker literals and direct references.inlineScriptCacheLayout.unit.test.ts— the new containment, ownership, base-interpreter-status, and typed sidecar-read helpers.inlineScriptInterpreter.unit.test.ts— constraint trimming / selection.On this rebased branch
npm run compile-testsis clean andnpm run unittestreports 1467 passing, 0 failing, 5 pending.User impact
None. These are pure utilities. Nothing calls the new code paths until the manager lands in 5c (#1656).
Merge order
#1651 has merged. Merge this PR next, then #1656.