Environment Stack File Formats

File naming and formats

By convention, virtual environment stacks are specified in a file named venvstacks.toml. This convention is NOT enforced, so the CLI will accept any TOML file containing a correctly formatted stack definition.

The package installation tool used to lock and build environments is uv. While the exact configuration settings used when invoking uv are generated dynamically by venvstacks, baseline settings may be provided via either a venvstacks.uv.toml file in the same folder as the stack definition file or by defining a [tool.uv] table in the stack definition file. If both are present, the inline table configuration settings are used and the separate configuration file is ignored (note: this is the opposite of the way uv works for regular Python projects, where the presence of a uv.toml file overrides the settings in pyproject.toml).

The format of the venvstacks.uv.toml configuration file is that described for uv.toml in uv’s tool documentation. The [tool.uv] table supports the same settings, nested under the tool.uv subtable. Many of the available configuration settings are not relevant to the venvstacks use case, but at least the following are expected to be applicable:

  • index: specifying which indexes to use for dependency resolution and retrieval

  • sources: specifying indexes to use for packages (overriding the default priorities)

  • exclude-newer: ignore packages uploaded after the specified date

  • override-dependencies: override inaccurate or unwanted dependency declarations

Note that the index and sources settings may be overridden when locking and building specific layers via the priority indexes and package indexes layer settings respectively. The sources field in the common configuration is also restricted to only referring to named indexes.

The default output folder for layer metadata when publishing artifacts and locally exporting environments is called __venvstacks__. The platform-specific layer summary metadata files are called venvstacks.json and each is written to a folder named after the target platform in the parent metadata folder. The per-layer metadata files are written to an env_metadata folder within the platform folders. Refer to Published layer metadata for additional details.

The layer configuration metadata within deployed environments is written to share/venv/metadata/venvstacks_layer.json. Refer to Deployed layer configuration for additional details.

All human-edited input files are written using TOML, as this is a file format that combines the runtime simplicity and Unicode text compatibility of JSON with the line-oriented human friendliness of the classic .ini format. It is the same config file syntax used to define pyproject.toml when publishing Python packages.

All output metadata files generated by the build process are emitted as JSON.

Changed in version 0.8.0: Added support for configuring uv execution and having it affect both locking and building (release details). User and system level default uv configuration settings no longer affect the behaviour of the locking process.

Defining virtual environment stacks

Virtual environment stacks are defined using the following top-level fields, which are all TOML arrays of tables:

  • [[runtimes]]

  • [[frameworks]]

  • [[applications]]

Common layer specification fields

All layer specifications must contain the following two fields:

  • name (string): the name of the layer being specified

  • requirements (array of strings): the top-level Python distribution packages to be installed as part of this layer. Dependencies are declared using the standard Python dependency specifier format. These declared dependencies will be transitively locked when locking the layer. The list of requirements must be present, but is permitted to be empty.

While there are no formal restrictions on the symbols permitted in layer names, it is recommended to avoid the use of any punctuation symbols other than _ and - (as future releases may assign specific semantics to those symbols). The @ symbol in particular is already used to separate the layer name from the lock version for implicitly versioned layers, so using it as part of a layer name may cause confusion when attempting to determine whether a published artifact or exported environment is using implicit lock versioning or is referring to an external version number.

