Skip to content

ENG-2084: Install private wheels into the Python runtime's site-packages#3850

Open
icanhasmath wants to merge 4 commits into
masterfrom
marcg/eng-2084-private-wheel-site-packages
Open

ENG-2084: Install private wheels into the Python runtime's site-packages#3850
icanhasmath wants to merge 4 commits into
masterfrom
marcg/eng-2084-private-wheel-site-packages

Conversation

@icanhasmath

@icanhasmath icanhasmath commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

state publish --build private wheels were installed into a flat site-packages directory at the artifact deploy-tree root, which the deploy linked to <installdir>/site-packages. That is the wrong location — the wheel must land in the Python runtime's own site-packages, whose layout varies by platform and Python version:

  • Linux: <installdir>/lib/pythonX.Y/site-packages (also lib64, usr/lib variants)
  • Windows: <installdir>/lib/site-packages

Root cause

The install ran during the parallel unpack phase, before the Python runtime artifact was guaranteed to be on disk, so there was no reliable way to discover the real site-packages layout — the code hard-coded a flat site-packages.

What changed

The placement is now deferred until every artifact is unpacked, then the correct location is discovered on disk rather than guessed:

  1. During unpack, private wheels are only recorded (recordPrivateWheel) instead of installed inline.
  2. After the unpack worker pool drains, installPrivateWheels runs.
  3. locateSitePackages scans the unpacked non-private artifacts (Python runtime among them; private wheels skipped) for their site-packages: cheap globs first (lib/python*/site-packages, lib/site-packages, usr/lib/python*/site-packages, lib64/..., Windows Lib/site-packages), with a full-walk fallback for unexpected layouts. Returns the path relative to the install root, chosen deterministically (shortest, then lexicographic).
  4. installPrivateWheel lays the wheel out at <artifactDir>/<InstallDir>/<relSitePackages> so the normal deploy merges it into <installdir>/<relSitePackages> alongside Python's own files, and points PYTHONPATH at the discovered path (keeping the : separator / inherit=false directives from ENG-1793 to avoid the cross-artifact env-merge error).

Because the wheel now lands via the standard deploy path, depot uninstall/eviction tracking works with no special-casing.

Testing

  • New unit tests (pkg/runtime/sitepackages_test.go) cover glob matching across Linux/macOS/Windows/usr/lib64 layouts, the walk fallback, and cross-root dedup. pkg/runtime builds and tests pass.
  • Outstanding: end-to-end state install of a private-wheel project against a real ActiveState Python runtime, confirming files land in lib/pythonX.Y/site-packages and import correctly — not yet run (no runtime available in the working environment).

Jira: ENG-2084 (under epic ENG-1563)

🤖 Generated with Claude Code

icanhasmath and others added 3 commits July 17, 2026 19:36
The `state publish --build` private-ingredient consume path installed the
decrypted wheel into a flat `site-packages` at the artifact deploy-tree root,
which linked to `<installdir>/site-packages` -- the wrong location. The wheel
must land in the Python runtime's own site-packages, whose layout varies by
platform and Python version (e.g. `lib/pythonX.Y/site-packages` on Linux,
`lib/site-packages` on macOS).

The install previously ran during the parallel unpack phase, before the Python
runtime was guaranteed on disk, so the correct path could not be discovered.
Defer the placement until every artifact is unpacked, then locate the real
site-packages by scanning the unpacked non-private artifacts (glob known
layouts, full-walk fallback) and lay the wheel out at that same relative path so
the normal deploy merges it into the runtime's site-packages. PYTHONPATH now
points at the discovered path, matching the runtime's separator/inherit
directives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Python runtime's site-packages layout is deterministic per OS, so the
six-pattern glob list was overkill: Linux and macOS always use
usr/lib/pythonX.Y/site-packages, and Windows always uses Lib/site-packages
(case-insensitive filesystem). Collapse to a single OS-conditional pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
If the OS-specific glob does not find the runtime's site-packages, something is
wrong with the runtime -- fail fast rather than fully walking every artifact
tree looking for an unexpected layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes how decrypted private Python wheels are incorporated into a runtime so they are installed into the Python runtime’s actual site-packages directory (instead of a hard-coded flat ${INSTALLDIR}/site-packages), by deferring wheel installation until after all artifacts are unpacked and then locating site-packages on disk.

