Files
phyllomeos/cook/manifest.py
T
Lukas Greve a4bc2967e5 Change: Update recipe generation to use single universal template
- Transition from multi-template (install/live recipe types) to single universal template (proteus)
- Remove recipe_type parameter from RecipeGenerator
- Update manifest format (remove recipe_type field)
- Simplify CLI (remove --type argument, use --output for single generation)
- Filename generation now uses primary modifier as first component
2026-03-27 15:39:30 +01:00

209 lines
7.8 KiB
Python

"""Manifest loading and variant expansion.
This module provides functionality for processing manifest YAML files, which
are used in batch recipe generation mode. It has two main responsibilities:
1. Loading and Parsing Manifests
- Reads YAML files containing recipe configurations
- Validates basic structure of the manifest
2. Variant Expansion
- Converts compact manifest entries with list values into individual variants
- Uses cartesian product to generate all combinations
- Enables generating many recipes from a single manifest entry
Manifest Example:
recipes:
- name: desktop
variants:
- version: 43
desktop: gnome
storage: encrypted
- version: ["43", "rawhide"]
storage: ["standard", "encrypted"]
The above would generate 3 recipes (1 desktop + 2 rawhide variants).
With the universal template (proteus), recipes are generated using a single
template that supports all system types. The 'name' field is purely for
organization and doesn't affect the generated filenames.
The key insight is that list values in variants represent "multiple values for
this field", and we want to generate one recipe for each combination. This is
the classic cartesian product problem.
"""
from __future__ import annotations
from itertools import product
from pathlib import Path
from typing import Dict, List
import yaml
class ManifestProcessor:
"""Load and process recipe manifests.
This class handles the loading and processing of manifest YAML files.
A manifest is a YAML file that defines which recipes to generate and
what variations (variants) of each recipe to create.
Key features:
- Loads and parses YAML manifest files
- Validates manifest structure (schema-level checks)
- Expands variants with list values into all combinations
The variant expansion is particularly useful for generating multiple
versions (e.g., both Fedora 43 and rawhide) or multiple configurations
(e.g., both standard and encrypted storage) in a compact format.
"""
def __init__(self, project_root: Path):
"""Initialize the processor with project root path.
Args:
project_root: Path to the phyllomeos project root directory.
Not currently used but available for future enhancements.
"""
self.project_root = project_root
def load(self, path: Path) -> Dict:
"""Load and parse manifest YAML file.
This method reads a YAML file from disk and parses it into a Python
dictionary. It's a simple wrapper around yaml.safe_load that provides
error handling and a consistent interface.
Args:
path: Path to the manifest YAML file
Returns:
Parsed manifest as a dictionary
Raises:
FileNotFoundError: If the file doesn't exist
yaml.YAMLError: If the file contains invalid YAML
"""
with open(path, encoding='utf-8') as f:
return yaml.safe_load(f)
def validate(self, manifest: Dict) -> List[str]:
"""Validate manifest structure.
This method performs schema-level validation of a manifest dictionary.
It checks that:
1. The top-level 'recipes' key exists
2. Each recipe configuration has a 'name' field (for organization only)
3. Each recipe configuration has a 'variants' field
4. Each variant has a 'version' field
5. No 'recipe_type' field exists (old format, removed with universal template)
This is a basic structural check that catches obvious errors before
trying to process the manifest. It doesn't validate the content of
individual recipes (that's done by RecipeGenerator and validators).
Args:
manifest: The manifest dictionary to validate
Returns:
List of validation error strings (empty if valid)
"""
errors = []
# Check for required top-level key
if 'recipes' not in manifest:
errors.append("Manifest missing 'recipes' key")
return errors # Can't proceed without recipes
# Validate each recipe configuration
for recipe_config in manifest.get('recipes', []):
if 'name' not in recipe_config:
errors.append("Recipe config missing 'name' key")
if 'variants' not in recipe_config:
errors.append(f"Recipe '{recipe_config.get('name', 'unnamed')}' "
"missing 'variants' key")
# Check for old recipe_type field (which is no longer used)
if 'recipe_type' in recipe_config:
errors.append(f"Recipe '{recipe_config.get('name', 'unnamed')}' "
"has 'recipe_type' field which is no longer used with universal template")
return errors
def expand_variants(self, variants: List[Dict]) -> List[Dict]:
"""Expand variants with list values into individual variants.
This is the core functionality of the manifest processor. It converts
a compact representation with list values into a complete list of
individual variants by generating the cartesian product.
Example Input:
[
{
'version': ['43', 'rawhide'],
'storage': ['standard', 'encrypted'],
'desktop': 'gnome'
},
{
'version': '43',
'security': 'devel'
}
]
Example Output:
[
{'version': '43', 'storage': 'standard', 'desktop': 'gnome'},
{'version': '43', 'storage': 'encrypted', 'desktop': 'gnome'},
{'version': 'rawhide', 'storage': 'standard', 'desktop': 'gnome'},
{'version': 'rawhide', 'storage': 'encrypted', 'desktop': 'gnome'},
{'version': '43', 'security': 'devel'}
]
How it works:
1. For each variant dictionary, separate list-valued keys from scalar keys
2. If there are no lists, keep the variant as-is
3. If there are lists, generate the cartesian product of all list values
4. For each combination, create a new variant with scalar values preserved
5. Return the complete list of expanded variants
This enables generating many recipes from a single compact manifest entry,
making it easy to generate variations across multiple dimensions.
Args:
variants: List of variant dictionaries, where some values may be lists
Returns:
Expanded list where each variant has only scalar (non-list) values
"""
expanded = []
for variant in variants:
# Separate list-valued keys from scalar keys
list_keys = {}
scalar_keys = {}
for key, value in variant.items():
if isinstance(value, list):
list_keys[key] = value
else:
scalar_keys[key] = value
if not list_keys:
# No lists to expand, keep variant as-is
expanded.append(variant)
continue
# Get the cartesian product of all list values
keys = list(list_keys.keys())
values_product = product(*[list_keys[k] for k in keys])
# Create a new variant for each combination
for combo in values_product:
new_variant = scalar_keys.copy()
for i, key in enumerate(keys):
new_variant[key] = combo[i]
expanded.append(new_variant)
return expanded