All layer specifications may also contain the following optional fields:

  • platforms (array of strings): by default, all layers are built for all target platforms. Setting this field allows the layer build to be narrowed to a subset of the supported targets. Setting this field to an empty list also allows a layer build to be disabled without having to delete it entirely. Permitted entries in the platforms list are:

    • "win_amd64": Windows on x86-64

    • "win_arm64": Windows on ARM64/Aarch64 (not currently tested in CI)

    • "linux_x86_64": Linux on x86_64

    • "linux_aarch64": Linux on ARM64/Aarch64 (not currently tested in CI)

    • "macosx_arm64": macOS on Apple (ARM64/Aarch64) silicon

    • "macosx_x86_64": macOS on Intel silicon (not currently tested in CI)

    Changed in version 0.3.0: Added win_arm64 and linux_aarch64 as permitted target platforms (release details).

  • dynlib_exclude (array of strings): by default, dynamic library (also known as shared object) files on Linux and macOS that do not appear to be Python extension modules will be symbolically linked from a share/venv/dynlib/ folder within the virtual environment (see How does dynamic linking work across layers? for additional details). Setting this field allows files to be excluded from the linking process based on filename glob patterns. These patterns are checked against the end of the full path to the files using the equivalent of glob.translate().

    Added in version 0.4.0: Added support for dynamic linking across layers on Linux and macOS (release details).

  • package_indexes (table mapping distribution package names to named uv indexes): by default, all layers are built with common tool configuration settings. To allow different layers to selectively retrieve wheels from different indexes, layers may define a package_indexes subtable that is used to add to or override the uv sources configuration for that layer. For example, one framework layer definition may specify package_indexes = {torch = "pytorch_cu128"}, while an alternate framework layer definition may specify package_indexes = {torch = "pytorch_cpu"}. Upper layers inherit the package index overrides of all of the layers they depend on. A stack definition error is reported if a source override refers to an unknown index name, or if the collected source overrides for a given layer definition are inconsistent. Note: retrieving several packages from an index server that contains only those packages may be tedious to specify when using this setting. In these cases, priority indexes may be a more appropriate setting to use.

  • priority_indexes (array of strings): by default, all layers are built with common tool configuration settings. To allow different layers to retrieve wheels from different indexes, layers may define a priority_indexes list that is used to adjust the uv index configuration for that layer by moving the named indexes to the start of the index list (in the given order) and clearing their explicit flag. Upper layers do NOT automatically inherit the index priorities of the layers they depend on (however, they also do not install any of the packages provided by lower layers). A stack definition error is reported if a priority index list refers to an unknown index name. Note: specifying priority indexes in a layer specification gives those indexes priority for all packages when locking or building that layer. This may be undesirable if the alternate index contains outdated versions of other packages in addition to the packages that should be installed from that index. In these cases, package indexes may be a more appropriate setting to use.

    Added in version 0.8.0: Added support for uv configuration with layer specific adjustments (release details).

    Added in version 0.8.0: Added support for uv configuration with layer specific adjustments (release details).

  • versioned (boolean): by default, and when this setting is false, the layer is considered unversioned (even if an @ symbol appears in the layer name). The layer metadata will always report the lock version for these layers as 1 and the lock version is never implicitly included when deriving other names from the layer name. When this setting is true, the layer is implicitly versioned. For implicitly versioned layers, a lock version number is stored as part of the environment lock metadata, and automatically incremented when the environment lock file changes as the result of a layer locking request. The layer metadata will report the saved lock version for implicitly versioned layers and this value is automatically included when deriving some other names from the layer name.

This means the following layer versioning styles are supported:

  • unversioned: layer name uses a format like my-app with versioned omitted or set to false. Dependencies from other layers (if any) refer to the unversioned layer name. Only the latest version of an unversioned layer can be built and published, and only one version can be installed on any given target system. Artifact tagging allows multiple versions of unversioned layers to still be distributed in parallel. The advantage of unversioned layers is that they allow for low impact security updates, where upper layers only need to be rebuilt if they actually depended on an updated component.

  • implicitly versioned: layer name uses a format like scipy with versioned set to true. Dependencies from other layers refer to the unversioned layer name, and are automatically updated to depend on the new version of the lower layer when the locked requirements change. Some component names derived from the layer name will be implicitly rewritten to use "{layer_name}@{lock_version}" rather than using the layer name on its own. Only the latest version of an implicitly versioned layer can be built and published, but different versions can be installed in parallel on target systems. Implicitly versioned layers lose support for low impact security updates (all upper layers must be rebuilt for any change to the implicitly versioned lower layer), but gain support for parallel installation of multiple versions on target systems.

  • externally versioned: layer name uses a format like cpython-3.12, where the external layer “version” is considered part of the layer name. Dependencies from other layers must refer to the specific version. External versioning allows upper layers to depend on different versions of the “same” lower layer, but also requires those layers to be explicitly migrated to new versions of the lower layer. External versioning always allows multiple versions of the “same” layer to be built and published in parallel. By default, externally versioned layers are handled in the same way as unversioned layers, but external versioning in the layer name may also be freely combined with implicit lock versioning in the derived names by setting versioned to true.

Refer to Layer names and versioning for additional details on how layer names are used when building virtual environment stacks.

Runtime layer specification fields

Runtime layer specifications must contain the following additional field:

  • python_implementation (string): the pbs-installer name of the Python runtime to be installed as the base runtime for this layer (and any upper layers that depend on this layer). Implementation names use the format {implementation_name}@{implementation_version} (for example, cpython@3.12.7).

Framework layer specification fields

Framework layer specifications must contain one of the following additional fields (but not both):

  • runtime (string): the name of the runtime layer that this framework layer uses.

  • frameworks (array of strings): the names of the other framework layers that this framework layer depends on.

