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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
Executable → Regular
+2
-764
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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,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,2 +1,3 @@
|
||||
PyYAML>=6.0
|
||||
pytest>=7.0
|
||||
pykickstart>=1.99
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user