pyOS-kernel 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. pyos/__init__.py +1 -1
  2. pyos/boot/bootloader.asm +72 -120
  3. pyos/builder.py +240 -151
  4. pyos/cli.py +172 -166
  5. pyos/compiler/codegen.py +181 -215
  6. pyos/debug.py +89 -0
  7. pyos/emulator.py +69 -117
  8. pyos/interrupts/handler.py +1 -1
  9. pyos/kernel.py +29 -22
  10. pyos/runtime/include/debug.h +20 -0
  11. pyos/runtime/include/heap.h +13 -0
  12. pyos/runtime/include/idt.h +10 -0
  13. pyos/runtime/include/io.h +32 -0
  14. pyos/runtime/include/kernel.h +23 -0
  15. pyos/runtime/include/keyboard.h +24 -0
  16. pyos/runtime/include/pic.h +12 -0
  17. pyos/runtime/include/screen.h +20 -0
  18. pyos/runtime/include/syscall.h +13 -0
  19. pyos/runtime/include/types.h +22 -0
  20. pyos/runtime/linker.ld +33 -0
  21. pyos/runtime/src/debug.c +130 -0
  22. pyos/runtime/src/heap.c +48 -0
  23. pyos/runtime/src/idt.c +156 -0
  24. pyos/runtime/src/isr.S +138 -0
  25. pyos/runtime/src/isr.asm +132 -0
  26. pyos/runtime/src/keyboard.c +110 -0
  27. pyos/runtime/src/kmain.c +50 -0
  28. pyos/runtime/src/pic.c +65 -0
  29. pyos/runtime/src/screen.c +134 -0
  30. pyos/runtime/src/start.S +17 -0
  31. pyos/runtime/src/start.asm +12 -0
  32. pyos/runtime/src/syscall.c +43 -0
  33. pyos/toolchain.py +196 -0
  34. pyos_kernel-0.2.0.dist-info/METADATA +143 -0
  35. pyos_kernel-0.2.0.dist-info/RECORD +51 -0
  36. {pyos_kernel-0.1.0.dist-info → pyos_kernel-0.2.0.dist-info}/WHEEL +1 -1
  37. pyos_kernel-0.1.0.dist-info/METADATA +0 -323
  38. pyos_kernel-0.1.0.dist-info/RECORD +0 -27
  39. pyos_kernel-0.1.0.dist-info/licenses/LICENSE +0 -21
  40. {pyos_kernel-0.1.0.dist-info → pyos_kernel-0.2.0.dist-info}/entry_points.txt +0 -0
  41. {pyos_kernel-0.1.0.dist-info → pyos_kernel-0.2.0.dist-info}/top_level.txt +0 -0
pyos/__init__.py CHANGED
@@ -10,7 +10,7 @@ from .memory.gdt import GDT
10
10
  from .interrupts.handler import Interrupts
11
11
  from .syscalls.handler import SysCall
12
12
 