Changes:

  • Defer private wheel installation during unpack by recording wheel artifact dirs, then install them after the unpack worker pool completes.
  • Add locateSitePackages + glob-based discovery to find the runtime’s site-packages relative path and update wheel install location + PYTHONPATH accordingly.
  • Add unit tests for globSitePackages and cross-root deduplication.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
pkg/runtime/setup.go Defers private wheel install until after unpack; adds site-packages discovery and installs wheels into the discovered path, updating PYTHONPATH.
pkg/runtime/sitepackages_test.go Adds tests for the current globSitePackages behavior and deduplication across roots.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/runtime/setup.go Outdated
Comment on lines +726 to +742
func (s *setup) installPrivateWheels() error {
if len(s.privateWheels) == 0 {
return nil
}

relSitePackages, err := s.locateSitePackages()
if err != nil {
return errs.Wrap(err, "could not locate the Python runtime's site-packages directory")
}

for _, dir := range s.privateWheels {
if err := s.installPrivateWheel(dir, relSitePackages); err != nil {
return errs.Wrap(err, "could not install private wheel in %s", dir)
}
}
return nil
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the cleanup — fixed in e8954f6. On failure installPrivateWheels now RemoveAlls each decrypted wheel directory, so no private plaintext lingers and the next run re-downloads and retries (the depot rebuilds its artifact set from a disk scan, so a leftover dir would otherwise be treated as already obtained). This restores the semantics the old inline path had via its os.RemoveAll(unpackPath).

On the mutex: the read here is safe without the lock. recordPrivateWheel writes s.privateWheels only from the unpack worker pool, and installPrivateWheels runs on the main goroutine after wp.Wait() has drained that pool — the Wait is a happens-before barrier, so the slice is fully published and no longer written concurrently. I added a comment noting this rather than taking a lock that would imply a concurrency that doesn't exist.

Comment thread pkg/runtime/setup.go
Comment on lines +837 to +847
// sitePackagesGlob is the relative location of the Python runtime's
// site-packages directory for the current OS. Windows uses a flat, unversioned
// Lib/site-packages (the filesystem is case-insensitive, so "lib" matches too);
// every other OS uses usr/lib/pythonX.Y/site-packages, with the version segment
// left as a wildcard to be resolved on disk.
func sitePackagesGlob() string {
if sysinfo.OS() == sysinfo.Windows {
return filepath.Join("Lib", "site-packages")
}
return filepath.Join("usr", "lib", "python*", "site-packages")
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a deliberate scoping decision. prepare_mac.go is part of the camel (legacy) builder path, which is entirely separate from this feature — state publish --build private ingredients flow through the modern alternative builder (the setup.go deploy path), whose runtimes are relocated under a single install root with a deterministic layout: usr/lib/pythonX.Y/site-packages on Linux/macOS and Lib/site-packages on Windows. That per-OS layout was confirmed by the team, which is why this collapses to one pattern per OS rather than enumerating camel's framework layouts.

The behavior on an unmatched layout is intentional: locateSitePackages returns an error and the install fails fast rather than silently landing the wheel somewhere wrong. If a private ingredient ever targets a runtime with a different layout, that error is the signal to extend sitePackagesGlob — better than guessing.

Comment on lines +18 to +33
func TestGlobSitePackages(t *testing.T) {
// The expected layout is OS-specific (see sitePackagesGlob).
layout := filepath.Join("usr", "lib", "python3.10", "site-packages")
if sysinfo.OS() == sysinfo.Windows {
layout = filepath.Join("Lib", "site-packages")
}

tests := []struct {
name string
layout string // relative dir to create under the root, "" for none
want string // expected relative site-packages path, "" for no match
}{
{"matching layout", layout, layout},
{"no site-packages", filepath.Join("bin"), ""},
{"unexpected layout not globbed", filepath.Join("opt", "python", "site-packages"), ""},
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional — the tests track the deterministic per-OS layout the alternative-builder runtimes actually use (see the reply on sitePackagesGlob). They're OS-aware (usr/lib/pythonX.Y/site-packages off-Windows, Lib/site-packages on Windows) rather than asserting a fixed string, so they follow sitePackagesGlob if it changes. If we ever need to support additional layouts, both the glob and these cases get extended together.

Deferring the private-wheel install lost the failure cleanup the inline path
had. On failure, remove the decrypted wheel artifact directories so no private
plaintext lingers and a later run re-downloads and retries (the depot rebuilds
its artifact set from a disk scan, so a leftover directory would otherwise be
treated as already obtained and never reinstalled).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants