diff options
Diffstat (limited to 'scripts/configure.py')
| -rwxr-xr-x | scripts/configure.py | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/scripts/configure.py b/scripts/configure.py new file mode 100755 index 0000000..053aac9 --- /dev/null +++ b/scripts/configure.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +""" +configure.py — MyOS Kernel Configuration Generator + +Reads .config (or a specified config file), validates dependencies, +and generates: + - include/kernel/config.h (C++ preprocessor defines) + - config_generated.mk (Makefile variables) + +Usage: + python3 scripts/configure.py # reads .config + python3 scripts/configure.py myconfig # reads myconfig + python3 scripts/configure.py --arch x86_64 # set arch, use default config + python3 scripts/configure.py --check # validate only, don't generate + +The ARCH variable is NOT set in .config — it comes from the build system. +This script receives it via --arch and injects it into the generated output. +""" + +import sys +import os +import argparse +from pathlib import Path +from datetime import datetime + +# ============================================================================ +# Dependency rules +# +# Format: (option, requires, description) +# If 'option' is 'y', then 'requires' must also be 'y' +# ============================================================================ + +DEPENDENCIES = [ + ("FS_EXT2", "DRIVER_VIRTIO_BLK", "ext2 requires a block device driver"), + ("FS_FAT32", "DRIVER_VIRTIO_BLK", "FAT32 requires a block device driver"), + ("FEATURE_NETWORK", "DRIVER_VIRTIO_NET", "networking requires a network driver"), + ("FEATURE_MODULES", "FS_INITRAMFS", "loadable modules require a filesystem"), + ("FEATURE_SMP", None, None), # no deps, just a marker +] + +# ============================================================================ +# Conflict rules +# +# Format: (option_a, option_b, description) +# Both cannot be 'y' at the same time +# ============================================================================ + +CONFLICTS = [ + # None yet — add as needed +] + +# ============================================================================ +# Valid values for non-boolean options +# ============================================================================ + +VALID_VALUES = { + "PMM_TYPE": ["buddy", "bitmap"], + "HEAP_TYPE": ["slab", "simple"], +} + +# ============================================================================ +# Options that must be present (with defaults if missing) +# ============================================================================ + +DEFAULTS = { + "DEBUG_SERIAL": "n", + "DEBUG_VERBOSE_BOOT": "n", + "DEBUG_PAGE_ALLOC": "n", + "DEBUG_SCHEDULER": "n", + "DEBUG_SYSCALL_TRACE": "n", + "MAX_CPUS": "4", + "KERNEL_STACK_SIZE": "16384", + "PMM_TYPE": "buddy", + "HEAP_TYPE": "slab", + "DRIVER_FBCON": "y", + "DRIVER_UART_16550": "n", + "DRIVER_PL011_UART": "n", + "DRIVER_PS2KBD": "n", + "DRIVER_VIRTIO_BLK": "n", + "DRIVER_AHCI": "n", + "DRIVER_USB": "n", + "DRIVER_VIRTIO_NET": "n", + "FS_INITRAMFS": "y", + "FS_TMPFS": "y", + "FS_EXT2": "n", + "FS_FAT32": "n", + "FBCON_FG_COLOR": "0x00CCCCCC", + "FBCON_BG_COLOR": "0x001A1A2E", + "FEATURE_SMP": "n", + "FEATURE_MODULES": "n", + "FEATURE_NETWORK": "n", + "FEATURE_POSIX_SIGNALS": "n", + "KERNEL_FILE_NAME": "kernel.elf", +} + + +def parse_config(path: str) -> dict: + """Parse a .config file into a dict.""" + config = {} + with open(path) as f: + for lineno, line in enumerate(f, 1): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + print(f"WARNING: {path}:{lineno}: malformed line: {line}", + file=sys.stderr) + continue + key, val = line.split("=", 1) + key = key.strip() + val = val.strip() + # Strip surrounding quotes from string values + if len(val) >= 2 and val[0] == '"' and val[-1] == '"': + val = val[1:-1] + config[key] = val + return config + + +def validate(config: dict) -> list: + """Validate config. Returns list of error strings (empty = ok).""" + errors = [] + + # Check dependencies + for opt, requires, desc in DEPENDENCIES: + if requires is None: + continue + if config.get(opt) == "y" and config.get(requires) != "y": + errors.append(f"{opt}=y requires {requires}=y ({desc})") + + # Check conflicts + for opt_a, opt_b, desc in CONFLICTS: + if config.get(opt_a) == "y" and config.get(opt_b) == "y": + errors.append(f"{opt_a} conflicts with {opt_b} ({desc})") + + # Check valid values + for key, valid in VALID_VALUES.items(): + val = config.get(key) + if val is not None and val not in valid: + errors.append(f"{key}={val} is invalid. Must be one of: {valid}") + + # Check numeric values + for key in ("MAX_CPUS", "KERNEL_STACK_SIZE"): + val = config.get(key) + if val is not None: + try: + int(val) + except ValueError: + errors.append(f"{key}={val} must be a number") + + return errors + + +def generate_config_h(config: dict, arch: str, output: str): + """Generate include/kernel/config.h from config dict.""" + lines = [ + "#pragma once", + "", + "// ============================================================================", + "// config.h — Auto-generated by scripts/configure.py", + f"// Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + f"// Architecture: {arch}", + "//", + "// DO NOT EDIT — edit .config and re-run make instead.", + "// ============================================================================", + "", + ] + + # Group by prefix for readability + groups = {} + for key, val in sorted(config.items()): + prefix = key.split("_")[0] + if prefix not in groups: + groups[prefix] = [] + groups[prefix].append((key, val)) + + for prefix, items in groups.items(): + lines.append(f"// ── {prefix} " + "─" * (60 - len(prefix))) + for key, val in items: + if val == "y": + lines.append(f"#define CONFIG_{key} 1") + elif val == "n": + lines.append(f"/* #undef CONFIG_{key} */") + elif val.startswith("0x"): + # Hex constant + lines.append(f"#define CONFIG_{key} {val}") + else: + # Try numeric + try: + int(val) + lines.append(f"#define CONFIG_{key} {val}") + except ValueError: + # String value — quote it for some, raw for others + lines.append(f'#define CONFIG_{key} "{val}"') + # For enum-like options, also define a variant for preprocessor comparisons + if key in VALID_VALUES: + lines.append(f'#define CONFIG_{key}_{val.upper()} 1') + + lines.append("") + + os.makedirs(os.path.dirname(output), exist_ok=True) + with open(output, "w") as f: + f.write("\n".join(lines) + "\n") + + +def generate_config_mk(config: dict, arch: str, output: str): + """Generate config_generated.mk from config dict.""" + lines = [ + "# ============================================================================", + "# config_generated.mk — Auto-generated by scripts/configure.py", + f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + "# DO NOT EDIT — edit .config and re-run make instead.", + "# ============================================================================", + "", + ] + + for key, val in sorted(config.items()): + lines.append(f"CONFIG_{key} := {val}") + + with open(output, "w") as f: + f.write("\n".join(lines) + "\n") + + +def main(): + parser = argparse.ArgumentParser(description="MyOS kernel configuration generator") + parser.add_argument("config_file", nargs="?", default=".config", + help="Path to config file (default: .config)") + parser.add_argument("--arch", default=None, + help="Target architecture (x86_64 or aarch64)") + parser.add_argument("--check", action="store_true", + help="Validate only, don't generate files") + parser.add_argument("--config-h", default="include/kernel/config.h", + help="Output path for config.h") + parser.add_argument("--config-mk", default="config_generated.mk", + help="Output path for config_generated.mk") + args = parser.parse_args() + + # ── Read config ───────────────────────────────────────────────────── + + if not os.path.exists(args.config_file): + print(f"ERROR: Config file '{args.config_file}' not found.", + file=sys.stderr) + print(f" Create one with: cp configs/default_x86_64 .config", + file=sys.stderr) + sys.exit(1) + + config = parse_config(args.config_file) + + # ── Apply defaults for missing keys ───────────────────────────────── + + for key, default_val in DEFAULTS.items(): + if key not in config: + config[key] = default_val + + # ── Determine architecture ────────────────────────────────────────── + + arch = args.arch or os.environ.get("ARCH", "x86_64") + + # ── Validate ──────────────────────────────────────────────────────── + + errors = validate(config) + if errors: + print("Configuration errors:", file=sys.stderr) + for e in errors: + print(f" ERROR: {e}", file=sys.stderr) + sys.exit(1) + + if args.check: + print("Configuration is valid.") + sys.exit(0) + + # ── Generate ──────────────────────────────────────────────────────── + + generate_config_h(config, arch, args.config_h) + generate_config_mk(config, arch, args.config_mk) + + # ── Summary ───────────────────────────────────────────────────────── + + enabled = [k for k, v in config.items() if v == "y"] + disabled = [k for k, v in config.items() if v == "n"] + other = [k for k, v in config.items() if v not in ("y", "n")] + + print(f"Configuration generated for {arch}:") + print(f" {len(enabled)} options enabled, {len(disabled)} disabled, " + f"{len(other)} custom values") + print(f" → {args.config_h}") + print(f" → {args.config_mk}") + + +if __name__ == "__main__": + main() |