When a framework layer declares a dependency on other framework layers, the runtime dependency for this layer is not specified directly. Instead, all of the declared framework dependencies must depend on the same runtime layer, and that base runtime also becomes the base runtime for this framework layer. In order to support this runtime inference step, and to prevent the declaration of circular dependencies between layers, forward references are not supported (in other words, layers must be declared after the layers they depend on).

Whether the runtime is specified directly or indirectly, the install_target and python_implementation attributes of the runtime layer are respectively recorded in the runtime_layer and python_implementation fields of the framework layer’s output metadata.

bound_to_implementation is an additional boolean field in the framework layer output metadata that indicates how tightly coupled the framework layer is to the underlying implementation layer.

On platforms which use symlinks between layered environments and their base environments (any platform other than Windows), bound_to_implementation will be false. This allows for transparent security updates of the base runtime layer (for example, to update to new OpenSSL versions or CPython maintenance releases), without needing to republish the upper layers that use that base runtime.

On Windows, where some elements of the base runtime are copied into each layered environment that depends on it, bound_to_implementation will be true. This still allows for transparent security updates of the base runtime layer in some cases (for example, to update to new OpenSSL versions), but indicates the upper layers will need to be rebuilt and republished for new CPython maintenance releases.

Changed in version 0.4.0: Added the ability for framework layers to depend on other framework layers instead of depending directly on a runtime layer (release details).

Application layer specification fields

Application layer specifications must contain one of the following additional fields (but not both):

  • runtime (string): the name of the runtime layer that this application layer uses.

  • frameworks (array of strings): the names of the framework layers that this application layer depends on.

These two fields are handled in the same way as they are for framework layer specifications.

Python code running in this application layer will be able to import modules from the specified base runtime layer, and from any of the framework layers declared as dependencies (whether directly or indirectly). Refer to Linearizing the Python import path for additional details on how the relative order of the application layer sys.path entries is determined.

Application layer specifications must also contain the following additional field:

  • launch_module (string): a relative path (starting from the folder containing the stack specification file) that specifies a Python module or import package that will be included in the built environment for execution with the -m switch.

Application layer specifications may also contain the following optional field:

  • support_modules (array of strings): an array of relative paths (each starting from the folder containing the stack specification file) that specify Python modules or import packages that will be included in the built environment for use by the application launch module.

Refer to Source tree content filtering for details on exactly which files will be included in the application layer from referenced launch modules and support modules.

Changed in version 0.4.0: Added the ability for application layers to depend directly on a runtime layer instead of declaring that they depend on one or more framework layers (release details).

Changed in version 0.5.0: Updating the name or contents of a launch module also updates the layer version for implicitly versioned layers (release details).

Added in version 0.6.0: Added the support_modules field (release details).

Added in version 0.6.0: Source tree content filtering for launch modules and support modules (release details).

Linearizing the Python import path

The venvstacks.toml file format allows the declared dependencies between framework layers to form a directed acyclic graph (DAG). Python’s import system requires that this graph be flattened into a list in order to be able to define the relative order of application layer sys.path entries in a consistent fashion.

This linearization problem is similar to the one that Python itself needs to solve when determining how to resolve attribute lookups on Python classes in the presence of multiple inheritance, and venvstacks intentionally uses the same solution: the C3 linearization algorithm described in this article about the Python 2.3 Method Resolution Order.

In simple cases where the only common point in the declared layer dependencies is the base runtime, this algorithm gives the same result as a depth-first left-to-right resolution of the declared dependencies.

The benefit of the more complex linearization arises in more complex cases, where the C3 algorithm either ensures that all layers are always listed in a consistent relative import priority order, or else it raises an exception reporting the relative priority conflict.

The Wikipedia article on C3 linearization includes additional details on the C3 algorithm and the assurances it provides.

Added in version 0.4.0: In previous versions, frameworks were not permitted to declare dependencies on other framework layers, so linearization was not required.

Layer names and versioning

Regardless of how a layer is versioned, the layer name is used directly (with no additional prefix or suffix) when referring to the layer as a dependency in another layer specification.

The layer name is also used directly (in combination with the layer type prefix) for the following purposes:

  • the name of the layer build environment

  • the name of the layer requirements file folder

  • as part of the name of the transitively locked layer requirements files

  • as the base name for the layer environment metadata file emitted when publishing or exporting the environment

  • as the layer_name field in the generated layer metadata

Runtime layers do not have a layer type prefix, while framework and application layers use app-* and framework-* respectively.

Layers with implicit lock versioning disabled use their layer name directly (in combination with their layer type prefix) for the following purposes:

  • the name of the deployed layer environment when publishing artifacts or locally exporting environments

  • as the install_target field in the generated layer metadata

  • when referring to the layer as a dependency in another layer’s deployment configuration and output metadata

