Multi-Output Recipes and Staging Caches#
This tutorial teaches you about advanced recipe structures:
- Multi-output recipes - Build multiple packages from one source
- Staging outputs - Create temporary build artifacts
- Output inheritance - Reuse build configurations
- Inspecting Stage1 staging caches
- Variants with multi-output recipes
- Complete build pipeline visualization
Let's get started!
import json
from rattler_build import (
MultiOutputRecipe,
PlatformConfig,
RenderConfig,
Stage0Recipe,
VariantConfig,
)
Example 1: Multi-Output with Inter-Output Dependencies#
Multi-output recipes build multiple packages from one recipe. When one output needs another, you list it as a host (or build) dependency. The dependency package is installed into the build environment, and your build script can use its files.
In this example:
- myproject-lib creates a Python module
- myproject-tools depends on myproject-lib as a host dependency and reads that file during its build
# Multi-output recipe with inter-output dependency
multi_output_yaml = """
schema_version: 1
context:
name: myproject
version: "2.0.0"
recipe:
version: ${{ version }}
outputs:
# First output: The library
- package:
name: ${{ name }}-lib
build:
script:
interpreter: python
content: |
import os
from pathlib import Path
prefix = Path(os.environ["PREFIX"])
lib_dir = prefix / "lib" / "python"
lib_dir.mkdir(parents=True, exist_ok=True)
(lib_dir / "myproject_lib.py").write_text('VERSION = "2.0.0"')
print(f"Created library at {lib_dir}")
requirements:
build:
- python
# Second output: Uses the library as a host dependency
- package:
name: ${{ name }}-tools
build:
script:
interpreter: python
content: |
import os
from pathlib import Path
prefix = Path(os.environ["PREFIX"])
# Read and print the lib file (installed as host dependency)
lib_file = prefix / "lib" / "python" / "myproject_lib.py"
print(f"Reading library from: {lib_file}")
print(lib_file.read_text())
requirements:
build:
- python
host:
- ${{ name }}-lib
"""
multi_recipe = Stage0Recipe.from_yaml(multi_output_yaml)
print("Multi-Output Recipe Loaded")
print("=" * 60)
print(f"Recipe type: {type(multi_recipe).__name__}")
print(f"Is multi-output: {isinstance(multi_recipe, MultiOutputRecipe)}")
print(f"Number of outputs: {len(multi_recipe.outputs)}")
print("\nOutputs:")
for idx, output in enumerate(multi_recipe.outputs, 1):
print(f" {idx}. {output.to_dict()['package']['name']}")
# Render the recipe
mo_variants = VariantConfig()
mo_render = RenderConfig()
mo_results = multi_recipe.render(mo_variants, mo_render)
print(f"\nRendered {len(mo_results)} package(s):")
print("=" * 60)
for rendered in mo_results:
stage1 = rendered.recipe
print(f"\nPackage: {stage1.package.name} {stage1.package.version}")
print(f" Build script: {stage1.build.script}")
print(f" Run requirements: {stage1.requirements.run}")
Multi-Output Recipe Loaded
============================================================
Recipe type: MultiOutputRecipe
Is multi-output: True
Number of outputs: 2
Outputs:
1. ${{ name }}-lib
2. ${{ name }}-tools
Rendered 2 package(s):
============================================================
Package: myproject-lib 2.0.0
Build script: {'interpreter': 'python', 'content': 'import os\nfrom pathlib import Path\n\nprefix = Path(os.environ["PREFIX"])\nlib_dir = prefix / "lib" / "python"\nlib_dir.mkdir(parents=True, exist_ok=True)\n\n(lib_dir / "myproject_lib.py").write_text(\'VERSION = "2.0.0"\')\nprint(f"Created library at {lib_dir}")\n'}
Run requirements: []
Package: myproject-tools 2.0.0
Build script: {'interpreter': 'python', 'content': 'import os\nfrom pathlib import Path\n\nprefix = Path(os.environ["PREFIX"])\n\n# Read and print the lib file (installed as host dependency)\nlib_file = prefix / "lib" / "python" / "myproject_lib.py"\nprint(f"Reading library from: {lib_file}")\nprint(lib_file.read_text())\n'}
Run requirements: []
# Build each variant
print("Building Example 1 packages...")
print("=" * 60)
print(f"Recipe path: {multi_recipe.recipe_path}")
for i, variant in enumerate(mo_results, 1):
print(f"\nBuilding variant {i}/{len(mo_results)}")
stage1_recipe = variant.recipe
package = stage1_recipe.package
build = stage1_recipe.build
print(f" Package: {package.name}")
print(f" Version: {package.version}")
print(f" Build string: {build.string}")
result = variant.run_build()
# Display build result information
print(f" Build complete in {result.build_time:.2f}s!")
print(f" Package: {result.packages[0]}")
if result.variant:
print(f" Variant: {result.variant}")
# Display build log
if result.log:
print(f" Build log: {len(result.log)} messages captured")
print("\n" + "=" * 60)
print("Example 1 builds completed successfully!")
print(f"\nBuilt packages are available in: {result.output_dir}")
Building Example 1 packages...
============================================================
Recipe path: /tmp/rattler_build_9xg9fq1c/recipe.yaml
Building variant 1/2
Package: myproject-lib
Version: 2.0.0
Build string: hb0f4dca_0
Build complete in 1.47s!
Package: /tmp/rattler_build_9xg9fq1c/output/linux-64/myproject-lib-2.0.0-hb0f4dca_0.conda
Variant: {'target_platform': 'linux-64'}
Build log: 54 messages captured
Building variant 2/2
Package: myproject-tools
Version: 2.0.0
Build string: hb0f4dca_0
Build complete in 1.45s!
Package: /tmp/rattler_build_9xg9fq1c/output/linux-64/myproject-tools-2.0.0-hb0f4dca_0.conda
Variant: {'target_platform': 'linux-64'}
Build log: 55 messages captured
============================================================
Example 1 builds completed successfully!
Built packages are available in: /tmp/rattler_build_9xg9fq1c/output
Example 2: Staging Outputs - Shared Build Artifacts#
Staging is different from regular dependencies (Example 1). A staging output runs its build script once, then copies its files directly into each inheriting package's prefix. Since these are "new" files in the prefix, they will be included in the final package.
Use the files field to select which subset of files to include from the staging prefix.
In this example:
- shared-build staging creates both /lib/shared.py AND /bin/tool.py
- compiled-project-python inherits and uses files: [lib/**] to only include lib files
- compiled-project-cli inherits and uses files: [bin/**] to only include bin files
# Recipe with staging output
staging_yaml = """
schema_version: 1
context:
name: compiled-project
version: "1.5.0"
recipe:
version: ${{ version }}
outputs:
# Staging output: Creates shared artifacts for multiple packages
- staging:
name: shared-build
build:
script:
interpreter: python
content: |
import os
from pathlib import Path
prefix = Path(os.environ["PREFIX"])
# Create lib files
lib_dir = prefix / "lib"
lib_dir.mkdir(parents=True, exist_ok=True)
(lib_dir / "shared.py").write_text('SHARED_VERSION = "1.5.0"')
print(f"Created shared library at {lib_dir}")
# Create bin files
bin_dir = prefix / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
(bin_dir / "tool.py").write_text('#!/usr/bin/env python\\nprint("CLI tool")')
print(f"Created CLI tool at {bin_dir}")
requirements:
build:
- python
# Package output 1: Python bindings (inherits lib files from staging)
- package:
name: ${{ name }}-python
inherit: shared-build
build:
files:
- lib/**
# Package output 2: CLI tool (inherits bin files from staging)
- package:
name: ${{ name }}-cli
inherit: shared-build
build:
files:
- bin/**
"""
staging_recipe = Stage0Recipe.from_yaml(staging_yaml)
print("Recipe with Staging Output")
print("=" * 60)
print(f"Total outputs defined: {len(staging_recipe.outputs)}")
print("\nOutput types:")
for idx, output in enumerate(staging_recipe.outputs, 1):
output_dict = output.to_dict()
if "staging" in output_dict:
print(f" {idx}. Staging: {output_dict['staging']['name']}")
elif "package" in output_dict:
pkg_name = output_dict["package"]["name"]
inherits = output_dict.get("inherits_from", None)
print(f" {idx}. Package: {pkg_name}", end="")
if inherits:
print(f" (inherits from: {inherits})")
else:
print()
# Render the recipe
staging_variants = VariantConfig()
platform_config = PlatformConfig(experimental=True) # Staging is still experimental
staging_render = RenderConfig(platform=platform_config)
staging_results = staging_recipe.render(staging_variants, staging_render)
print(f"\nRendered {len(staging_results)} package(s)")
print("(Staging outputs don't produce packages)")
print("=" * 60)
for rendered in staging_results:
stage1 = rendered.recipe
print(f"\n{stage1.package.name} {stage1.package.version}")
# Check for staging caches
if stage1.staging_caches:
print(f" Uses {len(stage1.staging_caches)} staging cache(s):")
for cache in stage1.staging_caches:
print(f" - {cache.name}")
print(f" Build script: {cache.build.script}")
# Check inheritance
if stage1.inherits_from:
print(f" Inherits from: {json.dumps(stage1.inherits_from, indent=6)}")
Recipe with Staging Output
============================================================
Total outputs defined: 3
Output types:
1. Staging: shared-build
2. Package: ${{ name }}-python
3. Package: ${{ name }}-cli
Rendered 2 package(s)
(Staging outputs don't produce packages)
============================================================
compiled-project-python 1.5.0
Uses 1 staging cache(s):
- shared-build
Build script: {'interpreter': 'python', 'content': 'import os\nfrom pathlib import Path\n\nprefix = Path(os.environ["PREFIX"])\n\n# Create lib files\nlib_dir = prefix / "lib"\nlib_dir.mkdir(parents=True, exist_ok=True)\n(lib_dir / "shared.py").write_text(\'SHARED_VERSION = "1.5.0"\')\nprint(f"Created shared library at {lib_dir}")\n\n# Create bin files\nbin_dir = prefix / "bin"\nbin_dir.mkdir(parents=True, exist_ok=True)\n(bin_dir / "tool.py").write_text(\'#!/usr/bin/env python\\nprint("CLI tool")\')\nprint(f"Created CLI tool at {bin_dir}")\n'}
Inherits from: {
"cache_name": "shared-build",
"inherit_run_exports": true
}
compiled-project-cli 1.5.0
Uses 1 staging cache(s):
- shared-build
Build script: {'interpreter': 'python', 'content': 'import os\nfrom pathlib import Path\n\nprefix = Path(os.environ["PREFIX"])\n\n# Create lib files\nlib_dir = prefix / "lib"\nlib_dir.mkdir(parents=True, exist_ok=True)\n(lib_dir / "shared.py").write_text(\'SHARED_VERSION = "1.5.0"\')\nprint(f"Created shared library at {lib_dir}")\n\n# Create bin files\nbin_dir = prefix / "bin"\nbin_dir.mkdir(parents=True, exist_ok=True)\n(bin_dir / "tool.py").write_text(\'#!/usr/bin/env python\\nprint("CLI tool")\')\nprint(f"Created CLI tool at {bin_dir}")\n'}
Inherits from: {
"cache_name": "shared-build",
"inherit_run_exports": true
}
# Build each variant
print("Building Example 2 packages (with staging)...")
print("=" * 60)
print(f"Recipe path: {staging_recipe.recipe_path}")
for i, variant in enumerate(staging_results, 1):
print(f"\nBuilding variant {i}/{len(staging_results)}")
stage1_recipe = variant.recipe
package = stage1_recipe.package
build = stage1_recipe.build
print(f" Package: {package.name}")
print(f" Version: {package.version}")
print(f" Build string: {build.string}")
if stage1_recipe.staging_caches:
print(f" Staging caches: {[c.name for c in stage1_recipe.staging_caches]}")
result = variant.run_build()
# Display build result information
print(f" Build complete in {result.build_time:.2f}s!")
print(f" Package: {result.packages[0]}")
if result.variant:
print(f" Variant: {result.variant}")
# Display build log
if result.log:
print(f" Build log: {len(result.log)} messages captured")
print("\n" + "=" * 60)
print("Example 2 builds completed successfully!")
print(f"\nBuilt packages are available in: {result.output_dir}")
Building Example 2 packages (with staging)...
============================================================
Recipe path: /tmp/rattler_build_4blf4v6a/recipe.yaml
Building variant 1/2
Package: compiled-project-python
Version: 1.5.0
Build string: hb0f4dca_0
Staging caches: ['shared-build']
Build complete in 1.53s!
Package: /tmp/rattler_build_4blf4v6a/output/linux-64/compiled-project-python-1.5.0-hb0f4dca_0.conda
Variant: {'target_platform': 'linux-64'}
Build log: 71 messages captured
Building variant 2/2
Package: compiled-project-cli
Version: 1.5.0
Build string: hb0f4dca_0
Staging caches: ['shared-build']
Build complete in 1.60s!
Package: /tmp/rattler_build_4blf4v6a/output/linux-64/compiled-project-cli-1.5.0-hb0f4dca_0.conda
Variant: {'target_platform': 'linux-64'}
Build log: 66 messages captured
============================================================
Example 2 builds completed successfully!
Built packages are available in: /tmp/rattler_build_4blf4v6a/output
Summary#
In this tutorial, you learned about multi-output recipes and staging:
- Multi-Output Recipes: Build multiple packages from one recipe using the
outputslist - Staging Outputs: Create temporary build artifacts with
staging:that other packages can inherit