Package Lock File

Overview

The package lock file (packages/package-lock.json) records the fully-resolved identity of every fetched package. Unlike ivpm.yaml, which describes what you want (e.g. branch: main), the lock file records what you actually got (e.g. commit_resolved: a1b2c3d).

The lock file is written automatically as a side-effect of ivpm update and ivpm sync. It is a local artifact — it lives inside the packages/ directory alongside the fetched packages and is not committed to version control by default.

When you need to reproduce an exact workspace — for archival, CI, or debugging — copy the lock file to a stable location and pass it back to ivpm update --lock-file.

Key Properties

  • Complete transitive closure — every package fetched, including transitive dependencies, is recorded. There is no need to scan sub-package ivpm.yaml files during reproduction.

  • Source-type aware — each entry records the fields relevant to its source type (git commit hash, GitHub release tag, HTTP ETag, pip version, etc.).

  • Platform-agnostic version — for GitHub Releases (gh-rls), the resolved version tag (e.g. v2.3.1) is stored. The correct platform binary is resolved at fetch time, so the same lock file works across Linux, macOS, and Windows.

  • Reproducibility flag — packages sourced from local paths (dir, file) are still recorded but are marked "reproducible": false because they cannot be restored on a different machine.

  • Python package versions — the Python handler contributes the complete set of pip-installed package versions under the python_packages key, enabling full Python environment reproducibility. This works regardless of whether pip or uv was used to install.

  • Integrity checksum — a SHA-256 checksum of the lock file body is embedded in the sha256 field. IVPM warns (but does not fail) if the checksum does not match, allowing you to detect accidental manual edits.

Lock File Format

{
  "ivpm_lock_version": 1,
  "generated": "2024-01-15T10:23:00+00:00",
  "sha256": "...",
  "packages": {
    "my_git_lib": {
      "src": "git",
      "url": "https://github.com/org/my_git_lib.git",
      "branch": "main",
      "tag": null,
      "commit_requested": null,
      "commit_resolved": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
      "cache": null,
      "resolved_by": "root",
      "dep_set": "default",
      "reproducible": true
    },
    "my_tool": {
      "src": "gh-rls",
      "url": "https://github.com/org/my_tool",
      "version_requested": "latest",
      "version_resolved": "v2.3.1",
      "cache": null,
      "resolved_by": "root",
      "dep_set": null,
      "reproducible": true
    },
    "an_archive": {
      "src": "http",
      "url": "https://example.com/archive.tar.gz",
      "etag": "abc123",
      "last_modified": "Wed, 15 Jan 2024 00:00:00 GMT",
      "cache": null,
      "resolved_by": "my_git_lib",
      "dep_set": null,
      "reproducible": true
    },
    "requests": {
      "src": "pypi",
      "version_requested": ">=2.0",
      "version_resolved": "2.31.0",
      "resolved_by": "root",
      "dep_set": null,
      "reproducible": true
    },
    "local_lib": {
      "src": "dir",
      "path": "../../shared/local_lib",
      "resolved_by": "root",
      "dep_set": null,
      "reproducible": false
    }
  },
  "python_packages": {
    "certifi": "2024.1.1",
    "charset-normalizer": "3.3.2",
    "idna": "3.6",
    "requests": "2.31.0",
    "urllib3": "2.1.0"
  }
}

Fields common to all entries:

src

Source type: git, gh-rls, http, pypi, dir, file, etc.

resolved_by

The package that first introduced this dependency. "root" means it came directly from the top-level ivpm.yaml. The first (highest- priority) specification wins; lower-level duplicates are ignored.

dep_set

The dependency-set name used when loading sub-dependencies from this package.

reproducible

true for packages that can be restored on any machine. false for dir and file packages (local paths).

Change Detection

When ivpm update runs and a lock file already exists, IVPM compares the user-specified fields in ivpm.yaml against the corresponding lock entry for each package:

  • git: url, branch, tag, commit, cache

  • gh-rls: url, version

  • http: url

  • pypi: version

If the specs match, the package is considered up to date and no network calls are made. If the specs differ (e.g. you changed branch: main to branch: dev), IVPM reports the differences but does not re-fetch unless you also pass --refresh-all or --force.

# Reports differences, takes no action
$ ivpm update

# Re-fetches packages whose specs changed
$ ivpm update --refresh-all

# Re-fetches everything; suppresses safety errors
$ ivpm update --force

Reproduction Mode

Pass a lock file as input to ivpm update to reproduce an exact workspace:

$ ivpm update --lock-file ./ivpm.lock

In this mode:

  • ivpm.yaml is not read for packages — the lock file is the sole source of truth.

  • Every package is fetched at its pinned resolved version:

    • git — shallow clone at commit_resolved; falls back to full clone if the server does not support arbitrary commit fetch.

    • gh-rls — fetches the exact version_resolved tag; platform binary selection still occurs at fetch time.

    • http — fetches the same URL; warns if the ETag/Last-Modified header no longer matches.

    • pypi — installs version_resolved exactly.

  • Sub-package ivpm.yaml files are not scanned; the lock file already encodes the complete transitive closure.

  • The packages/ destination is still determined by the active ivpm.yaml (or the CLI default), not the lock file.

  • Cache interaction is unchanged: if the pinned version is already in the IVPM cache, it is reused.

Note

The lock file does not encode absolute paths. deps_dir can differ between the machine that generated the lock and the machine that consumes it.

CI/CD Reproducibility Pattern

The recommended workflow for CI reproducibility:

On the developer workstation:

# 1. Fetch/update packages normally
$ ivpm update

# 2. Archive the lock file (packages/ may be .gitignore'd)
$ cp packages/package-lock.json ./ivpm.lock

# 3. Commit the lock file alongside ivpm.yaml
$ git add ivpm.lock
$ git commit -m "Update dependency lock file"

In CI:

# Reproduce the exact workspace from the committed lock file
$ ivpm update --lock-file ./ivpm.lock

GitHub Actions example:

- name: Install IVPM
  run: pip install ivpm

- name: Reproduce workspace from lock
  run: ivpm update --lock-file ./ivpm.lock

- name: Run tests
  run: ivpm activate -c "pytest"

Syncing and the Lock File

ivpm sync pulls the latest upstream commits into all editable (writable) git packages. After sync completes, the lock file is automatically regenerated to reflect the new HEAD commit of each updated package.

This means packages/package-lock.json always represents the current state of your packages directory, whether packages were fetched by update or brought forward by sync.

$ ivpm sync
# ... merges upstream changes into editable packages ...
# Note: Updated package-lock.json after sync

Python Package Version Locking

After Python packages are installed into the IVPM-managed virtual environment, the Python handler queries the venv for all installed package versions using pip list. This works regardless of whether pip or uv was used to install packages — both write to the same venv site-packages directory.

The result is stored under the top-level python_packages key in the lock file. In reproduction mode (--lock-file), PyPI packages are pinned to their version_resolved value, which corresponds to the version recorded in python_packages.

Note

python_packages records all packages installed in the venv, including transitive pip dependencies that were not explicitly listed in ivpm.yaml.

Format Versioning

The ivpm_lock_version field guards against schema changes. IVPM will reject lock files with an unrecognised version number and emit a clear error:

ValueError: package-lock.json version 2 is not supported (expected 1).
Please regenerate the lock file with this version of ivpm.

See Also