Layers with implicit lock versioning enabled will instead use "{layer_name}@{lock_version}" for these deployment related purposes.

Source tree content filtering

Application layer launch modules and support modules may be either single files or directories defining a Python import package. In the latter case, the contents of the source tree are filtered to exclude unwanted files rather than including every file in the specified directory.

When git source control information is available, any files explicitly excluded from source control will also be omitted from the application layers (that is, the exclusions are based on .gitignore patterns). Any files or folders with names starting with .git are also excluded.

If no recognised source control information is found, the source tree content filtering defaults to simply excluding __pycache__ folders (as these may be generated if the launch modules or support modules are imported for testing purposes from their source tree location).

Deprecated fields

The following field names were previously supported and now emit FutureWarning when used in a loaded stack specification:

  • build_requirements: no longer has any effect (rendered non-functional before 0.1.0rc1, warning emitted from 0.2.0)

  • fully_versioned_name: renamed to python_implementation in 0.2.0

Locked layer requirements

Environment lock metadata files saved alongside the layer’s transitively locked requirements file:

requirements_hash: str   # Uses "algorithm:hexdigest" format
lock_input_hash: str     # Uses "algorithm:hexdigest" format
other_inputs_hash: str   # Uses "algorithm:hexdigest" format
version_inputs_hash: str # Uses "algorithm:hexdigest" format
lock_version: int        # Auto-incremented from previous lock metadata
locked_at: str           # ISO formatted date/time value

Note: A future documentation update will cover these venvstacks lock output files in additional detail.

Deployed layer configuration

Deployed layer configuration files saved as share/venv/metadata/venvstacks_layer.json in the layer environments:

python: str                      # Relative path to this layer's Python executable
py_version: str                  # Expected X.Y.Z Python version for this environment
base_python: str                 # Relative path from layer dir to base Python executable
site_dir: str                    # Relative path to site-packages within this layer
pylib_dirs: Sequence[str]        # Relative paths to additional sys.path entries
dynlib_dirs: Sequence[str]       # Relative paths to additional Windows DLL directories
launch_module: NotRequired[str]  # Module to run with `-m` to launch the application

Primarily used by the post-installation script to finish setting up the environment after deployment. May also be used by the containing application to find the Python executable location for that platform.

All relative paths are relative to the layer folder (and may refer to peer folders). Base runtime layers will have python and base_python set to the same value. Application layers will have launch_module set.

Note: A future documentation update will cover these venvstacks build output files in additional detail.

Published layer metadata

Layer output metadata files saved to the __venvstacks__ metadata folder when publishing layer archives or locally exporting layer environments:

# Common fields defined for all layers, whether archived or exported
layer_name: EnvNameBuild       # Prefixed layer name without lock version info
install_target: EnvNameDeploy  # Target installation folder when unpacked
requirements_hash: str         # Uses "algorithm:hexdigest" format
lock_version: int              # Monotonically increasing version identifier
locked_at: str                 # ISO formatted date/time value

# Fields that are populated after the layer metadata has initially been defined
# "runtime_layer" is set to the underlying runtime's deployed environment name
# "python_implementation" is set to the underlying runtime's implementation name
# "bound_to_implementation" means that the layered environment includes
# copies of some files from the runtime implementation, and hence will
# need updating even for runtime maintenance releases
runtime_layer: NotRequired[str]
python_implementation: NotRequired[str]
bound_to_implementation: NotRequired[bool]

# Extra fields only defined for framework and application environments
required_layers: NotRequired[Sequence[EnvNameDeploy]]

# Extra fields only defined for application environments
app_launch_module: NotRequired[str]
app_launch_module_hash: NotRequired[str]

Additional metadata fields only included when publishing layer archives:

archive_build: int    # Auto-incremented from previous build metadata
archive_name: str     # Adds archive file extension to layer name
target_platform: str  # Target platform identifier
archive_size: int
archive_hashes: ArchiveHashes # Mapping from hash algorithm names to hashes

Hashes of layered environment dependencies are intentionally NOT incorporated into the published metadata. This allows an “only if needed” approach to rebuilding app and framework layers when the layers they depend on are updated (app layers will usually only depend on some of the components in the underlying environment, and such dependencies are picked up as version changes when regenerating the transitive dependency specifications for each environment).

Note: A future documentation update will cover the venvstacks publish and

venvstacks local-export output metadata files in additional detail, including the effects of the --tag-outputs option when publishing.