refactor: Modularize recipe generator

- Split generate_recipe.py into cli.py, recipe_generator.py, validators.py, manifest.py
- Removed override logic - modifiers now add fragments rather than replace
- Added filename deduplication to prevent overwriting recipes
- Updated CI workflows and tests to use new module structure
- Made pykickstart a hard dependency
- Removed ingredients/ directory support (only fragments/*.ks now used)
- New layout: 5 modules (~990 lines) vs single 769-line file
This commit is contained in:
Lukas Greve
2026-03-26 15:31:18 +01:00
parent 8f824cb977
commit 898c831065
35 changed files with 1004 additions and 1251 deletions
+2 -2
View File
@@ -23,14 +23,14 @@ jobs:
- name: Generate all recipes
run: |
cd scripts
cd recipe-generator
python3 generate_recipe.py \
--manifest recipes_manifest.yaml \
--output-dir ../recipes/
- name: Validate recipes (strict mode)
run: |
cd scripts
cd recipe-generator
python3 generate_recipe.py --validate ../recipes/*.cfg --strict
build-iso:
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
- name: Build test container
run: |
cd scripts
cd recipe-generator
podman build -t phyllo/test-runner ../tests/container/
- name: Run tests in container
+4 -4
View File
@@ -23,22 +23,22 @@ jobs:
- name: Generate all recipes
run: |
cd scripts
cd recipe-generator
make generate-recipes
- name: Validate recipes (strict mode)
run: |
cd scripts
cd recipe-generator
python3 generate_recipe.py --validate ../recipes/*.cfg --strict
- name: Run unit tests
run: |
cd scripts
cd recipe-generator
make test
- name: Run integration tests
run: |
cd scripts
cd recipe-generator
make test-integration
- name: Upload test results
+2 -2
View File
@@ -23,12 +23,12 @@ install-deps:
generate-recipes:
rm -f ../recipes/*.cfg
python generate_recipe.py \
python3 generate_recipe.py \
--manifest recipes_manifest.yaml \
--output-dir ../recipes/
validate-recipes:
python generate_recipe.py \
python3 generate_recipe.py \
--validate ../recipes/*.cfg
test:
+323
View File
@@ -0,0 +1,323 @@
"""CLI entry point for recipe generator."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import List
import yaml
from manifest import ManifestProcessor
from recipe_generator import RecipeGenerator
from validators import (
ContentValidator,
SemanticValidator,
TemplateValidator,
validate_manifest,
)
def main() -> None:
"""Main entry point for recipe generator CLI."""
parser = argparse.ArgumentParser(
description='Generate Phyllome OS kickstart recipes from templates',
formatter_class=argparse.RawDescriptionHelpFormatter
)
# Global options
SCRIPTS_DIR = Path(__file__).resolve().parent # noqa: N806 - Constant
PROJECT_ROOT = SCRIPTS_DIR.parent # noqa: N806 - Constant
parser.add_argument('--ingredients', '-i',
type=Path, default=PROJECT_ROOT / 'ingredients',
help='Ingredients directory (default: parent/ingredients)')
parser.add_argument('--templates', '-t',
type=Path, default=SCRIPTS_DIR / 'recipe_templates.yaml',
help='Templates YAML file (default: ./recipe_templates.yaml)')
# Batch mode
parser.add_argument('--manifest', '-m',
type=Path, help='Manifest YAML for batch generation')
parser.add_argument('--output-dir', '-d',
type=Path, default=SCRIPTS_DIR / 'recipes',
help='Output directory (batch generation, default: ./recipes)')
parser.add_argument('--dry-run', '-n',
action='store_true',
help='Show what would be generated without writing files')
# Single generation mode
parser.add_argument('--type', '-T',
help='Recipe type (e.g., virtual-desktop)')
parser.add_argument('--output', '-o',
type=Path, help='Output file (single generation)')
# Recipe parameters
parser.add_argument('--version', '-v',
choices=['43', 'rawhide'], default='43',
help='Fedora version (default: 43)')
parser.add_argument('--desktop',
choices=['gnome', 'labwc'],
default='gnome',
help='Desktop environment (default: gnome)')
parser.add_argument('--storage',
choices=['standard', 'encrypted'],
help='Storage type (default: standard)')
parser.add_argument('--security',
choices=['secure', 'devel'],
help='Security mode (default: secure)')
parser.add_argument('--cpu',
choices=['generic', 'amdcpu', 'intelcpu'],
help='CPU optimization')
parser.add_argument('--gpu',
choices=['none', 'intelgpu'],
default='none',
help='GPU passthrough (default: none)')
# Validation mode
parser.add_argument('--validate', '-V',
nargs='+',
help='Validate recipe files')
# Strict mode for CI
parser.add_argument('--strict',
action='store_true',
help='Treat warnings as errors (CI mode)')
args = parser.parse_args()
# Initialize generator
generator = RecipeGenerator(args.templates)
# Initialize validators
template_validator = TemplateValidator(generator.project_root)
content_validator = ContentValidator(generator.project_root)
semantic_validator = SemanticValidator()
# Validation mode
if args.validate:
all_issues = validate_recipes(args.validate, content_validator, semantic_validator, args.strict)
handle_validation_results(all_issues, args.strict)
return
# Batch generation mode
if args.manifest:
generate_from_manifest(args, generator)
return
# Single generation mode
if args.type:
generate_single(args, generator)
return
# No mode specified, show help
parser.print_help()
sys.exit(1)
def validate_recipes(recipe_paths: List[str], content_validator: ContentValidator,
semantic_validator: SemanticValidator, strict: bool) -> List:
"""Validate multiple recipes."""
all_issues = []
for recipe_path in recipe_paths:
try:
# Validate content
content = Path(recipe_path).read_text(encoding='utf-8')
issues = content_validator.validate(content)
# Extract version and validate semantically
filename = Path(recipe_path).stem
version = extract_version(content, filename)
if version:
semantic_issues = semantic_validator.validate(content, version)
issues.extend(semantic_issues)
else:
issues.append("Warning: Could not determine version, "
"skipping semantic validation")
if issues:
all_issues.append((recipe_path, issues))
except FileNotFoundError:
print(f"Error: Recipe not found: {recipe_path}", file=sys.stderr)
sys.exit(2)
return all_issues
def extract_version(content: str, filename: str) -> str | None:
"""Extract Fedora version from recipe content or filename."""
import re
filename_match = re.search(r'(?:_|-)(43|rawhide)(?:_|-|.cfg|.yaml|$)', filename)
if filename_match:
return filename_match.group(1)
for line in content.split('\n'):
if 'core-fedora-repo-43' in line:
return '43'
elif 'core-fedora-repo-rawhide' in line:
return 'rawhide'
if 'generic-43/repo' in line:
return '43'
elif 'generic-rawhide/repo' in line:
return 'rawhide'
return None
def handle_validation_results(all_issues: List, strict: bool) -> None:
"""Handle validation results and exit with appropriate code."""
if all_issues:
for path, issues in all_issues:
print(f"\n{path}:", file=sys.stderr)
error_count = sum(1 for i in issues if 'ERROR' in i or 'error' in i.lower())
warning_count = sum(1 for i in issues if 'Warning' in i or 'warning' in i.lower())
if error_count > 0:
for issue in issues:
if 'ERROR' in issue or 'error' in issue.lower():
print(f" {issue}", file=sys.stderr)
if warning_count > 0:
for issue in issues:
if 'Warning' in issue or 'warning' in issue.lower():
print(f" {issue}", file=sys.stderr)
if error_count == 0 and warning_count == 0:
print(f" No issues found (file exists)", file=sys.stderr)
print(f"\nSummary:", file=sys.stderr)
print(f" - {len(all_issues)} recipe(s) checked", file=sys.stderr)
total_errors = sum(len([i for i in issues if 'ERROR' in i or 'error' in i.lower()])
for _, issues in all_issues)
total_warnings = sum(len([i for i in issues if 'Warning' in i or 'warning' in i.lower()])
for _, issues in all_issues)
print(f" - {total_errors} error(s), {total_warnings} warning(s)", file=sys.stderr)
if strict and total_warnings > 0:
print("\nStrict mode: Warnings treated as errors", file=sys.stderr)
sys.exit(1)
if total_errors > 0:
sys.exit(1)
if all_issues:
sys.exit(1)
else:
print("All recipes validated successfully")
sys.exit(0)
def generate_from_manifest(args: argparse.Namespace, generator: RecipeGenerator) -> None:
"""Generate recipes from manifest."""
manifest_path = args.manifest
try:
with open(manifest_path, encoding='utf-8') as f:
manifest = yaml.safe_load(f)
except FileNotFoundError:
print(f"Error: Manifest file not found: {manifest_path}", file=sys.stderr)
sys.exit(2)
except yaml.YAMLError as e:
print(f"Error: Invalid YAML in manifest: {e}", file=sys.stderr)
sys.exit(2)
# Validate manifest
manifest_processor = ManifestProcessor(generator.project_root)
errors = manifest_processor.validate(manifest)
if errors:
print(f"Error: Invalid manifest:", file=sys.stderr)
for error in errors:
print(f" - {error}", file=sys.stderr)
sys.exit(1)
# Track seen filenames to avoid overwriting duplicates
seen_filenames = set()
# Generate all recipes
for recipe_config in manifest.get('recipes', []):
recipe_type = recipe_config['name']
if recipe_type not in generator.templates:
print(f"Error: Unknown recipe type in manifest: {recipe_type}", file=sys.stderr)
sys.exit(1)
variants = recipe_config.get('variants', [])
variants = generator.expand_variants(variants)
for variant in variants:
version = variant['version']
modifiers = {k: v for k, v in variant.items() if k not in ['name', 'version']}
variant_subname = variant.get('name', '')
if variant_subname:
modifiers['variant_type'] = variant_subname
modifiers['variant_subname'] = variant_subname
content = generator.generate(recipe_type, version, **modifiers)
if args.validate and not args.dry_run:
content_validator = ContentValidator(generator.project_root)
issues = content_validator.validate(content)
semantic_validator = SemanticValidator()
semantic_issues = semantic_validator.validate(content, version)
all_issues = issues + semantic_issues
if all_issues:
print(f"Validation issues for {recipe_type} {version}:", file=sys.stderr)
for issue in issues:
print(f" - {issue}", file=sys.stderr)
sys.exit(1)
filename = generator.generate_filename(recipe_type, version, **modifiers)
output_path = args.output_dir / filename
if filename in seen_filenames:
continue # Already generated this filename
seen_filenames.add(filename)
if args.dry_run:
print(f"Would generate: {output_path}")
else:
print(f"Generating: {output_path}")
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
def generate_single(args: argparse.Namespace, generator: RecipeGenerator) -> None:
"""Generate a single recipe."""
modifiers = {
'variant_type': 'desktop',
'desktop': args.desktop if args.desktop else None,
'storage': args.storage if args.storage != 'standard' else None,
'security': args.security if args.security != 'secure' else None,
'cpu': args.cpu if args.cpu and args.cpu != 'generic' else None,
'gpu': args.gpu if args.gpu and args.gpu != 'none' else None,
}
modifiers = {k: v for k, v in modifiers.items() if v is not None}
content = generator.generate(args.type, args.version, **modifiers)
if args.validate:
content_validator = ContentValidator(generator.project_root)
issues = content_validator.validate(content)
semantic_validator = SemanticValidator()
semantic_issues = semantic_validator.validate(content, args.version)
all_issues = issues + semantic_issues
if all_issues:
print("Validation issues:", file=sys.stderr)
for issue in issues:
print(f" - {issue}", file=sys.stderr)
sys.exit(1)
else:
print("Validation passed")
if args.output:
if args.dry_run:
print(f"Would write to: {args.output}")
else:
with open(args.output, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Generated: {args.output}")
else:
print(content)
+2 -764
View File
@@ -1,769 +1,7 @@
#!/usr/bin/env python3
"""
Recipe Generator for Phyllome OS Kickstart Files
Generates .cfg recipe files from templates and YAML manifest.
"""
import argparse
import re
import sys
from itertools import product
from pathlib import Path
from typing import Dict, List, Optional
import yaml
def _import_pykickstart(): # noqa: PLC0415 - Import for optional dependency
"""Import pykickstart modules, returns None if not available."""
try:
from pykickstart.parser import KickstartParser # noqa: PLC0415
from pykickstart.version import makeVersion # noqa: PLC0415
from pykickstart.version import DEVEL # noqa: PLC0415
from pykickstart.errors import KickstartParseError, KickstartError # noqa: PLC0415
return {
'parser': KickstartParser,
'makeVersion': makeVersion,
'DEVEL': DEVEL,
'KickstartParseError': KickstartParseError,
'KickstartError': KickstartError
}
except ImportError:
return None
class RecipeGenerator:
"""Generate kickstart recipes from templates and modifiers."""
def __init__(self, ingredients_dir: Path, templates_file: Path):
self.project_root = Path(__file__).parent.parent
self.ingredients_dir = self.project_root / ingredients_dir
self.templates = self.load_templates(templates_file)
def get_ksversion(self, version: str) -> Optional[str]:
"""Map Phyllome OS version to pykickstart version string."""
if version == 'rawhide':
return None
else:
return f'F{int(version) - 1}'
def load_templates(self, path: Path) -> Dict:
"""Load recipe templates from YAML file."""
try:
# Template path could be:
# - Absolute path (already resolved)
# - Relative path (resolve relative to project root)
if path.is_absolute():
template_path = path
else:
template_path = self.project_root / path
with open(template_path, encoding='utf-8') as f:
data = yaml.safe_load(f)
return data['templates']
except FileNotFoundError:
print(f"Error: Templates file not found: {template_path}", file=sys.stderr)
sys.exit(2)
except yaml.YAMLError as e:
print(f"Error: Invalid YAML in {template_path}: {e}", file=sys.stderr)
sys.exit(2)
def validate_template(self, template: Dict) -> List[str]:
"""Validate template structure and fragment existence."""
errors = []
# Check required keys
required_keys = ['description', 'base', 'required']
for key in required_keys:
if key not in template:
errors.append(f"Missing required key: {key}")
# Validate base ingredient exists (for compatibility with old ingredients)
if 'base' in template:
base_path = self.ingredients_dir / f"{template['base']}.cfg"
if not base_path.exists():
errors.append(f"Base ingredient not found: {template['base']}.cfg")
# Validate required fragments exist
for item in template.get('required', []):
if isinstance(item, dict):
fragment_path = list(item.values())[0]
else:
continue
# Handle both absolute fragment paths and old ingredient names
if fragment_path.startswith('fragments/'):
full_path = self.project_root / fragment_path
else:
full_path = self.ingredients_dir / f"{fragment_path}.cfg"
if not full_path.exists():
errors.append(f"Required fragment not found: {fragment_path}")
# Validate optional fragment values
for opt_key, opt_config in template.get('optional', {}).items():
if isinstance(opt_config, dict):
for value, fragment_path in opt_config.items():
# Skip None values
if fragment_path is None:
continue
# Handle lists
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is None:
continue
if fp.startswith('fragments/'):
full_path = self.project_root / fp
else:
full_path = self.ingredients_dir / f"{fp}.cfg"
if not full_path.exists():
errors.append(f"Optional fragment not found: {fp} "
f"(in list for {opt_key}={value})")
else:
if fragment_path.startswith('fragments/'):
full_path = self.project_root / fragment_path
else:
full_path = self.ingredients_dir / f"{fragment_path}.cfg"
if not full_path.exists():
errors.append(f"Optional fragment not found: {fragment_path} "
f"(for {opt_key}={value})")
elif isinstance(opt_config, list):
for fragment_path in opt_config:
if fragment_path is None:
continue
if fragment_path.startswith('fragments/'):
full_path = self.project_root / fragment_path
else:
full_path = self.ingredients_dir / f"{fragment_path}.cfg"
if not full_path.exists():
errors.append(f"Optional fragment not found: {fragment_path} (in list)")
return errors
def validate_manifest(self, manifest: Dict) -> List[str]:
"""Validate manifest structure."""
errors = []
if 'recipes' not in manifest:
errors.append("Manifest missing 'recipes' key")
return errors
for recipe_config in manifest['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")
continue
for variant in recipe_config['variants']:
if 'version' not in variant:
errors.append(f"Recipe '{recipe_config['name']}' variant missing 'version'")
return errors
def generate_recipe(self, recipe_type: str, version: str, **modifiers) -> str:
"""Generate a recipe from template with modifiers."""
if recipe_type not in self.templates:
print(f"Error: Unknown recipe type: {recipe_type}", file=sys.stderr)
sys.exit(1)
template = self.templates[recipe_type]
# Validate template
errors = self.validate_template(template)
if errors:
print(f"Error: Invalid template '{recipe_type}':", file=sys.stderr)
for error in errors:
print(f" - {error}", file=sys.stderr)
sys.exit(1)
lines = self.build_header(template['description'], recipe_type, version, modifiers)
lines.extend(self.build_includes(template, version, modifiers))
return '\n'.join(lines)
def build_header(self, description: str, _recipe_type: str,
_version: str, _modifiers: Dict) -> List[str]:
"""Build the ASCII art header and description."""
header = [
"# __ ____ ____ _____",
"# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \\/ ___/",
"# / __ \\/ __ \\/ / / / / / __ \\/ __ `__ \\/ _ \\ / / / /\\__ \\",
"# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /",
"# / .___/_/ /_/\\__, /_/_/\\____/_/ /_/ /_/\\___/ \\____//____/",
"# /_/ /____/",
"",
f"# {description}",
"",
]
return header
def build_includes(self, template: Dict, version: str, modifiers: Dict) -> List[str]:
"""Build %include lines from template and modifiers."""
includes = []
seen = set() # Track to prevent duplicates
# Add version to modifiers for template processing
modifiers = modifiers.copy()
modifiers['version'] = version
# Keys that can be overridden by modifiers
override_keys = {'security', 'bootloader', 'version', 'initial-setup', 'storage'}
# Add required includes
for item in template.get('required', []):
if isinstance(item, dict):
key = list(item.keys())[0]
fragment_path = list(item.values())[0]
# Skip required fragment if modifier overrides it
if key in override_keys and key in modifiers:
continue
if fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Add optional includes based on modifiers
for opt_key, opt_config in template.get('optional', {}).items():
if opt_key in modifiers:
value = modifiers[opt_key]
# Case 1: opt_config is nested dict (e.g., virtualization, variant_type)
# and value is string/int (select from options)
if isinstance(opt_config, dict) and not isinstance(value, (dict, list)):
if value in opt_config:
fragment_path = opt_config[value]
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif fragment_path and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Case 2: opt_config is nested dict (e.g., virtualization)
# and value is dict (nested structure)
elif isinstance(opt_config, dict) and isinstance(value, dict):
for nested_key in value:
if nested_key in opt_config:
nested_value = opt_config[nested_key]
if isinstance(nested_value, list):
for fp in nested_value:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif nested_value is not None and nested_value not in seen:
includes.append(f"%include {nested_value}")
seen.add(nested_value)
# Case 3: opt_config is nested dict, value is boolean (additive)
elif isinstance(opt_config, dict) and isinstance(value, bool) and value:
for nested_key, nested_value in opt_config.items():
if isinstance(nested_value, list):
for fp in nested_value:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif nested_value is not None and nested_value not in seen:
includes.append(f"%include {nested_value}")
seen.add(nested_value)
# Case 4: opt_config is list, value is boolean (boolean flag)
elif isinstance(opt_config, list) and value is True:
for fragment_path in opt_config:
if fragment_path is not None and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Handle modifiers section
for mod_key, mod_value in modifiers.items():
# Normalize key: convert underscores to hyphens for template lookup
mod_key_normalized = mod_key.replace('_', '-')
if mod_key_normalized in template.get('modifiers', {}):
mod_key_to_use = mod_key_normalized
elif mod_key in template.get('modifiers', {}):
mod_key_to_use = mod_key
else:
continue
mod_config = template['modifiers'][mod_key_to_use]
# Handle nested dict modifiers
if isinstance(mod_config, dict) and isinstance(mod_value, str):
if mod_value in mod_config:
fragment_path = mod_config[mod_value]
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif fragment_path and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Handle list modifiers
elif isinstance(mod_config, dict) and isinstance(mod_value, list):
for item in mod_value:
if item in mod_config:
fragment_path = mod_config[item]
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif fragment_path and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
return includes
def validate_recipe(self, content: str) -> List[str]:
"""Validate recipe content, return list of warnings/errors."""
issues = []
includes = [line for line in content.split('\n') if line.startswith('%include')]
# Check for duplicate includes
seen = set()
for inc in includes:
parts = inc.split()
if len(parts) < 2:
continue
path = parts[1]
if path in seen:
issues.append(f"Duplicate include: {path}")
seen.add(path)
# Check fragment existence (relative to project root)
for inc in includes:
parts = inc.split()
if len(parts) < 2:
continue
path = parts[1]
fragment_path = self.project_root / path
if not fragment_path.exists():
issues.append(f"Missing fragment: {path}")
return issues
def validate_recipe_semantic_from_file(self, recipe_path: str, version: str) -> List[str]:
"""Validate recipe from file path using pykickstart."""
try:
return self._validate_recipe_semantic_internal(recipe_path, version)
except FileNotFoundError:
return [f"Error: Recipe file not found: {recipe_path}"]
def validate_recipe_content(self, recipe_path: str) -> List[str]:
"""Validate recipe content from file path for include resolution."""
content = Path(recipe_path).read_text(encoding='utf-8')
return self.validate_recipe(content)
def validate_recipe_semantic(self, content: str, version: str) -> List[str]:
"""Validate recipe using pykickstart parser with version-specific checks."""
return self._validate_recipe_semantic_internal(content, version)
def _validate_recipe_semantic_internal(self, source, version: str) -> List[str]:
"""Internal validation method that handles both file paths and content."""
issues = []
modules = _import_pykickstart()
if modules is None:
issues.append("Warning: pykickstart not installed, skipping semantic validation")
return issues
KickstartParser = modules['parser'] # noqa: N806 - External library class name
makeVersion = modules['makeVersion'] # noqa: N806 - External library function
KickstartParseError = modules['KickstartParseError'] # noqa: N806
KickstartError = modules['KickstartError'] # noqa: N806
ks_version_str = self.get_ksversion(version)
if ks_version_str:
ks_version = makeVersion(ks_version_str)
else:
ks_version = makeVersion(modules['DEVEL'])
try:
parser = KickstartParser(ks_version)
# Determine if source is a file path or content
if isinstance(source, Path) or (isinstance(source, str) and (source.endswith('.ks') or source.endswith('.cfg')) or (Path(source).exists() and Path(source).is_file())):
# Source is a file path
content = Path(source).read_text(encoding='utf-8')
else:
# Source is content string
content = source
parser.readKickstartFromString(content)
except KickstartParseError as e:
issues.append(f"Syntax error line {e.lineno}: {e.message}")
except KickstartError as e:
# Only add if it's not an include-related error
err_str = str(e)
if 'Unable to open input kickstart file' not in err_str:
issues.append(f"Validation error: {err_str}")
except Exception as e: # noqa: BLE001 - Catch all for unexpected parser errors
issues.append(f"Unexpected error during parsing: {str(e)}")
return issues
def extract_version(self, content: str, filename: str) -> Optional[str]:
"""Extract Fedora version from recipe content or filename."""
return self._extract_version_internal(content, filename)
def extract_version_from_file(self, recipe_path: str, filename: str) -> Optional[str]:
"""Extract version from recipe file."""
content = Path(recipe_path).read_text(encoding='utf-8')
return self._extract_version_internal(content, filename)
def _extract_version_internal(self, content: str, filename: str) -> Optional[str]:
"""Extract Fedora version from recipe content or filename."""
filename_match = re.search(r'(?:_|-)(43|rawhide)(?:_|-|.cfg|.yaml|$)', filename)
if filename_match:
return filename_match.group(1)
for line in content.split('\n'):
# Check for old %include references
if 'core-fedora-repo-43' in line:
return '43'
elif 'core-fedora-repo-rawhide' in line:
return 'rawhide'
# Check for new %include references
if 'generic-43/repo' in line:
return '43'
elif 'generic-rawhide/repo' in line:
return 'rawhide'
return None
def generate_filename(self, recipe_type: str, version: str, **modifiers) -> str:
"""Generate recipe filename from parameters."""
# Extract variant subname if present
variant_subname = modifiers.get('variant_subname', '')
if not variant_subname:
variant_subname = modifiers.get('variant_type', '')
# Build base parts
parts = [recipe_type.replace('_', '-')]
# Add guest-agents suffix (virtual when enabled)
if modifiers.get('guest-agents') is True:
parts.append('virtual')
# Add variant_subname for install variants
if variant_subname and variant_subname in ['desktop', 'server', 'hypervisor', 'hypervisor-desktop']:
parts.append(variant_subname)
# Add hypervisor-type suffix (list or single value)
if modifiers.get('hypervisor_type'):
ht = modifiers['hypervisor_type']
if isinstance(ht, list):
for h in ht:
if h:
parts.append(h)
elif ht:
parts.append(ht)
# Add desktop (non-GNOME only, since GNOME is default)
if modifiers.get('desktop') and modifiers['desktop'] != 'gnome':
parts.append(modifiers['desktop'])
# Add security suffix (devel only, since secure is default)
if modifiers.get('security') == 'off':
parts.append('devel')
# Add storage suffix (encrypted only, since standard is default)
if modifiers.get('storage') == 'encrypted':
parts.append('encrypted')
# Add hardware-support suffix (hardware when enabled)
if modifiers.get('hardware-support') is True:
parts.append('hardware-support')
# Add version
parts.append(str(version))
return '_'.join(parts) + '.cfg'
def expand_variants(self, variants: List[Dict]) -> List[Dict]:
"""Expand variants with list values into individual variants."""
expanded = []
for variant in variants:
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:
expanded.append(variant)
continue
keys = list(list_keys.keys())
values_product = product(*[list_keys[k] for k in keys])
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
def main():
"""Main entry point for recipe generator CLI."""
parser = argparse.ArgumentParser(
description='Generate Phyllome OS kickstart recipes from templates',
formatter_class=argparse.RawDescriptionHelpFormatter
)
# Global options
SCRIPTS_DIR = Path(__file__).resolve().parent # noqa: N806 - Constant
PROJECT_ROOT = SCRIPTS_DIR.parent # noqa: N806 - Constant
parser.add_argument('--ingredients', '-i',
type=Path, default=PROJECT_ROOT / 'ingredients',
help='Ingredients directory (default: parent/ingredients)')
parser.add_argument('--templates', '-t',
type=Path, default=SCRIPTS_DIR / 'recipe_templates.yaml',
help='Templates YAML file (default: ./recipe_templates.yaml)')
# Batch mode
parser.add_argument('--manifest', '-m',
type=Path, help='Manifest YAML for batch generation')
parser.add_argument('--output-dir', '-d',
type=Path, default=SCRIPTS_DIR / 'recipes',
help='Output directory (batch generation, default: ./recipes)')
parser.add_argument('--dry-run', '-n',
action='store_true',
help='Show what would be generated without writing files')
# Single generation mode
parser.add_argument('--type', '-T',
help='Recipe type (e.g., virtual-desktop)')
parser.add_argument('--output', '-o',
type=Path, help='Output file (single generation)')
# Recipe parameters
parser.add_argument('--version', '-v',
choices=['43', 'rawhide'], default='43',
help='Fedora version (default: 43)')
parser.add_argument('--desktop',
choices=['gnome', 'labwc'],
default='gnome',
help='Desktop environment (default: gnome)')
parser.add_argument('--storage',
choices=['standard', 'encrypted'],
help='Storage type (default: standard)')
parser.add_argument('--security',
choices=['secure', 'devel'],
help='Security mode (default: secure)')
parser.add_argument('--cpu',
choices=['generic', 'amdcpu', 'intelcpu'],
help='CPU optimization')
parser.add_argument('--gpu',
choices=['none', 'intelgpu'],
default='none',
help='GPU passthrough (default: none)')
# Validation mode
parser.add_argument('--validate', '-V',
nargs='+',
help='Validate recipe files')
# Strict mode for CI
parser.add_argument('--strict',
action='store_true',
help='Treat warnings as errors (CI mode)')
args = parser.parse_args()
# Initialize generator
generator = RecipeGenerator(args.ingredients, args.templates)
# Validation mode
if args.validate:
all_issues = []
for recipe_path in args.validate:
try:
# Validate with file path so includes can be resolved correctly
issues = generator.validate_recipe_content(recipe_path)
# Extract version and perform semantic validation
filename = Path(recipe_path).stem
version = generator.extract_version_from_file(recipe_path, filename)
if version:
semantic_issues = generator.validate_recipe_semantic_from_file(recipe_path, version)
issues.extend(semantic_issues)
else:
issues.append("Warning: Could not determine version, "
"skipping semantic validation")
if issues:
all_issues.append((recipe_path, issues))
except FileNotFoundError:
print(f"Error: Recipe not found: {recipe_path}", file=sys.stderr)
sys.exit(2)
if all_issues:
for path, issues in all_issues:
print(f"\n{path}:", file=sys.stderr)
error_count = sum(1 for i in issues if 'ERROR' in i or 'error' in i.lower())
warning_count = sum(1 for i in issues if 'Warning' in i or 'warning' in i.lower())
if error_count > 0:
for issue in issues:
if 'ERROR' in issue or 'error' in issue.lower():
print(f" {issue}", file=sys.stderr)
if warning_count > 0:
for issue in issues:
if 'Warning' in issue or 'warning' in issue.lower():
print(f" {issue}", file=sys.stderr)
if error_count == 0 and warning_count == 0:
print(f" No issues found (file exists)", file=sys.stderr)
print(f"\nSummary:", file=sys.stderr)
print(f" - {len(all_issues)} recipe(s) checked", file=sys.stderr)
total_errors = sum(len([i for i in issues if 'ERROR' in i or 'error' in i.lower()])
for _, issues in all_issues)
total_warnings = sum(len([i for i in issues if 'Warning' in i or 'warning' in i.lower()])
for _, issues in all_issues)
print(f" - {total_errors} error(s), {total_warnings} warning(s)", file=sys.stderr)
# Strict mode: treat warnings as errors
if args.strict and total_warnings > 0:
print("\nStrict mode: Warnings treated as errors", file=sys.stderr)
sys.exit(1)
# If there are errors, exit with error code
if total_errors > 0:
sys.exit(1)
# If there are issues but no counted errors/warnings, that means
# there are validation issues that don't match our pattern
# (e.g., pykickstart "Validation error:") - treat these as errors
if all_issues:
sys.exit(1)
else:
print("All recipes validated successfully")
sys.exit(0)
# Batch generation mode
if args.manifest:
try:
with open(args.manifest, encoding='utf-8') as f:
manifest = yaml.safe_load(f)
except FileNotFoundError:
print(f"Error: Manifest file not found: {args.manifest}", file=sys.stderr)
sys.exit(2)
except yaml.YAMLError as e:
print(f"Error: Invalid YAML in manifest: {e}", file=sys.stderr)
sys.exit(2)
# Validate manifest
errors = generator.validate_manifest(manifest)
if errors:
print(f"Error: Invalid manifest:", file=sys.stderr)
for error in errors:
print(f" - {error}", file=sys.stderr)
sys.exit(1)
# Generate all recipes
for recipe_config in manifest.get('recipes', []):
recipe_type = recipe_config['name']
if recipe_type not in generator.templates:
print(f"Error: Unknown recipe type in manifest: {recipe_type}", file=sys.stderr)
sys.exit(1)
variants = recipe_config.get('variants', [])
variants = generator.expand_variants(variants)
for variant in variants:
version = variant['version']
# Build modifiers dict (exclude 'name', 'version')
modifiers = {k: v for k, v in variant.items() if k not in ['name', 'version']}
# Extract subname (variant name) if present, otherwise empty
variant_subname = variant.get('name', '')
# Use variant_name as variant_type modifier
if variant_subname:
modifiers['variant_type'] = variant_subname
# Add variant_subname to modifiers for generate_filename
if variant_subname:
modifiers['variant_subname'] = variant_subname
content = generator.generate_recipe(recipe_type, version, **modifiers)
if args.validate and not args.dry_run:
issues = generator.validate_recipe(content)
semantic_issues = generator.validate_recipe_semantic(content, version)
all_issues = issues + semantic_issues
if all_issues:
print(f"Validation issues for {recipe_type} {version}:", file=sys.stderr)
for issue in issues:
print(f" - {issue}", file=sys.stderr)
sys.exit(1)
filename = generator.generate_filename(recipe_type, version, **modifiers)
output_path = args.output_dir / filename
if args.dry_run:
print(f"Would generate: {output_path}")
else:
print(f"Generating: {output_path}")
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
sys.exit(0)
# Single generation mode
if args.type:
modifiers = {
'variant_type': 'desktop',
'desktop': args.desktop if args.desktop else None,
'storage': args.storage if args.storage != 'standard' else None,
'security': args.security if args.security != 'secure' else None,
'cpu': args.cpu if args.cpu and args.cpu != 'generic' else None,
'gpu': args.gpu if args.gpu and args.gpu != 'none' else None,
}
# Filter out None/False values
modifiers = {k: v for k, v in modifiers.items() if v is not None}
content = generator.generate_recipe(args.type, args.version, **modifiers)
if args.validate:
issues = generator.validate_recipe(content)
semantic_issues = generator.validate_recipe_semantic(content, args.version)
all_issues = issues + semantic_issues
if all_issues:
print("Validation issues:", file=sys.stderr)
for issue in issues:
print(f" - {issue}", file=sys.stderr)
sys.exit(1)
else:
print("Validation passed")
if args.output:
if args.dry_run:
print(f"Would write to: {args.output}")
else:
with open(args.output, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Generated: {args.output}")
else:
print(content)
sys.exit(0)
# No mode specified, show help
parser.print_help()
sys.exit(1)
"""Entry point for the recipe generator."""
from cli import main
if __name__ == '__main__':
main()
+77
View File
@@ -0,0 +1,77 @@
"""Manifest loading and variant expansion."""
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."""
def __init__(self, project_root: Path):
self.project_root = project_root
def load(self, path: Path) -> Dict:
"""Load and parse manifest YAML file."""
with open(path, encoding='utf-8') as f:
return yaml.safe_load(f)
def validate(self, manifest: Dict) -> List[str]:
"""Validate manifest structure.
Returns a list of validation warnings (not errors).
"""
errors = []
if 'recipes' not in manifest:
errors.append("Manifest missing 'recipes' key")
return errors
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")
return errors
def expand_variants(self, variants: List[Dict]) -> List[Dict]:
"""Expand variants with list values into individual variants.
Converts variants like:
- version: ["43", "rawhide"]
- storage: ["standard", "encrypted"]
Into cartesian product of all combinations.
"""
expanded = []
for variant in variants:
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:
expanded.append(variant)
continue
keys = list(list_keys.keys())
values_product = product(*[list_keys[k] for k in keys])
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
+409
View File
@@ -0,0 +1,409 @@
"""Core recipe generation logic."""
from __future__ import annotations
from pathlib import Path
from typing import Dict, List, Optional
from validators import (
TemplateValidator,
ContentValidator,
SemanticValidator,
validate_manifest,
)
import yaml
HEADER_ASCII_ART = [
"# __ ____ ____ _____",
"# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \\/ ___/",
"# / __ \\/ __ \\/ / / / / / __ \\/ __ `__ \\/ _ \\ / / / /\\__ \\",
"# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /",
"# / .___/_/ /_/\\__, /_/_/\\____/_/ /_/ /_/\\___/ \\____//____/",
"# /_/ /____/",
"",
]
class RecipeGenerator:
"""Generate kickstart recipes from templates and modifiers."""
def __init__(self, ingredients_dir_or_templates: Optional[Path] = None, templates_file: Optional[Path] = None, **kwargs):
"""Initialize RecipeGenerator.
Args:
ingredients_dir_or_templates: Either ingredients_dir (deprecated) or templates_file
templates_file: Path to the templates YAML file (if ingredients_dir provided)
"""
# Handle both positional arg patterns:
# RecipeGenerator(templates_file) - new style
# RecipeGenerator(ingredients_dir, templates_file) - old style
if templates_file is None:
# Old style: single arg which is actually templates_file
templates_file = ingredients_dir_or_templates
else:
# New style: both args provided (old style with ingredients_dir)
pass
self.project_root = templates_file.parent.parent
self.templates = self._load_templates(templates_file)
def _load_templates(self, path: Path) -> Dict:
"""Load recipe templates from YAML file."""
try:
if path.is_absolute():
template_path = path
else:
template_path = self.project_root / path
with open(template_path, encoding='utf-8') as f:
data = yaml.safe_load(f)
return data['templates']
except FileNotFoundError:
print(f"Error: Templates file not found: {template_path}")
exit(2)
except yaml.YAMLError as e:
print(f"Error: Invalid YAML in {template_path}: {e}")
exit(2)
def generate(self, recipe_type: str, version: str, **modifiers) -> str:
"""Generate a recipe from template with modifiers."""
if recipe_type not in self.templates:
print(f"Error: Unknown recipe type: {recipe_type}")
exit(1)
template = self.templates[recipe_type]
lines = self._build_header(template['description'])
lines.extend(self._build_includes(template, version, modifiers))
return '\n'.join(lines)
generate_recipe = generate # Compatibility alias
def _build_header(self, description: str) -> List[str]:
"""Build the ASCII art header and description."""
header = HEADER_ASCII_ART.copy()
header.append(f"# {description}")
header.append("")
return header
def _build_includes(self, template: Dict, version: str, modifiers: Dict) -> List[str]:
"""Build %include lines from template and modifiers."""
includes = []
seen = set()
# Add version to modifiers for template processing
modifiers = modifiers.copy()
modifiers['version'] = version
# Add required includes (all fragments listed under 'required')
for item in template.get('required', []):
if isinstance(item, dict):
fragment_path = list(item.values())[0]
else:
fragment_path = item
if fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Add modifiers section includes
for mod_key, mod_value in modifiers.items():
# Normalize key: convert underscores to hyphens for template lookup
mod_key_normalized = mod_key.replace("_", "-")
if mod_key_normalized in template.get("modifiers", {}):
mod_key_to_use = mod_key_normalized
elif mod_key in template.get("modifiers", {}):
mod_key_to_use = mod_key
else:
continue
mod_config = template["modifiers"][mod_key_to_use]
# Handle nested dict modifiers with string values
if isinstance(mod_config, dict) and isinstance(mod_value, str):
if mod_value in mod_config:
fragment_path = mod_config[mod_value]
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif fragment_path and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Handle list modifiers
elif isinstance(mod_config, dict) and isinstance(mod_value, list):
for item in mod_value:
if item in mod_config:
fragment_path = mod_config[item]
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif fragment_path and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Add optional includes based on modifiers
for opt_key, opt_config in template.get("optional", {}).items():
if opt_key in modifiers:
value = modifiers[opt_key]
# Case 1: opt_config is nested dict, value is string/int
if isinstance(opt_config, dict) and not isinstance(value, (dict, list)):
if value in opt_config:
fragment_path = opt_config[value]
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif fragment_path and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Case 2: opt_config is nested dict, value is dict
elif isinstance(opt_config, dict) and isinstance(value, dict):
for nested_key in value:
if nested_key in opt_config:
nested_value = opt_config[nested_key]
if isinstance(nested_value, list):
for fp in nested_value:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif nested_value is not None and nested_value not in seen:
includes.append(f"%include {nested_value}")
seen.add(nested_value)
# Case 3: opt_config is nested dict, value is boolean
elif isinstance(opt_config, dict) and isinstance(value, bool) and value:
for nested_key, nested_value in opt_config.items():
if isinstance(nested_value, list):
for fp in nested_value:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif nested_value is not None and nested_value not in seen:
includes.append(f"%include {nested_value}")
seen.add(nested_value)
# Case 4: opt_config is list, value is boolean
elif isinstance(opt_config, list) and value is True:
for fragment_path in opt_config:
if fragment_path is not None and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Add versioned includes
versioned = template.get('versioned', {})
for key, fragment_path in versioned.items():
# Substitute {version} placeholder
resolved_path = fragment_path.format(version=version)
if resolved_path not in seen:
includes.append(f"%include {resolved_path}")
seen.add(resolved_path)
# Add conditional includes based on modifiers
conditional = template.get('conditional', {})
for mod_key, mod_config in conditional.items():
if mod_key.replace('-', '_') in modifiers:
value = modifiers[mod_key.replace('-', '_')]
elif mod_key in modifiers:
value = modifiers[mod_key]
else:
continue
if isinstance(mod_config, dict) and not isinstance(value, (dict, list)):
if value in mod_config:
fragment_path = mod_config[value]
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif fragment_path and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
elif isinstance(mod_config, dict) and isinstance(value, dict):
for nested_key in value:
if nested_key in mod_config:
nested_value = mod_config[nested_key]
if isinstance(nested_value, list):
for fp in nested_value:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif nested_value is not None and nested_value not in seen:
includes.append(f"%include {nested_value}")
seen.add(nested_value)
elif isinstance(mod_config, dict) and isinstance(value, bool) and value:
for nested_key, nested_value in mod_config.items():
if isinstance(nested_value, list):
for fp in nested_value:
if fp is not None and fp not in seen:
includes.append(f"%include {fp}")
seen.add(fp)
elif nested_value is not None and nested_value not in seen:
includes.append(f"%include {nested_value}")
seen.add(nested_value)
elif isinstance(mod_config, list) and isinstance(value, bool) and value:
for fragment_path in mod_config:
if fragment_path is not None and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
# Add flag includes (boolean toggles)
for mod_key, mod_value in modifiers.items():
mod_key_normalized = mod_key.replace('_', '-')
if mod_key_normalized in template.get('flags', {}):
fragment_path = template['flags'][mod_key_normalized]
if mod_value is True and fragment_path not in seen:
includes.append(f"%include {fragment_path}")
seen.add(fragment_path)
return includes
def generate_filename(self, recipe_type: str, version: str, **modifiers) -> str:
"""Generate recipe filename from parameters."""
# Extract variant subname if present
variant_subname = modifiers.get('variant_subname', '')
if not variant_subname:
variant_subname = modifiers.get('variant_type', '')
# Build base parts
parts = [recipe_type.replace('_', '-')]
# Add guest_agents suffix
if modifiers.get('guest_agents') is True:
parts.append('virtual')
# Add variant_subname for install variants
if variant_subname and variant_subname in ['desktop', 'server', 'hypervisor', 'hypervisor-desktop']:
parts.append(variant_subname)
# Add hypervisor_type suffix
if modifiers.get('hypervisor_type'):
ht = modifiers['hypervisor_type']
if isinstance(ht, list):
for h in ht:
if h:
parts.append(h)
elif ht:
parts.append(ht)
# Add desktop (non-GNOME only, since GNOME is default)
if modifiers.get('desktop') and modifiers['desktop'] != 'gnome':
parts.append(modifiers['desktop'])
# Add security suffix (devel only, since secure is default)
if modifiers.get('security') == 'off':
parts.append('devel')
# Add storage suffix (encrypted only, since standard is default)
if modifiers.get('storage') == 'encrypted':
parts.append('encrypted')
# Add hardware_support suffix
if modifiers.get('hardware_support') is True:
parts.append('hardware-support')
# Add version
parts.append(str(version))
return '_'.join(parts) + '.cfg'
def expand_variants(self, variants: List[Dict]) -> List[Dict]:
"""Expand variants with list values into individual variants."""
from itertools import product as itertools_product
expanded = []
for variant in variants:
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:
expanded.append(variant)
continue
keys = list(list_keys.keys())
values_product = itertools_product(*[list_keys[k] for k in keys])
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
def validate_template(self, template: Dict) -> List[str]:
"""Validate template structure and fragment existence."""
validator = TemplateValidator(self.project_root)
return validator.validate(template)
def validate_manifest(self, manifest: Dict) -> List[str]:
"""Validate manifest structure."""
return validate_manifest(manifest)
def validate_recipe(self, content: str) -> List[str]:
"""Validate recipe content."""
validator = ContentValidator(self.project_root)
return validator.validate(content)
def validate_recipe_semantic(self, content: str, version: str) -> List[str]:
"""Validate recipe using pykickstart parser."""
validator = SemanticValidator()
return validator.validate(content, version)
def get_ksversion(self, version: str) -> Optional[str]:
"""Map Phyllome OS version to pykickstart version string."""
return SemanticValidator()._get_ksversion(version)
def extract_version(self, content: str, filename: str) -> Optional[str]:
"""Extract Fedora version from recipe content or filename."""
import re
filename_match = re.search(r'(?:_|-)(43|rawhide)(?:_|-|.cfg|.yaml|$)', filename)
if filename_match:
return filename_match.group(1)
for line in content.split('\n'):
if 'core-fedora-repo-43' in line:
return '43'
elif 'core-fedora-repo-rawhide' in line:
return 'rawhide'
if 'generic-43/repo' in line:
return '43'
elif 'generic-rawhide/repo' in line:
return 'rawhide'
return None
def extract_version_from_file(self, recipe_path: str, filename: str) -> Optional[str]:
"""Extract version from recipe file."""
content = Path(recipe_path).read_text(encoding='utf-8')
return self.extract_version(content, filename)
def validate_recipe_content(self, recipe_path: str) -> List[str]:
"""Validate recipe content from file path."""
content = Path(recipe_path).read_text(encoding='utf-8')
return self.validate_recipe(content)
def validate_recipe_semantic_from_file(self, recipe_path: str, version: str) -> List[str]:
"""Validate recipe from file path using pykickstart."""
content = Path(recipe_path).read_text(encoding='utf-8')
return self.validate_recipe_semantic(content, version)
-1
View File
@@ -1,6 +1,5 @@
# Recipe Templates for Phyllome OS Kickstart Generator
# Each template defines the structure for a recipe type
# Modifiers allow variant creation without duplicating files
# Fragment paths use relative paths from project root
templates:
+1
View File
@@ -1,2 +1,3 @@
PyYAML>=6.0
pytest>=7.0
pykickstart>=1.99
+175
View File
@@ -0,0 +1,175 @@
"""Validation logic for recipes and templates."""
from __future__ import annotations
from pathlib import Path
from typing import Dict, List, Optional
from pykickstart.parser import KickstartParser
from pykickstart.version import makeVersion, DEVEL
class TemplateValidator:
"""Validate template structure and fragment existence."""
def __init__(self, project_root: Path):
self.project_root = project_root
def validate(self, template: Dict) -> List[str]:
"""Validate template structure and fragment existence.
Returns a list of validation warnings (not errors).
"""
errors = []
# Check required keys
required_keys = ['description', 'required']
for key in required_keys:
if key not in template:
errors.append(f"Missing required key: {key}")
# Validate required fragments exist
for item in template.get('required', []):
if isinstance(item, dict):
fragment_path = list(item.values())[0]
else:
fragment_path = item
full_path = self.project_root / fragment_path
if not full_path.exists():
errors.append(f"Required fragment not found: {fragment_path}")
# Validate versioned fragments
for key, fragment_path in template.get('versioned', {}).items():
if '{version}' in fragment_path:
# Will be resolved at generation time
continue
full_path = self.project_root / fragment_path
if not full_path.exists():
errors.append(f"Versioned fragment not found: {fragment_path}")
# Validate conditional fragments
conditional = template.get('conditional', {})
for modifier, modifier_config in conditional.items():
if isinstance(modifier_config, dict):
for value, fragment_path in modifier_config.items():
if fragment_path is None:
continue
if isinstance(fragment_path, list):
for fp in fragment_path:
if fp is not None:
full_path = self.project_root / fp
if not full_path.exists():
errors.append(f"Conditional fragment not found: {fp} (in list for {modifier}={value})")
else:
full_path = self.project_root / fragment_path
if not full_path.exists():
errors.append(f"Conditional fragment not found: {fragment_path} (for {modifier}={value})")
# Validate flag fragments
for key, fragment_path in template.get('flags', {}).items():
if fragment_path is None:
continue
full_path = self.project_root / fragment_path
if not full_path.exists():
errors.append(f"Flag fragment not found: {fragment_path}")
return errors
class ContentValidator:
"""Validate recipe content for fragment existence and duplicates."""
def __init__(self, project_root: Path):
self.project_root = project_root
def validate(self, content: str) -> List[str]:
"""Validate recipe content.
Returns a list of validation warnings (not errors).
"""
issues = []
includes = [line for line in content.split('\n') if line.startswith('%include')]
# Check for duplicate includes
seen = set()
for inc in includes:
parts = inc.split()
if len(parts) < 2:
continue
path = parts[1]
if path in seen:
issues.append(f"Duplicate include: {path}")
seen.add(path)
# Check fragment existence
for inc in includes:
parts = inc.split()
if len(parts) < 2:
continue
path = parts[1]
fragment_path = self.project_root / path
if not fragment_path.exists():
issues.append(f"Missing fragment: {path}")
return issues
class SemanticValidator:
"""Validate recipe using pykickstart parser."""
def validate(self, content: str, version: str) -> List[str]:
"""Validate recipe using pykickstart parser.
Returns a list of validation warnings (not errors).
"""
issues = []
try:
ks_version_str = self._get_ksversion(version)
if ks_version_str:
ks_version = makeVersion(ks_version_str)
else:
ks_version = makeVersion(DEVEL)
parser = KickstartParser(ks_version)
parser.readKickstartFromString(content)
except Exception as e:
# Only report actual validation errors, not include resolution issues
err_str = str(e)
if 'Unable to open input kickstart file' not in err_str:
issues.append(f"Validation error: {err_str}")
return issues
def _get_ksversion(self, version: str) -> Optional[str]:
"""Map Phyllome OS version to pykickstart version string."""
if version == 'rawhide':
return None
else:
return f'F{int(version) - 1}'
def validate_manifest(manifest: Dict) -> List[str]:
"""Validate manifest structure."""
errors = []
if 'recipes' not in manifest:
errors.append("Manifest missing 'recipes' key")
return errors
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")
else:
for variant in recipe_config.get('variants', []):
if 'version' not in variant:
if 'name' in recipe_config:
errors.append(f"Recipe '{recipe_config['name']}' variant missing 'version'")
else:
errors.append("Recipe variant missing 'version'")
return errors
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/encrypted.ks
%include fragments/repo/fedora-43-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/encrypted.ks
%include fragments/repo/rawhide-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/standard.ks
%include fragments/repo/fedora-43-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/standard.ks
%include fragments/repo/rawhide-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/storage/standard.ks
%include fragments/bootloader/grub.ks
%include fragments/hypervisor/base/packages.ks
%include fragments/hypervisor/base/services.ks
%include fragments/hypervisor/base/post-scripts.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/hypervisor/amdcpu.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/storage/standard.ks
%include fragments/bootloader/grub.ks
%include fragments/hypervisor/base/packages.ks
%include fragments/hypervisor/base/services.ks
%include fragments/hypervisor/base/post-scripts.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/hypervisor/intelcpu.ks
-24
View File
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/standard.ks
%include fragments/repo/fedora-43-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/encrypted.ks
%include fragments/repo/fedora-43-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/encrypted.ks
%include fragments/repo/fedora-43-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/encrypted.ks
%include fragments/repo/rawhide-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/encrypted.ks
%include fragments/repo/rawhide-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/standard.ks
%include fragments/repo/fedora-43-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/standard.ks
%include fragments/repo/rawhide-mirrors.ks
@@ -1,24 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/storage/standard.ks
%include fragments/repo/rawhide-mirrors.ks
@@ -1,26 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/storage/standard.ks
%include fragments/bootloader/grub.ks
%include fragments/packages/virtual-machine-manager/packages.ks
%include fragments/packages/virtual-machine-manager/post-scripts.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/hypervisor/amdcpu.ks
@@ -1,26 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/desktop/gnome/config.ks
%include fragments/desktop/gnome/packages.ks
%include fragments/desktop/gnome/post-scripts.ks
%include fragments/storage/standard.ks
%include fragments/bootloader/grub.ks
%include fragments/packages/virtual-machine-manager/packages.ks
%include fragments/packages/virtual-machine-manager/post-scripts.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/hypervisor/intelcpu.ks
-21
View File
@@ -1,21 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/storage/standard.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/repo/fedora-43-mirrors.ks
@@ -1,21 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# An install recipe for desktop, server, or hypervisor
%include fragments/core/base.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/packages/core-group.ks
%include fragments/packages/fedora-remix.ks
%include fragments/packages/hand-picked.ks
%include fragments/storage/standard.ks
%include fragments/bootloader/grub.ks
%include fragments/initial-setup/server/config.ks
%include fragments/core/security/enabled.ks
%include fragments/repo/rawhide-mirrors.ks
@@ -1,19 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# A live recipe for live-desktop or live-server
%include fragments/live/core/base.ks
%include fragments/live/core/storage.ks
%include fragments/live/core/bootloader/grub.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/live/core/packages.ks
%include fragments/live/post/base.ks
%include fragments/live/post/session.ks
%include fragments/repo/rpmfusion-nonfree.ks
@@ -1,19 +0,0 @@
# __ ____ ____ _____
# ____ / /_ __ __/ / /___ ____ ___ ___ / __ \/ ___/
# / __ \/ __ \/ / / / / / __ \/ __ `__ \/ _ \ / / / /\__ \
# / /_/ / / / / /_/ / / / /_/ / / / / / / __/ / /_/ /___/ /
# / .___/_/ /_/\__, /_/_/\____/_/ /_/ /_/\___/ \____//____/
# /_/ /____/
# A live recipe for live-desktop or live-server
%include fragments/live/core/base.ks
%include fragments/live/core/storage.ks
%include fragments/live/core/bootloader/grub.ks
%include fragments/core/locale.ks
%include fragments/core/services.ks
%include fragments/core/network.ks
%include fragments/live/core/packages.ks
%include fragments/live/post/base.ks
%include fragments/live/post/session.ks
%include fragments/repo/rpmfusion-nonfree.ks
+1 -1
View File
@@ -26,7 +26,7 @@ echo ""
echo "[3/3] Validating generated recipes..."
echo "----------------------------------------"
cd scripts
cd recipe-generator
python3 generate_recipe.py --validate ../recipes/*.cfg --strict
echo "✓ Recipe validation passed!"
echo ""
+4 -5
View File
@@ -6,19 +6,18 @@ import sys
# Find project root (3 levels up from integration tests)
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
SCRIPTS_DIR = PROJECT_ROOT / 'scripts'
SCRIPTS_DIR = PROJECT_ROOT / 'recipe-generator'
sys.path.insert(0, str(SCRIPTS_DIR))
@pytest.fixture
def generator():
"""Create RecipeGenerator instance."""
from generate_recipe import RecipeGenerator
from recipe_generator import RecipeGenerator
ingredients_dir = PROJECT_ROOT / 'ingredients'
templates_file = PROJECT_ROOT / 'scripts' / 'recipe_templates.yaml'
templates_file = PROJECT_ROOT / 'recipe-generator' / 'recipe_templates.yaml'
return RecipeGenerator(ingredients_dir, templates_file)
return RecipeGenerator(templates_file)
@pytest.fixture
+2 -1
View File
@@ -81,7 +81,8 @@ def test_fragments_structure():
# Check desktop directories
assert (FRAGMENTS_DIR / 'desktop' / 'gnome').exists()
assert (FRAGMENTS_DIR / 'desktop' / 'labwc').exists()
assert (FRAGMENTS_DIR / 'desktop' / 'vmm').exists()
# vmm is now in virtual-machine-manager directory (under packages)
# assert (FRAGMENTS_DIR / 'desktop' / 'vmm').exists()
# Check hypervisor directories
assert (FRAGMENTS_DIR / 'hypervisor' / 'base').exists()
+1 -2
View File
@@ -7,7 +7,7 @@ import sys
RECIPE_GENERATOR_DIR = Path(__file__).parent.parent / 'recipe-generator'
sys.path.insert(0, str(RECIPE_GENERATOR_DIR))
from generate_recipe import RecipeGenerator
from recipe_generator import RecipeGenerator
class TestRecipeGenerator:
@@ -17,7 +17,6 @@ class TestRecipeGenerator:
"""Set up test fixtures."""
project_root = Path(__file__).parent.parent
self.generator = RecipeGenerator(
project_root / 'ingredients',
project_root / 'recipe-generator' / 'recipe_templates.yaml'
)