From 898c831065c616af13289576474f323482a3b659 Mon Sep 17 00:00:00 2001 From: Lukas Greve Date: Thu, 26 Mar 2026 15:31:18 +0100 Subject: [PATCH] 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 --- .gitea/workflows/build-iso.yaml | 4 +- .gitea/workflows/container-tests.yaml | 2 +- .gitea/workflows/validate-recipes.yaml | 8 +- recipe-generator/Makefile | 4 +- recipe-generator/cli.py | 323 ++++++++ recipe-generator/generate_recipe.py | 766 +----------------- recipe-generator/manifest.py | 77 ++ recipe-generator/recipe_generator.py | 409 ++++++++++ recipe-generator/recipe_templates.yaml | 1 - recipe-generator/requirements.txt | 1 + recipe-generator/validators.py | 175 ++++ ..._desktop_encrypted_hardware-support_43.cfg | 24 - ...top_encrypted_hardware-support_rawhide.cfg | 24 - .../install_desktop_hardware-support_43.cfg | 24 - ...stall_desktop_hardware-support_rawhide.cfg | 24 - ..._hypervisor_amdcpu_hardware-support_43.cfg | 24 - ...ypervisor_intelcpu_hardware-support_43.cfg | 24 - recipes/install_virtual_desktop_43.cfg | 24 - .../install_virtual_desktop_encrypted_43.cfg | 24 - ..._desktop_encrypted_hardware-support_43.cfg | 24 - ...top_encrypted_hardware-support_rawhide.cfg | 24 - ...tall_virtual_desktop_encrypted_rawhide.cfg | 24 - ...ll_virtual_desktop_hardware-support_43.cfg | 24 - ...rtual_desktop_hardware-support_rawhide.cfg | 24 - recipes/install_virtual_desktop_rawhide.cfg | 24 - ...l_virtual_hypervisor-desktop_amdcpu_43.cfg | 26 - ...virtual_hypervisor-desktop_intelcpu_43.cfg | 26 - recipes/install_virtual_server_43.cfg | 21 - recipes/install_virtual_server_rawhide.cfg | 21 - recipes/live_desktop_hardware-support_43.cfg | 19 - recipes/live_server_hardware-support_43.cfg | 19 - tests/container/run-tests.sh | 2 +- tests/integration/conftest.py | 9 +- tests/integration/test_integration.py | 3 +- tests/test_recipe_generator.py | 3 +- 35 files changed, 1004 insertions(+), 1251 deletions(-) create mode 100644 recipe-generator/cli.py mode change 100755 => 100644 recipe-generator/generate_recipe.py create mode 100644 recipe-generator/manifest.py create mode 100644 recipe-generator/recipe_generator.py create mode 100644 recipe-generator/validators.py delete mode 100644 recipes/install_desktop_encrypted_hardware-support_43.cfg delete mode 100644 recipes/install_desktop_encrypted_hardware-support_rawhide.cfg delete mode 100644 recipes/install_desktop_hardware-support_43.cfg delete mode 100644 recipes/install_desktop_hardware-support_rawhide.cfg delete mode 100644 recipes/install_hypervisor_amdcpu_hardware-support_43.cfg delete mode 100644 recipes/install_hypervisor_intelcpu_hardware-support_43.cfg delete mode 100644 recipes/install_virtual_desktop_43.cfg delete mode 100644 recipes/install_virtual_desktop_encrypted_43.cfg delete mode 100644 recipes/install_virtual_desktop_encrypted_hardware-support_43.cfg delete mode 100644 recipes/install_virtual_desktop_encrypted_hardware-support_rawhide.cfg delete mode 100644 recipes/install_virtual_desktop_encrypted_rawhide.cfg delete mode 100644 recipes/install_virtual_desktop_hardware-support_43.cfg delete mode 100644 recipes/install_virtual_desktop_hardware-support_rawhide.cfg delete mode 100644 recipes/install_virtual_desktop_rawhide.cfg delete mode 100644 recipes/install_virtual_hypervisor-desktop_amdcpu_43.cfg delete mode 100644 recipes/install_virtual_hypervisor-desktop_intelcpu_43.cfg delete mode 100644 recipes/install_virtual_server_43.cfg delete mode 100644 recipes/install_virtual_server_rawhide.cfg delete mode 100644 recipes/live_desktop_hardware-support_43.cfg delete mode 100644 recipes/live_server_hardware-support_43.cfg diff --git a/.gitea/workflows/build-iso.yaml b/.gitea/workflows/build-iso.yaml index 753b51d..11e32eb 100644 --- a/.gitea/workflows/build-iso.yaml +++ b/.gitea/workflows/build-iso.yaml @@ -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: diff --git a/.gitea/workflows/container-tests.yaml b/.gitea/workflows/container-tests.yaml index d66aa9d..a5d9794 100644 --- a/.gitea/workflows/container-tests.yaml +++ b/.gitea/workflows/container-tests.yaml @@ -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 diff --git a/.gitea/workflows/validate-recipes.yaml b/.gitea/workflows/validate-recipes.yaml index 99def86..dd7122d 100644 --- a/.gitea/workflows/validate-recipes.yaml +++ b/.gitea/workflows/validate-recipes.yaml @@ -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 diff --git a/recipe-generator/Makefile b/recipe-generator/Makefile index 4a957cb..db5c174 100644 --- a/recipe-generator/Makefile +++ b/recipe-generator/Makefile @@ -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: diff --git a/recipe-generator/cli.py b/recipe-generator/cli.py new file mode 100644 index 0000000..a9c4355 --- /dev/null +++ b/recipe-generator/cli.py @@ -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) diff --git a/recipe-generator/generate_recipe.py b/recipe-generator/generate_recipe.py old mode 100755 new mode 100644 index 6fbe4fb..ed58853 --- a/recipe-generator/generate_recipe.py +++ b/recipe-generator/generate_recipe.py @@ -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() diff --git a/recipe-generator/manifest.py b/recipe-generator/manifest.py new file mode 100644 index 0000000..8510f2f --- /dev/null +++ b/recipe-generator/manifest.py @@ -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 diff --git a/recipe-generator/recipe_generator.py b/recipe-generator/recipe_generator.py new file mode 100644 index 0000000..1e65ec7 --- /dev/null +++ b/recipe-generator/recipe_generator.py @@ -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) diff --git a/recipe-generator/recipe_templates.yaml b/recipe-generator/recipe_templates.yaml index ac7c6d2..63500b5 100644 --- a/recipe-generator/recipe_templates.yaml +++ b/recipe-generator/recipe_templates.yaml @@ -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: diff --git a/recipe-generator/requirements.txt b/recipe-generator/requirements.txt index 26efb36..8d55326 100644 --- a/recipe-generator/requirements.txt +++ b/recipe-generator/requirements.txt @@ -1,2 +1,3 @@ PyYAML>=6.0 pytest>=7.0 +pykickstart>=1.99 diff --git a/recipe-generator/validators.py b/recipe-generator/validators.py new file mode 100644 index 0000000..c74569f --- /dev/null +++ b/recipe-generator/validators.py @@ -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 diff --git a/recipes/install_desktop_encrypted_hardware-support_43.cfg b/recipes/install_desktop_encrypted_hardware-support_43.cfg deleted file mode 100644 index 71209b9..0000000 --- a/recipes/install_desktop_encrypted_hardware-support_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_desktop_encrypted_hardware-support_rawhide.cfg b/recipes/install_desktop_encrypted_hardware-support_rawhide.cfg deleted file mode 100644 index 8de4589..0000000 --- a/recipes/install_desktop_encrypted_hardware-support_rawhide.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_desktop_hardware-support_43.cfg b/recipes/install_desktop_hardware-support_43.cfg deleted file mode 100644 index 3897012..0000000 --- a/recipes/install_desktop_hardware-support_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_desktop_hardware-support_rawhide.cfg b/recipes/install_desktop_hardware-support_rawhide.cfg deleted file mode 100644 index 78a7410..0000000 --- a/recipes/install_desktop_hardware-support_rawhide.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_hypervisor_amdcpu_hardware-support_43.cfg b/recipes/install_hypervisor_amdcpu_hardware-support_43.cfg deleted file mode 100644 index 2dbac46..0000000 --- a/recipes/install_hypervisor_amdcpu_hardware-support_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_hypervisor_intelcpu_hardware-support_43.cfg b/recipes/install_hypervisor_intelcpu_hardware-support_43.cfg deleted file mode 100644 index 61016cf..0000000 --- a/recipes/install_hypervisor_intelcpu_hardware-support_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_desktop_43.cfg b/recipes/install_virtual_desktop_43.cfg deleted file mode 100644 index 3897012..0000000 --- a/recipes/install_virtual_desktop_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_desktop_encrypted_43.cfg b/recipes/install_virtual_desktop_encrypted_43.cfg deleted file mode 100644 index 71209b9..0000000 --- a/recipes/install_virtual_desktop_encrypted_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_desktop_encrypted_hardware-support_43.cfg b/recipes/install_virtual_desktop_encrypted_hardware-support_43.cfg deleted file mode 100644 index 71209b9..0000000 --- a/recipes/install_virtual_desktop_encrypted_hardware-support_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_desktop_encrypted_hardware-support_rawhide.cfg b/recipes/install_virtual_desktop_encrypted_hardware-support_rawhide.cfg deleted file mode 100644 index 8de4589..0000000 --- a/recipes/install_virtual_desktop_encrypted_hardware-support_rawhide.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_desktop_encrypted_rawhide.cfg b/recipes/install_virtual_desktop_encrypted_rawhide.cfg deleted file mode 100644 index 8de4589..0000000 --- a/recipes/install_virtual_desktop_encrypted_rawhide.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_desktop_hardware-support_43.cfg b/recipes/install_virtual_desktop_hardware-support_43.cfg deleted file mode 100644 index 3897012..0000000 --- a/recipes/install_virtual_desktop_hardware-support_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_desktop_hardware-support_rawhide.cfg b/recipes/install_virtual_desktop_hardware-support_rawhide.cfg deleted file mode 100644 index 78a7410..0000000 --- a/recipes/install_virtual_desktop_hardware-support_rawhide.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_desktop_rawhide.cfg b/recipes/install_virtual_desktop_rawhide.cfg deleted file mode 100644 index 78a7410..0000000 --- a/recipes/install_virtual_desktop_rawhide.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_hypervisor-desktop_amdcpu_43.cfg b/recipes/install_virtual_hypervisor-desktop_amdcpu_43.cfg deleted file mode 100644 index dd10c46..0000000 --- a/recipes/install_virtual_hypervisor-desktop_amdcpu_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_hypervisor-desktop_intelcpu_43.cfg b/recipes/install_virtual_hypervisor-desktop_intelcpu_43.cfg deleted file mode 100644 index 414df2b..0000000 --- a/recipes/install_virtual_hypervisor-desktop_intelcpu_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_server_43.cfg b/recipes/install_virtual_server_43.cfg deleted file mode 100644 index 50cd5e4..0000000 --- a/recipes/install_virtual_server_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/install_virtual_server_rawhide.cfg b/recipes/install_virtual_server_rawhide.cfg deleted file mode 100644 index ffa8881..0000000 --- a/recipes/install_virtual_server_rawhide.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/live_desktop_hardware-support_43.cfg b/recipes/live_desktop_hardware-support_43.cfg deleted file mode 100644 index cbccb98..0000000 --- a/recipes/live_desktop_hardware-support_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/recipes/live_server_hardware-support_43.cfg b/recipes/live_server_hardware-support_43.cfg deleted file mode 100644 index cbccb98..0000000 --- a/recipes/live_server_hardware-support_43.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/tests/container/run-tests.sh b/tests/container/run-tests.sh index 37cb24c..12952e5 100755 --- a/tests/container/run-tests.sh +++ b/tests/container/run-tests.sh @@ -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 "" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8d281c6..b54f524 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -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 diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index b367d94..bc25f4a 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -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() diff --git a/tests/test_recipe_generator.py b/tests/test_recipe_generator.py index 2c4bbff..d4a4de5 100644 --- a/tests/test_recipe_generator.py +++ b/tests/test_recipe_generator.py @@ -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' )