import shutil import os import sys import subprocess # 解析参数 clean = '--noclean' not in sys.argv kernel_only = '--kernel-only' in sys.argv noapp = '--noapp' in sys.argv bootloader = '--bootloader' in sys.argv # --onlyrun:跳过全部编译/打包,直接用现有 disk.img 启动 QEMU only_run = '--onlyrun' in sys.argv # --onlyapp:只编译库与应用(保留兼容;现在这已是默认行为) only_app = '--onlyapp' in sys.argv # --kernel:重编内核。**默认不重编内核**(多数迭代只改应用/库),显式传才编。 build_kernel = '--kernel' in sys.argv or kernel_only # --libs:编译 Libs/ 下的动态库(不带 --app 时默认也会编) libs = '--libs' in sys.argv # --app=a,b,c:只编译列出的应用(逗号分隔、可多个)。不传则编译全部应用。 app_filter = None for _arg in sys.argv: if _arg.startswith('--app='): app_filter = [x.strip().lower() for x in _arg[len('--app='):].split(',') if x.strip()] def BuildKernel(): """编译内核:链接器一次产出带符号 kernel.elf,再由 objcopy 二次处理成裸镜像 kernel.bin""" cmd = r'..\..\ViperC\output\viperc.exe --project ./VKernel/project.vpj --clear-cache --rebuild-includes' if clean: cmd += ' --clean' os.system(cmd) # project.vpj 的 linker 已改为直接产出 ELF(保留符号表,去掉 --oformat binary/--strip-all), # kernel.bin 由同一份 ELF 二次处理得到 —— 两者布局逐字节一致,日志里的 rip 可直接用 # objdump / nm / readelf 落到函数名,不必再猜地址(此前自行 glob .obj 去链会得到不同布局)。 # --set-section-flags .bss=alloc,load,contents:把 .bss 一并写进裸镜像(ELF 里 .bss 默认 # NOBITS、不占文件空间),这样 bootloader 按“文件大小”预留就能覆盖 .bss。 kernel_elf = "VKernel/__vpcbuild__/output/kernel.elf" kernel_bin = "VKernel/__vpcbuild__/output/kernel.bin" rc = os.system(f'llvm-objcopy.exe -O binary --set-section-flags .bss=alloc,load,contents "{kernel_elf}" "{kernel_bin}"') if rc != 0: print("[build] 警告: objcopy 生成 kernel.bin 失败") shutil.copy(kernel_bin, "./disk/sys/kernel/x86_64/kernel.bin") # 应用表:(--app 的筛选名, 工程目录, 产物文件名, disk 下的目标文件名) APP_LIST = [ ("hello", "Apps/HelloWorld", "helloworld.elf", "hello.elf"), ("terminal", "Apps/Terminal", "terminal.elf", "terminal.elf"), ("gargantua", "Apps/Gargantua", "gargantua.elf", "gargantua.elf"), ("scene3d", "Apps/Scene3D", "scene3d.elf", "scene3d.elf"), ("explorer", "Apps/Explorer", "explorer.elf", "explorer.elf"), ] # 动态库表:(工程目录, 产物文件名) LIB_LIST = [ ("Libs/SerialLogger", "slog.so"), ("Libs/VPUI64", "vpui64.so"), ] def BuildLibs(): """编译 Libs/ 下的动态库(应用运行期经 LOAD_SO 加载)。""" os.makedirs("./disk/libs", exist_ok=True) for proj, out in LIB_LIST: cmd = rf'..\..\ViperC\output\viperc.exe --project {proj}/project.vpj --clear-cache' if clean: cmd += ' --clean' os.system(cmd) src = f"{proj}/__vpcbuild__/output/{out}" if os.path.exists(src): shutil.copy(src, f"./disk/libs/{out}") print(f"[build] lib {out} -> disk/libs/{out}") def CopyExisting(): """--noapp:不编译,直接把已有产物拷进 disk/(缺文件只警告)。""" os.makedirs("./disk/apps", exist_ok=True) for _, proj, out, dst in APP_LIST: src = f"{proj}/__vpcbuild__/output/{out}" if os.path.exists(src): shutil.copy(src, f"./disk/apps/{dst}") else: print(f"[build] 警告: {src} 不存在,请先完整构建一次") for proj, out in LIB_LIST: src = f"{proj}/__vpcbuild__/output/{out}" if os.path.exists(src): shutil.copy(src, f"./disk/libs/{out}") def BuildApps(): """编译库与应用,并拷贝到 disk/。 · 不带 --app:编库 + 全部应用 · --app=a,b:只编列出的应用(库要一起编就再加 --libs) """ if noapp: print("[build] --noapp: 跳过所有库与应用编译,复用已有缓存文件") CopyExisting() return if app_filter is None or libs: BuildLibs() else: print("[build] --app 已指定且未传 --libs:跳过库编译") os.makedirs("./disk/apps", exist_ok=True) for name, proj, out, dst in APP_LIST: if app_filter is not None and name not in app_filter: continue cmd = rf'..\..\ViperC\output\viperc.exe --project {proj}/project.vpj --clear-cache' if clean: cmd += ' --clean' os.system(cmd) src = f"{proj}/__vpcbuild__/output/{out}" shutil.copy(src, f"./disk/apps/{dst}") print(f"[build] app {name}: {src} -> disk/apps/{dst}") def BuildBootloader(): """编译 bootloader(GNU-EFI:MSYS make → WSL Ubuntu); bootx64.efi 不由 viperc 产出,容易忘记重编而一直用旧引导器(改了 boot.c / 内核预留逻辑 却仍在用旧件)。显式传 --bootloader 才重编;未传时若 boot.c 比产物新则给出提醒。""" boot_src = "boot/boot.c" boot_out = "disk/EFI/BOOT/bootx64.efi" if bootloader: print("[build] building bootloader (make -C boot)...") boot_rc = os.system('make -C boot') if boot_rc != 0: print("[build] 警告: bootloader 构建失败(需 MSYS make + WSL Ubuntu + _UNCADK/tools/gnu-efi)") elif os.path.exists(boot_src) and os.path.exists(boot_out) and os.path.getmtime(boot_src) > os.path.getmtime(boot_out): print("[build] 警告: boot/boot.c 比 disk/EFI/BOOT/bootx64.efi 新 → bootloader 已过期,请用 --bootloader 重编") def BuildDisk(): """把 disk/ 目录内容打包成 disk.img""" os.system('powershell -ExecutionPolicy Bypass -File ./Scripts/disk.ps1') def RunQemu(): """清空 serial.log 并启动 QEMU,退出后打印串口日志""" serial_log = os.path.abspath("serial.log") if os.path.exists(serial_log): os.remove(serial_log) print(f"[build] CWD: {os.getcwd()}") print(f"[build] serial_log: {serial_log}") # OVMF 需要**可写**的 NVRAM。此前只传 code fd、且它是可写的 pflash ⇒ OVMF 把启动变量 # 写进 code 卷本身 ⇒ 每次启动都改动 edk2-x86_64-code.fd ⇒ 反复出现 # "pacman -Qkk 报 SHA256 校验和不一致 / 固件损坏"(重装只能好一阵,下次启动又被写)。 # 正确做法:code 只读 + 单独的一份可写 vars 副本(首次从 edk2-i386-vars.fd 拷贝生成)。 ovmf_code = r'D:\msys64\mingw64\share\qemu\edk2-x86_64-code.fd' ovmf_vars_src = r'D:\msys64\mingw64\share\qemu\edk2-i386-vars.fd' ovmf_vars_dir = os.path.abspath('build') os.makedirs(ovmf_vars_dir, exist_ok=True) ovmf_vars = os.path.join(ovmf_vars_dir, 'ovmf-vars.fd') if not os.path.exists(ovmf_vars): try: shutil.copy(ovmf_vars_src, ovmf_vars) print(f"[build] OVMF vars 副本已生成: {ovmf_vars}") except Exception as e: print(f"[build] 警告: 生成 OVMF vars 副本失败({e}),回退为旧的 -pflash code 用法") ovmf_vars = None qemu_cmd = [ 'qemu-system-x86_64', ] if ovmf_vars is not None: qemu_cmd += ['-drive', f'if=pflash,format=raw,readonly=on,file={ovmf_code}', '-drive', f'if=pflash,format=raw,file={ovmf_vars}'] else: qemu_cmd += ['-pflash', ovmf_code] qemu_cmd += [ '-drive', 'file=disk.img,format=raw,if=ide', '-m', '256m', '-vga', 'std', '-usb', '-device', 'usb-kbd', '-device', 'usb-mouse', '-accel', 'tcg,tb-size=128', '-serial', f'file:{serial_log}', '-no-reboot' ] print(f"[build] QEMU cmd: {' '.join(qemu_cmd)}") proc = subprocess.Popen(qemu_cmd) proc.wait() print(f"[build] serial_log path: {serial_log}") print(f"[build] file exists: {os.path.exists(serial_log)}") if os.path.exists(serial_log): with open(serial_log, "r", encoding="utf-8", errors="replace") as f: content = f.read() print(f"[build] serial_log size: {len(content)} bytes") print(content) else: print("[build] serial.log not found!") if only_run: print("[build] --onlyrun: 跳过编译/打包,直接用现有 disk.img 启动 QEMU") else: if kernel_only: print("[build] --kernel-only: 只编译内核") BuildKernel() else: if build_kernel: BuildKernel() else: print("[build] 默认不重编内核(需要时加 --kernel)") BuildApps() BuildBootloader() BuildDisk() RunQemu()