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.
- pyos/__init__.py +1 -1
- pyos/boot/bootloader.asm +72 -120
- pyos/builder.py +240 -151
- pyos/cli.py +172 -166
- pyos/compiler/codegen.py +181 -215
- pyos/debug.py +89 -0
- pyos/emulator.py +69 -117
- pyos/interrupts/handler.py +1 -1
- pyos/kernel.py +29 -22
- pyos/runtime/include/debug.h +20 -0
- pyos/runtime/include/heap.h +13 -0
- pyos/runtime/include/idt.h +10 -0
- pyos/runtime/include/io.h +32 -0
- pyos/runtime/include/kernel.h +23 -0
- pyos/runtime/include/keyboard.h +24 -0
- pyos/runtime/include/pic.h +12 -0
- pyos/runtime/include/screen.h +20 -0
- pyos/runtime/include/syscall.h +13 -0
- pyos/runtime/include/types.h +22 -0
- pyos/runtime/linker.ld +33 -0
- pyos/runtime/src/debug.c +130 -0
- pyos/runtime/src/heap.c +48 -0
- pyos/runtime/src/idt.c +156 -0
- pyos/runtime/src/isr.S +138 -0
- pyos/runtime/src/isr.asm +132 -0
- pyos/runtime/src/keyboard.c +110 -0
- pyos/runtime/src/kmain.c +50 -0
- pyos/runtime/src/pic.c +65 -0
- pyos/runtime/src/screen.c +134 -0
- pyos/runtime/src/start.S +17 -0
- pyos/runtime/src/start.asm +12 -0
- pyos/runtime/src/syscall.c +43 -0
- pyos/toolchain.py +196 -0
- pyos_kernel-0.2.0.dist-info/METADATA +143 -0
- pyos_kernel-0.2.0.dist-info/RECORD +51 -0
- {pyos_kernel-0.1.0.dist-info → pyos_kernel-0.2.0.dist-info}/WHEEL +1 -1
- pyos_kernel-0.1.0.dist-info/METADATA +0 -323
- pyos_kernel-0.1.0.dist-info/RECORD +0 -27
- pyos_kernel-0.1.0.dist-info/licenses/LICENSE +0 -21
- {pyos_kernel-0.1.0.dist-info → pyos_kernel-0.2.0.dist-info}/entry_points.txt +0 -0
- {pyos_kernel-0.1.0.dist-info → pyos_kernel-0.2.0.dist-info}/top_level.txt +0 -0
pyos/__init__.py
CHANGED
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
20
|
+
mov al, 'B'
|
|
21
|
+
out 0xE9, al
|
|
19
22
|
|
|
20
|
-
;
|
|
21
|
-
mov
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
32
|
+
mov al, 'X'
|
|
33
|
+
out 0xE9, al
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
|
|
76
|
+
mov al, 'E'
|
|
77
|
+
out 0xE9, al
|
|
109
78
|
jmp $
|
|
110
79
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
112
|
+
mov al, 'K'
|
|
113
|
+
out 0xE9, al
|
|
152
114
|
call KERNEL_OFFSET
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def __init__(self, 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
)
|