summaryrefslogtreecommitdiff
path: root/scripts/configure.py
blob: 053aac9131e4c3a4c1191fe519bb09c9dc7be2a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
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()