13
- __version__ = "0.1.0"
13
+ __version__ = "0.2.0"
14
14
  __all__ = [
15
15
  "Kernel",
16
16
  "Screen",
pyos/boot/bootloader.asm CHANGED
@@ -1,174 +1,126 @@
1
- ; pyOS Bootloader
2
- ; Simple bootloader that loads kernel and switches to protected mode
3
-
1
+ ; pyOS Bootloader — LBA (int 13h AH=42h), QEMU-friendly
4
2
  [BITS 16]
5
3
  [ORG 0x7C00]
6
4
 
7
5
  KERNEL_OFFSET equ 0x1000
6
+ KERNEL_SEGS equ 0x0000
7
+ KERNEL_SECTORS equ 64
8
8
 
9
9
  start:
10
- ; Setup segments
10
+ cli
11
11
  xor ax, ax
12
12
  mov ds, ax
13
13
  mov es, ax
14
14
  mov ss, ax
15
15
  mov sp, 0x7C00
16
+ sti
17
+
18
+ mov [boot_drive], dl
16
19
 
17
- ; Save boot drive
18
- mov [BOOT_DRIVE], dl
20
+ mov al, 'B'
21
+ out 0xE9, al
19
22
 
20
- ; Print message
21
- mov si, MSG_BOOT
22
- call print
23
+ ; Check extensions
24
+ mov ah, 0x41
25
+ mov bx, 0x55AA
26
+ mov dl, [boot_drive]
27
+ int 0x13
28
+ jc .no_lba
29
+ cmp bx, 0xAA55
30
+ jne .no_lba
23
31
 
24
- ; Load kernel
25
- call load_kernel
32
+ mov al, 'X'
33
+ out 0xE9, al
26
34
 
27
- mov si, MSG_OK
28
- call print
35
+ ; LBA read: DAP
36
+ mov si, dap
37
+ mov ah, 0x42
38
+ mov dl, [boot_drive]
39
+ int 0x13
40
+ jc .error
29
41
 
30
- ; Go to protected mode
42
+ mov al, 'L'
43
+ out 0xE9, al
44
+ jmp .pm
45
+
46
+ .no_lba:
47
+ ; Fallback CHS: one track from sector 2 (17 sectors)
48
+ mov al, 'C'
49
+ out 0xE9, al
50
+ mov ax, KERNEL_SEGS
51
+ mov es, ax
52
+ mov bx, KERNEL_OFFSET
53
+ mov ah, 0x02
54
+ mov al, 17
55
+ mov ch, 0
56
+ mov cl, 2
57
+ mov dh, 0
58
+ mov dl, [boot_drive]
59
+ int 0x13
60
+ jc .error
61
+ mov al, 'L'
62
+ out 0xE9, al
63
+
64
+ .pm:
31
65
  cli
32
-
33
- ; Enable A20
34
66
  in al, 0x92
35
67
  or al, 2
36
68
  out 0x92, al
37
-
38
- ; Load GDT
39
69
  lgdt [gdt_desc]
40
-
41
- ; Enter protected mode
42
70
  mov eax, cr0
43
71
  or eax, 1
44
72
  mov cr0, eax
45
-
46
73
  jmp 0x08:pm_start
47
74
 
48
- ;----------------------------------------
49
- ; Print (SI = string)
50
- ;----------------------------------------
51
- print:
52
- pusha
53
- .loop:
54
- lodsb
55
- test al, al
56
- jz .done
57
- mov ah, 0x0E
58
- int 0x10
59
- jmp .loop
60
- .done:
61
- popa
62
- ret
63
-
64
- ;----------------------------------------
65
- ; Load kernel - read sectors one by one
66
- ;----------------------------------------
67
- load_kernel:
68
- pusha
69
-
70
- mov si, MSG_LOAD
71
- call print
72
-
73
- ; Read 16 sectors (8KB) - enough for our kernel
74
- mov bx, KERNEL_OFFSET ; Destination
75
- mov cl, 2 ; Start sector
76
- mov ch, 0 ; Cylinder
77
- mov dh, 0 ; Head
78
- mov dl, [BOOT_DRIVE]
79
-
80
- mov al, 16 ; Sectors to read
81
-
82
- .read:
83
- mov ah, 0x02 ; Read function
84
- mov al, 1 ; Read 1 sector at a time
85
- int 0x13
86
- jc .error
87
-
88
- add bx, 512 ; Next buffer position
89
- inc cl ; Next sector
90
- cmp cl, 18 ; Sector 18 reached?
91
- jne .continue
92
- mov cl, 1 ; Reset to sector 1
93
- inc dh ; Next head
94
- cmp dh, 2
95
- jne .continue
96
- mov dh, 0
97
- inc ch ; Next cylinder
98
-
99
- .continue:
100
- cmp bx, KERNEL_OFFSET + (16 * 512) ; Read 16 sectors?
101
- jb .read
102
-
103
- popa
104
- ret
105
-
106
75
  .error:
107
- mov si, MSG_ERR
108
- call print
76
+ mov al, 'E'
77
+ out 0xE9, al
109
78
  jmp $
110
79
 
111
- ;----------------------------------------
112
- ; GDT
113
- ;----------------------------------------
114
- gdt_start:
115
- dq 0 ; Null descriptor
80
+ ; Disk Address Packet
81
+ align 4
82
+ dap:
83
+ db 16
84
+ db 0
85
+ dw KERNEL_SECTORS
86
+ dw KERNEL_OFFSET
87
+ dw KERNEL_SEGS
88
+ dq 1 ; LBA 1 (sector after MBR)
116
89
 
90
+ gdt_start:
91
+ dq 0
117
92
  gdt_code:
118
- dw 0xFFFF ; Limit
119
- dw 0x0000 ; Base low
120
- db 0x00 ; Base mid
121
- db 10011010b ; Access
122
- db 11001111b ; Flags + limit high
123
- db 0x00 ; Base high
124
-
93
+ dw 0xFFFF, 0x0000
94
+ db 0x00, 10011010b, 11001111b, 0x00
125
95
  gdt_data:
126
- dw 0xFFFF
127
- dw 0x0000
128
- db 0x00
129
- db 10010010b
130
- db 11001111b
131
- db 0x00
96
+ dw 0xFFFF, 0x0000
97
+ db 0x00, 10010010b, 11001111b, 0x00
132
98
  gdt_end:
133
-
134
99
  gdt_desc:
135
100
  dw gdt_end - gdt_start - 1
136
101
  dd gdt_start
137
102
 
138
- ;----------------------------------------
139
- ; 32-bit mode
140
- ;----------------------------------------
141
103
  [BITS 32]
142
104
  pm_start:
143
- mov ax, 0x10 ; Data segment
105
+ mov ax, 0x10
144
106
  mov ds, ax
145
107
  mov es, ax
146
108
  mov fs, ax
147
109
  mov gs, ax
148
110
  mov ss, ax
149
111
  mov esp, 0x90000
150
-
151
- ; Jump to kernel
112
+ mov al, 'K'
113
+ out 0xE9, al
152
114
  call KERNEL_OFFSET
153
-
154
- ; Halt
115
+ mov al, 'H'
116
+ out 0xE9, al
155
117
  cli
156
118
  .halt:
157
119
  hlt
158
120
  jmp .halt
159
121
 
160
- ;----------------------------------------
161
- ; Data
162
- ;----------------------------------------
163
122
  [BITS 16]
164
- BOOT_DRIVE: db 0
165
- MSG_BOOT: db "pyOS v0.1", 13, 10, 0
166
- MSG_LOAD: db "Loading...", 13, 10, 0
167
- MSG_OK: db "OK", 13, 10, 0
168
- MSG_ERR: db "Disk Error!", 0
169
-
170
- ;----------------------------------------
171
- ; Boot signature
172
- ;----------------------------------------
123
+ boot_drive: db 0
124
+
173
125
  times 510 - ($ - $$) db 0
174
126
  dw 0xAA55
pyos/builder.py CHANGED
@@ -1,177 +1,266 @@
1
1
  """
2
- pyOS Builder - Build OS images
2
+ pyOS Builder bootloader (ASM) + C kernel via MinGW-w64
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import shutil
5
9
  import subprocess
6
10
  import tempfile
7
- import shutil
8
- import os
9
11
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Optional
12
+ from typing import TYPE_CHECKING, List, Optional
13
+
14
+ from .toolchain import Toolchain
11
15
 
12
16
  if TYPE_CHECKING:
13
17
  from .kernel import Kernel
14
18
 
15
19
 
16
20
  class BuildError(Exception):
17
- """Raised when build fails."""
18
- pass
21
+ """Raised when build fails, with human-readable reason."""
22
+
23
+ def __init__(self, message: str, hint: str = ""):
24
+ self.hint = hint
25
+ full = message
26
+ if hint:
27
+ full = f"{message}\n Hint: {hint}"
28
+ super().__init__(full)
19
29
 
20
30
 
21
31
  class OSBuilder:
22
- """
23
- Builds complete OS images from compiled kernel.
24
- """
25
-
26
- def __init__(self, kernel: 'Kernel'):
32
+ """Builds floppy OS images from Python kernel + C runtime."""
33
+
34
+ KERNEL_SECTORS = 64 # must match bootloader.asm
35
+
36
+ def __init__(self, kernel: "Kernel"):
27
37
  self.kernel = kernel
38
+ self.runtime_dir = Path(__file__).parent / "runtime"
28
39
  self.bootloader_path = Path(__file__).parent / "boot" / "bootloader.asm"
29
-
40
+ self.toolchain = Toolchain()
41
+ self._last_glue: Optional[str] = None
42
+ self._last_symbols: list = []
43
+
44
+ def get_asm(self) -> str:
45
+ from .compiler.codegen import CodeGenerator
46
+
47
+ gen = CodeGenerator(self.kernel)
48
+ # Prefer C glue content for inspection
49
+ return gen.generate()
50
+
51
+ def save_asm(self, output_path: str) -> str:
52
+ glue = self.get_asm()
53
+ Path(output_path).write_text(glue, encoding="utf-8")
54
+ return output_path
55
+
30
56
  def build_bin(self, output_path: str) -> str:
31
- """
32
- Build a raw binary image.
33
-
34
- Args:
35
- output_path: Output file path
36
-
37
- Returns:
38
- Path to the built image.
39
- """
40
- from .compiler.assembler import Assembler
57
+ self.toolchain.require()
58
+ gcc = self.toolchain.gcc
59
+ nasm = self.toolchain.nasm
60
+ objcopy = self.toolchain.objcopy
61
+ if not objcopy:
62
+ raise BuildError(
63
+ "objcopy not found next to MinGW gcc",
64
+ "Reinstall WinLibs/MinGW so objcopy.exe is on PATH",
65
+ )
66
+
41
67
  from .compiler.codegen import CodeGenerator
42
-
43
- with tempfile.TemporaryDirectory() as tmpdir:
44
- tmpdir = Path(tmpdir)
45
-
68
+
69
+ generator = CodeGenerator(self.kernel)
70
+ glue = generator.generate()
71
+ self._last_glue = glue
72
+ self._last_symbols = generator.get_symbol_map()
73
+
74
+ with tempfile.TemporaryDirectory() as tmp:
75
+ tmpdir = Path(tmp)
76
+ glue_c = tmpdir / "glue.c"
77
+ glue_c.write_text(glue, encoding="utf-8")
78
+
46
79
  # Assemble bootloader
47
- assembler = Assembler(self.kernel.config.arch.value)
48
- bootloader_bin = tmpdir / "bootloader.bin"
49
-
50
- bootloader_asm = self.bootloader_path.read_text()
51
- assembler.assemble_to_file(bootloader_asm, str(bootloader_bin), "bin")
52
-
53
- # Generate and assemble kernel
54
- generator = CodeGenerator(self.kernel)
55
- kernel_asm = generator.generate()
56
-
80
+ boot_bin = tmpdir / "bootloader.bin"
81
+ self._run(
82
+ [nasm, "-f", "bin", str(self.bootloader_path), "-o", str(boot_bin)],
83
+ where="NASM bootloader",
84
+ hint="Check pyos/boot/bootloader.asm for syntax errors",
85
+ )
86
+
87
+ # Assemble start.S / isr.S with GCC (matches MinGW symbol decoration)
88
+ start_o = tmpdir / "start.o"
89
+ isr_o = tmpdir / "isr.o"
90
+ self._run(
91
+ [gcc, "-m32", "-c", str(self.runtime_dir / "src" / "start.S"), "-o", str(start_o)],
92
+ where="GCC assemble start.S",
93
+ )
94
+ self._run(
95
+ [gcc, "-m32", "-c", str(self.runtime_dir / "src" / "isr.S"), "-o", str(isr_o)],
96
+ where="GCC assemble isr.S",
97
+ )
98
+
99
+ c_sources = [
100
+ "debug.c",
101
+ "screen.c",
102
+ "keyboard.c",
103
+ "pic.c",
104
+ "idt.c",
105
+ "heap.c",
106
+ "syscall.c",
107
+ "kmain.c",
108
+ ]
109
+ objects: List[Path] = [start_o, isr_o]
110
+ include = str(self.runtime_dir / "include")
111
+ cflags = [
112
+ "-m32",
113
+ "-ffreestanding",
114
+ "-fno-builtin",
115
+ "-fno-stack-protector",
116
+ "-fno-pic",
117
+ "-fno-asynchronous-unwind-tables",
118
+ "-fno-exceptions",
119
+ "-nostdlib",
120
+ "-Wall",
121
+ "-Wextra",
122
+ "-O1",
123
+ f"-I{include}",
124
+ ]
125
+
126
+ for name in c_sources:
127
+ src = self.runtime_dir / "src" / name
128
+ obj = tmpdir / (name.replace(".c", ".o"))
129
+ self._run(
130
+ [gcc, *cflags, "-c", str(src), "-o", str(obj)],
131
+ where=f"GCC compile {name}",
132
+ hint="A C runtime file failed to compile — see compiler output above",
133
+ )
134
+ objects.append(obj)
135
+
136
+ glue_o = tmpdir / "glue.o"
137
+ self._run(
138
+ [gcc, *cflags, "-c", str(glue_c), "-o", str(glue_o)],
139
+ where="GCC compile generated glue.c",
140
+ hint="Your Python Screen/Memory ops produced invalid C — check print strings and positions",
141
+ )
142
+ objects.append(glue_o)
143
+
57
144
  kernel_bin = tmpdir / "kernel.bin"
58
- assembler.assemble_to_file(kernel_asm, str(kernel_bin), "bin")
59
-
60
- # Combine bootloader + kernel
61
- with open(output_path, "wb") as out:
62
- # Write bootloader (512 bytes)
63
- bootloader_data = bootloader_bin.read_bytes()
64
- out.write(bootloader_data)
65
-
66
- # Write kernel
67
- kernel_data = kernel_bin.read_bytes()
68
- out.write(kernel_data)
69
-
70
- # Pad to 16 sectors (8KB) after bootloader
71
- current_size = len(bootloader_data) + len(kernel_data)
72
- target_size = 512 + (16 * 512) # Bootloader + 16 sectors
73
- if current_size < target_size:
74
- out.write(bytes(target_size - current_size))
75
-
76
- return output_path
77
-
145
+ map_file = tmpdir / "kernel.map"
146
+ pe = tmpdir / "kernel.pe"
147
+ ld = self.toolchain.ld
148
+ # Link PE/COFF first, then strip to flat binary at VMA 0x1000
149
+ if ld:
150
+ self._run(
151
+ [
152
+ ld,
153
+ "-m",
154
+ "i386pe",
155
+ "--image-base",
156
+ "0",
157
+ "-T",
158
+ str(self.runtime_dir / "linker.ld"),
159
+ "-Map",
160
+ str(map_file),
161
+ "-o",
162
+ str(pe),
163
+ *[str(o) for o in objects],
164
+ ],
165
+ where="ld link kernel (PE)",
166
+ hint="Missing symbol usually means glue/runtime mismatch",
167
+ )
168
+ else:
169
+ self._run(
170
+ [
171
+ gcc,
172
+ "-m32",
173
+ "-ffreestanding",
174
+ "-nostdlib",
175
+ "-nostartfiles",
176
+ "-Wl,-T," + str(self.runtime_dir / "linker.ld"),
177
+ "-Wl,-Map," + str(map_file),
178
+ "-o",
179
+ str(pe),
180
+ *[str(o) for o in objects],
181
+ ],
182
+ where="GCC link kernel",
183
+ hint="Missing symbol usually means glue/runtime mismatch",
184
+ )
185
+ self._run(
186
+ [objcopy, "-O", "binary", str(pe), str(kernel_bin)],
187
+ where="objcopy PE -> flat binary",
188
+ )
189
+
190
+ boot_data = boot_bin.read_bytes()
191
+ if len(boot_data) != 512 or boot_data[-2:] != b"\x55\xaa":
192
+ raise BuildError(
193
+ "Bootloader is invalid (expected 512-byte sector ending with 0x55AA)",
194
+ "bootloader.asm must end with times 510-($-$$) db 0 / dw 0xAA55",
195
+ )
196
+
197
+ kernel_data = kernel_bin.read_bytes()
198
+ max_kernel = self.KERNEL_SECTORS * 512
199
+ if len(kernel_data) > max_kernel:
200
+ raise BuildError(
201
+ f"Kernel is too large ({len(kernel_data)} bytes > {max_kernel} byte limit)",
202
+ "Reduce Screen.print strings or raise KERNEL_SECTORS in bootloader + builder",
203
+ )
204
+
205
+ # Write symbol map beside output for human debug
206
+ out = Path(output_path)
207
+ out.parent.mkdir(parents=True, exist_ok=True)
208
+ map_path = out.with_suffix(out.suffix + ".symbols.json")
209
+ map_path.write_text(
210
+ json.dumps(
211
+ {
212
+ "symbols": self._last_symbols,
213
+ "kernel_bytes": len(kernel_data),
214
+ "heap_size": self.kernel.config.heap_size,
215
+ },
216
+ indent=2,
217
+ ),
218
+ encoding="utf-8",
219
+ )
220
+
221
+ # Pad to a full 1.44MB floppy so BIOS int 0x13 does not hang in QEMU
222
+ floppy_size = 1474560
223
+ with open(out, "wb") as f:
224
+ f.write(boot_data)
225
+ f.write(kernel_data)
226
+ current = len(boot_data) + len(kernel_data)
227
+ if current < floppy_size:
228
+ f.write(bytes(floppy_size - current))
229
+
230
+ # Also copy map file from linker if present
231
+ link_map = tmpdir / "kernel.map"
232
+ if link_map.exists():
233
+ shutil.copy(link_map, out.with_suffix(out.suffix + ".map"))
234
+
235
+ return str(output_path)
236
+
78
237
  def build_iso(self, output_path: str) -> str:
79
238
  """
80
- Build an ISO image for CD/USB boot.
81
-
82
- Args:
83
- output_path: Output file path
84
-
85
- Returns:
86
- Path to the built ISO.
239
+ Build a bootable image.
240
+
241
+ Without grub-mkrescue, writes a floppy-style .bin under the given path
242
+ (QEMU should boot it with -fda). True ISO requires grub-mkrescue.
87
243
  """
88
- with tempfile.TemporaryDirectory() as tmpdir:
89
- tmpdir = Path(tmpdir)
90
-
91
- # Build binary first
92
- bin_path = tmpdir / "os.bin"
93
- self.build_bin(str(bin_path))
94
-
95
- # Create ISO directory structure
96
- iso_dir = tmpdir / "iso"
97
- boot_dir = iso_dir / "boot"
98
- grub_dir = boot_dir / "grub"
99
- grub_dir.mkdir(parents=True)
100
-
101
- # Copy kernel
102
- shutil.copy(bin_path, boot_dir / "kernel.bin")
103
-
104
- # Create GRUB config
105
- grub_cfg = grub_dir / "grub.cfg"
106
- grub_cfg.write_text("""
107
- set timeout=0
108
- set default=0
109
-
110
- menuentry "pyOS" {
111
- multiboot /boot/kernel.bin
112
- boot
113
- }
114
- """)
115
-
116
- # Try to create ISO with grub-mkrescue
117
- if self._has_grub_mkrescue():
118
- self._create_iso_grub(iso_dir, output_path)
119
- else:
120
- # Fallback: just copy the binary
121
- shutil.copy(bin_path, output_path)
122
- print("Warning: grub-mkrescue not found, created raw binary instead of ISO")
123
-
124
- return output_path
125
-
126
- def _has_grub_mkrescue(self) -> bool:
127
- """Check if grub-mkrescue is available."""
128
- try:
129
- result = subprocess.run(
130
- ["grub-mkrescue", "--version"],
131
- capture_output=True,
244
+ out = Path(output_path)
245
+ # Always produce a working floppy image first
246
+ bin_path = out.with_suffix(".bin") if out.suffix.lower() == ".iso" else out
247
+ self.build_bin(str(bin_path))
248
+
249
+ if out.suffix.lower() == ".iso":
250
+ # Honest fallback: copy bin to requested path and warn via print
251
+ shutil.copy(bin_path, out)
252
+ print(
253
+ "Warning: created floppy-boot image at "
254
+ f"{out} (not a GRUB ISO). Run with: qemu-system-i386 -fda {out}"
132
255
  )
133
- return result.returncode == 0
134
- except FileNotFoundError:
135
- return False
136
-
137
- def _create_iso_grub(self, iso_dir: Path, output_path: str) -> None:
138
- """Create ISO using grub-mkrescue."""
139
- cmd = [
140
- "grub-mkrescue",
141
- "-o", output_path,
142
- str(iso_dir),
143
- ]
144
-
256
+ return str(out)
257
+
258
+ def _run(self, cmd: List[str], where: str, hint: str = "") -> None:
145
259
  result = subprocess.run(cmd, capture_output=True, text=True)
146
-
147
- if result.returncode != 0:
148
- raise BuildError(f"grub-mkrescue failed:\n{result.stderr}")
149
-
150
- def get_asm(self) -> str:
151
- """
152
- Get the generated Assembly code.
153
-
154
- Returns:
155
- Assembly source code.
156
- """
157
- from .compiler.codegen import CodeGenerator
158
-
159
- generator = CodeGenerator(self.kernel)
160
- return generator.generate()
161
-
162
- def save_asm(self, output_path: str) -> str:
163
- """
164
- Save generated Assembly to file.
165
-
166
- Args:
167
- output_path: Output file path
168
-
169
- Returns:
170
- Path to saved file.
171
- """
172
- asm = self.get_asm()
173
-
174
- with open(output_path, "w") as f:
175
- f.write(asm)
176
-
177
- return output_path
260
+ if result.returncode == 0:
261
+ return
262
+ err = (result.stderr or result.stdout or "").strip()
263
+ raise BuildError(
264
+ f"Build failed at: {where}\n{err}",
265
+ hint or "Run `pyos check` to verify gcc/nasm/qemu",
266
+ )