Подготовить автономную сборку и запуск перед разделением проектов

This commit is contained in:
Александр Петров
2026-09-15 23:04:04 +03:00
parent 5323b168a7
commit 0e74aaa7ee
116 changed files with 6908 additions and 333 deletions
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""Упаковщик FAT12-дискеты Sprinter. Образ принадлежит вызывающему проекту."""
import struct
import sys
import os
def create_floppy_image(output_file, files_to_add):
"""Создаёт FAT12 образ дискеты 1.44MB и добавляет файлы"""
# Размер дискеты 1.44MB
SECTOR_SIZE = 512
TOTAL_SECTORS = 2880
FAT_SIZE = 9
ROOT_DIR_ENTRIES = 224
# Проверяем весь набор до открытия выходного файла: ошибочная упаковка
# не должна уничтожать предыдущий пригодный носитель приложения.
capacity = TOTAL_SECTORS - 1 - 2 * FAT_SIZE - ROOT_DIR_ENTRIES * 32 // SECTOR_SIZE
needed = 0
names = set()
if len(files_to_add) > ROOT_DIR_ENTRIES:
raise ValueError("слишком много файлов для корневого каталога FAT12")
for filename, src_path in files_to_add:
name, ext = os.path.splitext(filename)
short_name = (name[:8].upper(), ext[1:4].upper())
# Исторические тесты SDK могут иметь длинное имя; на носителе оно
# сокращается до 8.3. Коллизия после сокращения — настоящая ошибка.
if not name or short_name in names:
raise ValueError(f"пустое или повторное имя FAT 8.3: {filename}")
short_name[0].encode('ascii')
short_name[1].encode('ascii')
names.add(short_name)
needed += max(1, (os.path.getsize(src_path) + SECTOR_SIZE - 1) // SECTOR_SIZE)
if needed > capacity:
raise ValueError(f"данные не помещаются на дискету: {needed} > {capacity} секторов")
os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
# Создаём пустой образ
with open(output_file, 'wb') as f:
f.write(b'\x00' * (TOTAL_SECTORS * SECTOR_SIZE))
# Читаем и модифицируем образ
with open(output_file, 'r+b') as f:
# BPB (BIOS Parameter Block) - начинаем с смещения 0
# Jump instruction
f.seek(0)
f.write(b'\xEB\x3C\x90')
# OEM name
f.write(b'MSDOS5.0')
# Bytes per sector
f.write(struct.pack('<H', SECTOR_SIZE))
# Sectors per cluster
f.write(struct.pack('B', 1))
# Reserved sectors
f.write(struct.pack('<H', 1))
# Number of FATs
f.write(struct.pack('B', 2))
# Root directory entries
f.write(struct.pack('<H', ROOT_DIR_ENTRIES))
# Total sectors (16-bit)
f.write(struct.pack('<H', 0))
# Media descriptor
f.write(struct.pack('B', 0xF0))
# Sectors per FAT
f.write(struct.pack('<H', FAT_SIZE))
# Sectors per track
f.write(struct.pack('<H', 18))
# Heads
f.write(struct.pack('<H', 2))
# Hidden sectors
f.write(struct.pack('<I', 0))
# Total sectors (32-bit)
f.write(struct.pack('<I', TOTAL_SECTORS))
# Extended BPB
# Drive number
f.write(struct.pack('B', 0))
# Reserved
f.write(struct.pack('B', 0))
# Extended boot signature
f.write(struct.pack('B', 0x29))
# Volume serial number
f.write(struct.pack('<I', 0x12345678))
# Volume label
f.write(b'NO NAME ')
# File system type
f.write(b'FAT12 ')
# Boot signature
f.seek(510)
f.write(struct.pack('<H', 0xAA55))
# FAT таблицы
fat_start = 1 * SECTOR_SIZE
fat1_start = fat_start
fat2_start = fat_start + FAT_SIZE * SECTOR_SIZE
# Инициализация FAT
# Первые две записи зарезервированы
fat = [0xFFF8, 0xFFFF]
# Добавляем файлы
root_dir_start = (1 + 2 * FAT_SIZE) * SECTOR_SIZE
data_start = root_dir_start + ROOT_DIR_ENTRIES * 32
cluster = 2 # Первый кластер данных
for filename, src_path in files_to_add:
# Читаем файл
with open(src_path, 'rb') as src:
file_data = src.read()
file_size = len(file_data)
print(f"Добавляем файл: {filename}, размер: {file_size} байт (0x{file_size:04X})")
# Вычисляем количество кластеров
clusters_needed = max(1, (file_size + SECTOR_SIZE - 1) // SECTOR_SIZE)
# Добавляем записи в FAT
first_cluster = cluster
for i in range(clusters_needed):
if i == clusters_needed - 1:
fat.append(0xFFFF) # Последний кластер
else:
fat.append(cluster + 1)
cluster += 1
# Записываем данные файла
file_offset = (first_cluster - 2) * SECTOR_SIZE
f.seek(data_start + file_offset)
f.write(file_data)
# Создаём запись в корневом каталоге
# Имя файла (8.3 формат)
name, ext = os.path.splitext(filename)
name = name.upper().ljust(8, ' ')[:8]
# ext comes from splitext as ".XYZ" (with leading dot) or "".
# Strip the dot first, then space-pad to 3 chars. The old form
# `ext.upper().ljust(3, ' ')[1:4]` lost padding for 2-char
# extensions like ".MD" → "MD" (2 bytes), corrupting the dir
# entry layout.
ext = ext[1:].upper().ljust(3, ' ')[:3] if ext else ' '
# Атрибуты
attr = 0x20 # Archive
print(f"Атрибуты: 0x{attr:02X}")
# Время и дата (упрощённо)
time_val = 0x0000
date_val = 0x0021
# Первый кластер (FAT12: младший байт по смещению 26, старший по 27)
first_cluster_lo = first_cluster & 0xFF
first_cluster_hi = (first_cluster >> 8) & 0xFF
# Размер файла (пробуем big-endian для Sprinter)
file_size_bytes = struct.pack('<I', file_size)
print(f"Размер файла в записи: {file_size_bytes.hex()}")
# Записываем в корневой каталог (32 байта)
dir_entry = bytearray(32)
dir_entry[0:8] = name.encode('ascii')
dir_entry[8:11] = ext.encode('ascii')
dir_entry[11] = attr
dir_entry[12:22] = b'\x00' * 10 # Reserved
dir_entry[22:24] = struct.pack('<H', time_val)
dir_entry[24:26] = struct.pack('<H', date_val)
dir_entry[26] = first_cluster_lo
dir_entry[27] = first_cluster_hi
dir_entry[28:32] = file_size_bytes
print(f"Атрибуты в записи: 0x{dir_entry[11]:02X}")
print(f"Запись каталога: {dir_entry.hex()}")
# Находим свободное место в корневом каталоге
f.seek(root_dir_start)
found = False
for i in range(ROOT_DIR_ENTRIES):
entry_pos = root_dir_start + i * 32
f.seek(entry_pos)
first_byte = f.read(1)
if first_byte == b'\x00' or first_byte == b'\xE5':
f.seek(entry_pos)
f.write(dir_entry)
found = True
print(f"Записан в корневой каталог, запись #{i}, позиция 0x{entry_pos:04X}")
break
if not found:
print(f"Ошибка: нет места в корневом каталоге для {filename}")
return False
# Записываем FAT
# FAT12 кодирование: 3 байта на 2 записи
fat_bytes = bytearray()
for i in range(0, len(fat), 2):
if i + 1 < len(fat):
val1 = fat[i] & 0xFFF
val2 = fat[i + 1] & 0xFFF
fat_bytes.extend([
val1 & 0xFF,
((val2 & 0xF) << 4) | ((val1 >> 8) & 0xF),
(val2 >> 4) & 0xFF
])
else:
val1 = fat[i] & 0xFFF
fat_bytes.extend([
val1 & 0xFF,
(val1 >> 8) & 0xF,
0x00
])
# Записываем FAT1
f.seek(fat1_start)
f.write(fat_bytes[:FAT_SIZE * SECTOR_SIZE])
# Записываем FAT2
f.seek(fat2_start)
f.write(fat_bytes[:FAT_SIZE * SECTOR_SIZE])
print(f"Создан образ: {output_file}")
print(f"Использовано кластеров: {cluster - 2}")
return True
if __name__ == '__main__':
if len(sys.argv) < 3:
print("Использование: python3 make_disk.py <output.img> <file1> [file2 ...]")
sys.exit(1)
output_file = sys.argv[1]
files = []
for i in range(2, len(sys.argv)):
files.append((os.path.basename(sys.argv[i]), sys.argv[i]))
try:
ok = create_floppy_image(output_file, files)
except (OSError, ValueError, UnicodeEncodeError) as error:
print(f"make_disk: {error}", file=sys.stderr)
sys.exit(1)
sys.exit(0 if ok else 1)
+14 -1
View File
@@ -7,6 +7,7 @@
# переносит всё дерево под DIR, например games/sprpop.
#
# Требует: mtools (mpartition/mformat/mcopy) + chdman (из MAME).
# CHDMAN_BIN может указывать на установленный chdman вне PATH.
# Sprinter DSS штатно монтирует стандартный MBR+FAT16 (подтверждено).
#
# Использование:
@@ -48,6 +49,18 @@ if [ $# -lt 2 ]; then
exit 1
fi
for command in mpartition mformat mcopy mmd mdir; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "make_hdd.sh: не найден host-инструмент $command (mtools)" >&2
exit 1
fi
done
CHDMAN_BIN=${CHDMAN_BIN:-chdman}
if ! command -v "$CHDMAN_BIN" >/dev/null 2>&1; then
echo "make_hdd.sh: не найден chdman: $CHDMAN_BIN" >&2
exit 1
fi
OUT_CHD="$1"; shift
# DSS использует обратный слэш, но mtools — прямой. Ведущие/замыкающие
@@ -145,6 +158,6 @@ mt mdir -/ z:
# RAW -> CHD (без сжатия = быстро; та же геометрия).
echo "--- конвертация RAW -> CHD ---"
chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \
"$CHDMAN_BIN" createhd -i "$RAW" -o "$OUT_CHD" -c none \
-chs $CYLS,$HEADS,$SECS -ss 512 -f
echo "готово: $OUT_CHD"
+17 -7
View File
@@ -14,10 +14,11 @@
# - libc/include/ : public headers
# - libc/{io,stdio,mem,gfx}/ : libc sources (assembled into sprinter.lib)
# - lib/Makefile + lib/sprinter.lib : prebuilt library + recipe to rebuild
# - examples/ : 27 ready-to-build programs
# - app.mk + disk/run tools : standalone application integration
# - libbgi/ + lib/bgi256*.lib : graphics library
# - third_party/sdcc/ : vendored SDCC 4.5
# - release_docs/ → docs/ : user-facing documentation
# - examples.mk-style root Makefile, README, RELEASE_NOTES, LICENSE
# - root Makefile, README, RELEASE_NOTES, LICENSE
#
# NOT included (developer-only):
# - docs/ (research notes, original PetersPlus reference material)
@@ -50,22 +51,30 @@ mkdir -p "$STAGE"
# --- WHITELIST: only these directories/files end up in the tarball ---
# Top-level metadata
cp README.md RELEASE_NOTES.md LICENSE Makefile "$STAGE/"
cp README.md RELEASE_NOTES.md LICENSE Makefile app.mk "$STAGE/"
# Toolchain
cp -R bin runtime libc examples "$STAGE/"
# toolchain/ — only mkexe (prebuilt + sources) and check_banks.py
cp -R bin runtime libc libbgi "$STAGE/"
# Core tools and source debugger backend needed by sprinter-cc/app.mk.
mkdir -p "$STAGE/toolchain/mkexe"
cp toolchain/mkexe/mkexe "$STAGE/toolchain/mkexe/"
cp toolchain/mkexe/mkexe.c "$STAGE/toolchain/mkexe/"
cp toolchain/mkexe/Makefile "$STAGE/toolchain/mkexe/"
[[ -f toolchain/mkexe/check.sh ]] && cp toolchain/mkexe/check.sh "$STAGE/toolchain/mkexe/"
cp toolchain/check_banks.py "$STAGE/toolchain/"
cp toolchain/check_bank_calls.py toolchain/check_w0_isr.py \
toolchain/sdbg_build.py toolchain/sdbg_driver.py toolchain/sdbg_dap.py \
toolchain/sdbg_launcher.py toolchain/mame_interactive.py \
toolchain/mame_profile.py toolchain/make_disk.py \
toolchain/make_hdd.sh toolchain/run_sprinter_mame.py "$STAGE/toolchain/"
cp -R toolchain/sdbg "$STAGE/toolchain/"
cp -R toolchain/mcp "$STAGE/toolchain/"
# Lib — keep Makefile and the prebuilt archive only; sources are in libc/
mkdir -p "$STAGE/lib"
cp lib/Makefile "$STAGE/lib/"
cp lib/sprinter.lib "$STAGE/lib/"
cp lib/sprinter_safe.lib lib/bgi256.lib lib/bgi256_safe.lib "$STAGE/lib/"
# Vendored SDCC — follow symlinks so the real content lands in the tarball
mkdir -p "$STAGE/third_party"
@@ -77,7 +86,7 @@ mkdir -p "$STAGE/third_party"
# NOT shipped.
[[ -d release_docs ]] && cp -R release_docs "$STAGE/docs"
# --- Strip build artefacts that crept in via examples/ ---
# --- Strip build artefacts that crept in via sources ---
echo ">> stripping build artefacts and macOS metadata"
find "$STAGE" \( \
-name '.sprinter-cc-*' -o \
@@ -105,4 +114,5 @@ echo "Try it:"
echo " tar xzf $(basename "$TARBALL")"
echo " cd $NAME"
echo " make all"
echo " bin/sprinter-cc -o hello.exe examples/hello/hello.c"
echo " bin/sprinter-cc -o hello.exe hello.c"
echo " Examples are released as a separate repository"
+75 -60
View File
@@ -20,7 +20,7 @@ mame_interactive.py — автотест .exe в MAME без участия че
автоповтор (typematic) и слипание scancode'ов.
- загрузка должна встать на приглашении C:\> (system.bat не должен
автозапускать Flex Navigator/приложение).
- перед запуском проверяем, что нет висящих копий MAME.
- тестовый носитель, конфигурация и состояние MAME изолированы от других сессий.
Использование:
python3 toolchain/mame_interactive.py [exe] [--data f ...] \
@@ -53,27 +53,16 @@ mame_interactive.py — автотест .exe в MAME без участия че
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import uuid
from mame_profile import MameProfile, add_arguments, from_arguments, write_keyboard_config
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAME_DIR = os.path.join(PROJECT_ROOT, "mame", "v306")
IMG_DIR = os.path.join(MAME_DIR, "IMG")
MC_IMG = os.path.join(IMG_DIR, "mc.img")
SNAP_DIR = os.path.join(MAME_DIR, "snap_auto")
LUA_SCRIPT = os.path.join(MAME_DIR, "_interactive_gen.lua")
COMMON_ARGS = [
"-skip_gameinfo", "-video", "opengl", "-window", "-nofilter",
"-beta:wd179x:0", "35hd", "-beta:wd179x:1", "35hd",
"-flop1", MC_IMG, "-flop2", os.path.join(IMG_DIR, "dss171u.img"),
"-isa0", "zxbus_adapter", "-isa0:zxbus_adapter:card", "neogs",
"-hard1", os.path.join(IMG_DIR, "sp_hdd_sys.chd"),
"-hard2", os.path.join(IMG_DIR, "sp_hdd_media.chd"),
"-ata2:0", "cdrom", "-cdrom", os.path.join(IMG_DIR, "SprinterCD.iso"),
"-bios", "v3.06",
]
# --- Раскладка AT-клавиатуры :kbd:ms_naturl (tag, mask) ---
# Снято дампом ioport-полей в MAME (см. docs/mame-autotest.md).
@@ -207,21 +196,7 @@ emu.register_periodic(function()
end
end)
"""
with open(LUA_SCRIPT, "w") as f:
f.write(lua)
def check_no_stray_mame():
# Ловим только запущенный ЭМУЛЯТОР (mame/mame.arm/mame.exe c машиной
# sprinter), а не всё, где в командной строке встречается «mame»:
# голый паттерн ложно срабатывал на параллельную сборку MAME из
# исходников (clang/make с путями .../src/mame/...).
out = subprocess.run(["pgrep", "-fl", r"mame[^ ]* sprinter( |$)"],
capture_output=True, text=True).stdout
if out.strip():
print("ОШИБКА: есть запущенные копии MAME — закройте их:")
print(out)
sys.exit(1)
return lua
def parse_step(s):
@@ -234,6 +209,7 @@ def parse_step(s):
def main():
ap = argparse.ArgumentParser()
add_arguments(ap)
ap.add_argument("exe", nargs="?", help=".exe на дискету A: (авто-запуск)")
ap.add_argument("--data", nargs="*", default=[], help="доп. файлы на дискету")
ap.add_argument("--launch-at", type=float, default=8.0,
@@ -242,9 +218,15 @@ def main():
help="'T:TEXT' — в момент T сек напечатать TEXT (можно много)")
ap.add_argument("--snap", default=None, help="секунды для скриншотов, через запятую")
ap.add_argument("--timeout", type=float, default=None, help="сек эмуляции до выхода")
ap.add_argument("--snapshot-dir", help="каталог кадров, по умолчанию build/mame-autotest")
args = ap.parse_args()
check_no_stray_mame()
profile = from_arguments(args)
if profile.binary is None and (Path(PROJECT_ROOT)/"mame/v306/mame.arm").is_file():
profile = MameProfile.resolve({"MAME_HOME":str(Path(PROJECT_ROOT)/"mame/v306")})
try:
profile.validate(dss=True)
except ValueError as error:
ap.error(str(error))
# Собираем шаги ввода: авто-запуск exe (печать пути) + пользовательские.
steps = list(args.step)
@@ -272,7 +254,7 @@ def main():
snap_times = [last_ev_t + pad, last_ev_t + pad + 2.0]
timeout = args.timeout if args.timeout is not None else (max(snap_times) + 1.5)
gen_lua(events, snap_times, timeout)
lua_source = gen_lua(events, snap_times, timeout)
# Дискета БЕЗ авто-запускаемого bat → приглашение C:\>.
disk_files = []
@@ -280,34 +262,67 @@ def main():
disk_files.append(exe_path)
disk_files += [os.path.abspath(p) for p in args.data]
if os.path.exists(MC_IMG):
shutil.copy(MC_IMG, MC_IMG + ".bak")
make_disk_cmd = [sys.executable, os.path.join(MAME_DIR, "make_disk.py"), MC_IMG]
subprocess.run(make_disk_cmd + disk_files, check=True, cwd=MAME_DIR)
snapshot_base = Path(args.snapshot_dir or Path.cwd()/"build/mame-autotest")
snapshot_root = snapshot_base.resolve()/uuid.uuid4().hex[:10]
snapshot_root.mkdir(parents=True)
with tempfile.TemporaryDirectory(prefix="sprinter-mame-autotest-") as directory:
state = Path(directory)
for name in ("cfg", "nvram", "diff"):
(state/name).mkdir()
write_keyboard_config(state/"cfg")
disk = state/"test.img"
subprocess.run([sys.executable, str(Path(PROJECT_ROOT)/"toolchain/make_disk.py"),
str(disk), *disk_files], check=True)
lua_script = state/"interactive.lua"
lua_script.write_text(lua_source, encoding="utf-8")
cmd = [str(profile.binary), "sprinter", "-noreadconfig",
"-rompath", str(profile.rompath), "-bios", profile.bios,
"-kbd", "ms_naturl,bios=sp2k", "-skip_gameinfo",
"-video", "soft", "-window", "-beta:wd179x:0", "35hd",
"-beta:wd179x:1", "35hd", "-flop1", str(disk),
"-flop2", str(profile.dss_image),
"-hard1", str(profile.system_hdd_image),
"-isa0", "zxbus_adapter", "-isa0:zxbus_adapter:card", "neogs",
"-snapshot_directory", str(snapshot_root),
"-autoboot_script", str(lua_script)]
for name in ("cfg", "nvram", "diff"):
cmd += ["-"+name+"_directory", str(state/name)]
if profile.home:
media = profile.home/"IMG/sp_hdd_media.chd"
optical = profile.home/"IMG/SprinterCD.iso"
if media.is_file():
test_media = state/"media.chd"
shutil.copyfile(media, test_media)
cmd += ["-hard2", str(test_media)]
if optical.is_file():
cmd += ["-ata2:0", "cdrom", "-cdrom", str(optical)]
print("Ввод по шагам:", [(round(t, 2), repr(txt)) for t, txt in steps])
print("Скриншоты (сек):", snap_times, " таймаут:", timeout)
try:
result = subprocess.run(cmd, cwd=state, capture_output=True,
text=True, timeout=timeout+8)
except subprocess.TimeoutExpired as error:
print("MAME: истёк лимит времени", file=sys.stderr)
output = error.stdout or ""
stderr = error.stderr or ""
if isinstance(output, bytes):
output = output.decode(errors="replace")
if isinstance(stderr, bytes):
stderr = stderr.decode(errors="replace")
result = subprocess.CompletedProcess(cmd, 124, output, stderr)
for line in result.stdout.splitlines():
if "[interactive]" in line:
print(line)
if result.returncode not in (0, 124):
print("STDERR:", result.stderr[-2000:])
snap_sub = os.path.join(SNAP_DIR, "sprinter")
if os.path.isdir(snap_sub):
shutil.rmtree(snap_sub)
cmd = ["timeout", str(int(timeout) + 8), "./mame.arm", "sprinter"] + COMMON_ARGS + [
"-snapshot_directory", SNAP_DIR,
"-autoboot_script", LUA_SCRIPT,
]
print("Ввод по шагам:", [(round(t, 2), repr(txt)) for t, txt in steps])
print("Скриншоты (сек):", snap_times, " таймаут:", timeout)
result = subprocess.run(cmd, cwd=MAME_DIR, capture_output=True, text=True)
for line in result.stdout.splitlines():
if "[interactive]" in line:
print(line)
if result.returncode not in (0, 124):
print("STDERR:", result.stderr[-2000:])
if os.path.isdir(snap_sub):
snap_sub = snapshot_root/"sprinter"
if snap_sub.is_dir():
print("Скриншоты:")
for s in sorted(os.listdir(snap_sub)):
print(" ", os.path.join(snap_sub, s))
for shot in sorted(snap_sub.iterdir()):
print(" ", shot)
else:
print("Скриншотов не создано.")
print("Скриншотов не создано. Каталог:", snapshot_root)
if __name__ == "__main__":
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Пути к установленному MAME; исходники fork приложению не нужны."""
from __future__ import annotations
from dataclasses import dataclass
import os
from pathlib import Path
@dataclass(frozen=True)
class MameProfile:
home: Path | None
binary: Path | None
rompath: Path | None
dss_image: Path | None
system_hdd_image: Path | None
bios: str
@classmethod
def resolve(cls, overrides: dict[str, str | None] | None = None) -> "MameProfile":
values = dict(os.environ)
values.update({key: value for key, value in (overrides or {}).items()
if value is not None})
home = _path(values.get("MAME_HOME"))
def selected(key: str, suffix: str) -> Path | None:
return _path(values.get(key)) or (home / suffix if home else None)
return cls(home=home,
binary=selected("MAME_BIN", "mame.arm"),
rompath=selected("MAME_ROMPATH", "roms"),
dss_image=selected("MAME_DSS_IMAGE", "IMG/dss171u.img"),
system_hdd_image=selected("MAME_SYSTEM_HDD_IMAGE", "IMG/sp_hdd_sys.chd"),
bios=values.get("MAME_BIOS") or "v3.06")
def validate(self, *, dss: bool = False, system_hdd: bool = True) -> None:
paths = {"MAME_BIN": self.binary, "MAME_ROMPATH": self.rompath}
if dss:
paths["MAME_DSS_IMAGE"] = self.dss_image
if system_hdd:
paths["MAME_SYSTEM_HDD_IMAGE"] = self.system_hdd_image
for variable, path in paths.items():
if path is None:
raise ValueError(f"задайте MAME_HOME или {variable}")
if not path.exists():
raise ValueError(f"{variable}: файл/каталог не найден: {path}")
if self.binary and not self.binary.is_file():
raise ValueError(f"MAME_BIN не является файлом: {self.binary}")
if self.rompath and not self.rompath.is_dir():
raise ValueError(f"MAME_ROMPATH не является каталогом: {self.rompath}")
def _path(value: str | None) -> Path | None:
if not value:
return None
return Path(os.path.expanduser(value)).resolve()
def add_arguments(parser) -> None:
"""Одинаковые аргументы для обычного запуска, автотеста и DAP."""
for option in ("mame-home", "mame-bin", "mame-rompath", "mame-dss-image",
"mame-system-hdd-image", "mame-bios"):
parser.add_argument("--" + option, default=None)
def from_arguments(args) -> MameProfile:
names = ("MAME_HOME", "MAME_BIN", "MAME_ROMPATH", "MAME_DSS_IMAGE",
"MAME_SYSTEM_HDD_IMAGE", "MAME_BIOS")
return MameProfile.resolve({name: getattr(args, name.lower(), None)
for name in names})
def write_keyboard_config(cfg_directory: Path) -> None:
"""Включить обе клавиатуры Sprinter, включая прямой ввод в DSS."""
cfg_directory.mkdir(parents=True, exist_ok=True)
(cfg_directory / "sprinter.cfg").write_text(
'<?xml version="1.0"?>\n'
'<mameconfig version="10">\n'
' <system name="sprinter">\n'
' <input>\n'
' <keyboard tag=":" enabled="1" />\n'
' <keyboard tag=":kbd:ms_naturl" enabled="1" />\n'
' </input>\n'
' </system>\n'
'</mameconfig>\n', encoding="utf-8")
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Обычный запуск Sprinter в MAME с локальным образом приложения."""
from __future__ import annotations
import argparse
from pathlib import Path
import subprocess
import sys
import tempfile
from mame_profile import add_arguments, from_arguments, write_keyboard_config
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
add_arguments(parser)
parser.add_argument("--floppy", help="локальный образ приложения для A:")
parser.add_argument("--hdd", help="локальный образ приложения для D:")
parser.add_argument("--video", default="soft")
parser.add_argument("--script", help="Lua-сценарий запуска/теста")
parser.add_argument("--snapshot-dir", help="постоянный каталог снимков")
parser.add_argument("--timeout", type=float, help="лимит работы MAME в секундах")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
try:
profile = from_arguments(args)
profile.validate(dss=True)
if not args.floppy and not args.hdd:
raise ValueError("для запуска укажите --floppy или --hdd")
media = {name: Path(value).resolve() for name, value in
(("floppy", args.floppy), ("hdd", args.hdd)) if value}
for name, path in media.items():
if not path.is_file():
raise ValueError(f"локальный образ {name} не найден: {path}")
script = Path(args.script).resolve() if args.script else None
if script and not script.is_file():
raise ValueError(f"Lua-сценарий не найден: {script}")
except ValueError as error:
print(f"run_sprinter_mame: {error}", file=sys.stderr)
return 2
with tempfile.TemporaryDirectory(prefix="sprinter-mame-run-") as directory:
state = Path(directory)
for name in ("cfg", "nvram", "diff", "snapshot"):
(state / name).mkdir()
snapshots = Path(args.snapshot_dir).resolve() if args.snapshot_dir else state / "snapshot"
snapshots.mkdir(parents=True, exist_ok=True)
write_keyboard_config(state / "cfg")
command = [str(profile.binary), "sprinter", "-noreadconfig",
"-rompath", str(profile.rompath), "-bios", profile.bios,
"-kbd", "ms_naturl,bios=sp2k", "-video", args.video,
"-window", "-skip_gameinfo", "-beta:wd179x:0", "35hd",
"-flop2", str(profile.dss_image),
"-hard1", str(profile.system_hdd_image)]
if "floppy" in media:
command += ["-flop1", str(media["floppy"])]
if "hdd" in media:
command += ["-hard2", str(media["hdd"])]
if script:
command += ["-autoboot_script", str(script)]
for name in ("cfg", "nvram", "diff"):
command += ["-" + name + "_directory", str(state / name)]
command += ["-snapshot_directory", str(snapshots)]
print("MAME:", profile.binary, "ROM:", profile.rompath,
"DSS:", profile.dss_image, "BIOS:", profile.bios, flush=True)
if args.dry_run:
print("Команда:", command)
return 0
try:
return subprocess.run(command, cwd=state, timeout=args.timeout).returncode
except subprocess.TimeoutExpired:
print("run_sprinter_mame: истёк лимит времени", file=sys.stderr)
return 124
if __name__ == "__main__":
raise SystemExit(main())
+11
View File
@@ -95,6 +95,17 @@ class Protocol:
'--build', build, '--socket', socket_path]
if arguments.get('mame'):
command.extend(['--mame', arguments['mame']])
profile_arguments = {
'mameHome': '--mame-home',
'mameBin': '--mame-bin',
'mameRompath': '--mame-rompath',
'mameDssImage': '--mame-dss-image',
'mameSystemHddImage': '--mame-system-hdd-image',
'mameBios': '--mame-bios',
}
for key, option in profile_arguments.items():
if arguments.get(key):
command.extend([option, arguments[key]])
if arguments.get('debugger'):
command.extend(['--debugger', arguments['debugger']])
if arguments.get('launchAt') is not None:
+29 -27
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
import argparse
from contextlib import redirect_stdout
import importlib.util
import json
import os
from pathlib import Path
@@ -19,6 +18,9 @@ import uuid
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT/'toolchain'))
from mame_interactive import basename_83, build_events
from make_disk import create_floppy_image
from mame_profile import (MameProfile, add_arguments, from_arguments,
write_keyboard_config)
from sdbg.image import read_ihx
from sdbg.model import DebugMap
@@ -38,20 +40,6 @@ def disk_entries(exe: Path, data) -> list[tuple[str, str]]:
return entries
def write_keyboard_config(cfg_directory: Path) -> None:
"""Включить обе клавиатуры Sprinter для физического ввода в DSS."""
(cfg_directory/'sprinter.cfg').write_text(
'<?xml version="1.0"?>\n'
'<mameconfig version="10">\n'
' <system name="sprinter">\n'
' <input>\n'
' <keyboard tag=":" enabled="1" />\n'
' <keyboard tag=":kbd:ms_naturl" enabled="1" />\n'
' </input>\n'
' </system>\n'
'</mameconfig>\n', encoding='utf-8')
def lua_script(path, ready, main_address, signature, events, launch_at,
dss_timeout=30):
rows = ',\n'.join('{'+f'{t},"{tag}",{mask},{value}'+'}'
@@ -165,6 +153,7 @@ end)
def main():
parser=argparse.ArgumentParser(description=__doc__)
add_arguments(parser)
parser.add_argument('--build',required=True)
parser.add_argument('--socket',required=True)
parser.add_argument('--data',action='append',default=[])
@@ -172,7 +161,8 @@ def main():
help='не начинать ввод раньше этой секунды эмуляции')
parser.add_argument('--dss-timeout',type=float,default=30,
help='таймаут появления стабильного prompt DSS')
parser.add_argument('--mame',default=str(ROOT/'mame/v306/mame.arm'))
parser.add_argument('--mame',default=None,
help='совместимый alias для --mame-bin')
parser.add_argument('--debugger',default='sdbg',
help='OSD debugger provider (по умолчанию sdbg из project patch)')
args=parser.parse_args()
@@ -188,8 +178,13 @@ def main():
function['start'],min(function['end'],function['start']+24)))
if not signature:
raise ValueError('У main нет проверяемой сигнатуры')
mame=Path(args.mame).resolve()
mame_dir=mame.parent
if args.mame and not args.mame_bin:
args.mame_bin=args.mame
profile=from_arguments(args)
if profile.binary is None and (ROOT/'mame/v306/mame.arm').is_file():
profile=MameProfile.resolve({'MAME_HOME':str(ROOT/'mame/v306')})
profile.validate(dss=True)
mame=profile.binary
stopping=False
def stop(*_):
nonlocal stopping
@@ -202,13 +197,11 @@ def main():
for name in ('nvram','cfg','diff','snapshot'): (state/name).mkdir()
write_keyboard_config(state/'cfg')
system=state/'system.chd'
shutil.copyfile(mame_dir/'IMG/sp_hdd_sys.chd',system)
shutil.copyfile(profile.system_hdd_image,system)
disk_path=state/'debug.img'
spec=importlib.util.spec_from_file_location('make_disk',mame_dir/'make_disk.py')
disk=importlib.util.module_from_spec(spec);spec.loader.exec_module(disk)
files=disk_entries(exe,args.data)
with redirect_stdout(sys.stderr):
if not disk.create_floppy_image(str(disk_path),files):
if not create_floppy_image(str(disk_path),files):
raise RuntimeError('Не удалось создать debug-дискету')
command_text='a:\\'+basename_83(exe.name).replace(' ','')+'\n'
events=build_events([(0,command_text)])
@@ -218,11 +211,20 @@ def main():
args.dss_timeout)
session_id=uuid.uuid4().hex
environment=dict(os.environ,SDBG_IPC_DIR=str(ipc),SDBG_SESSION_ID=session_id)
command=[str(mame),'sprinter','-noreadconfig','-rompath',str(mame_dir/'roms'),
'-bios','v3.06','-kbd','ms_naturl,bios=sp2k','-video','soft','-window',
plugin_paths=[str(ROOT/'toolchain/mcp')]
installed_plugins=profile.home.parent/'plugins' if profile.home else None
if installed_plugins and installed_plugins.is_dir():
plugin_paths.append(str(installed_plugins))
legacy_plugins=ROOT/'mame/sources/MAME/plugins'
if legacy_plugins.is_dir():
plugin_paths.append(str(legacy_plugins))
command=[str(mame),'sprinter','-noreadconfig','-rompath',str(profile.rompath),
'-bios',profile.bios,'-kbd','ms_naturl,bios=sp2k','-video','soft','-window',
'-sound','none','-skip_gameinfo','-beta:wd179x:0','35hd',
'-flop1',str(disk_path),'-hard1',str(system),'-debug','-debugger',args.debugger,
'-plugin','sdbgbridge','-pluginspath',str(ROOT/'toolchain/mcp')+';'+str(ROOT/'mame/sources/MAME/plugins'),
'-beta:wd179x:1','35hd','-flop1',str(disk_path),
'-flop2',str(profile.dss_image),'-hard1',str(system),
'-debug','-debugger',args.debugger,
'-plugin','sdbgbridge','-pluginspath',';'.join(plugin_paths),
'-autoboot_delay','0','-autoboot_script',str(lua)]
for name in ('nvram','cfg','diff','snapshot'):
command.extend(['-'+name+'_directory',str(state/name)])
@@ -250,7 +252,7 @@ def main():
'--socket',args.socket],stdout=subprocess.PIPE,stderr=sys.stderr,text=True)
line=server.stdout.readline()
if not line:
raise RuntimeError('Session server не запустился')
raise RuntimeError('Session server не запустился'+mame_diagnostics())
server_ready=json.loads(line)
print(json.dumps({'ready':True,'socket':args.socket,'pid':mame_process.pid,
'entry':entry,'build_id':server_ready['build_id']},
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sprinter C Compiler contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -1
View File
@@ -30,7 +30,7 @@ function projectForLaunch(configuration, workspace) {
function isSprinterMakefile(filename) {
try {
return /include\s+\$\(PROJ_ROOT\)\/app\.mk/.test(
return /include\s+(?:\$\((?:PROJ_ROOT|SPRINTER_ROOT)\)\/app\.mk|\$\(CURDIR\)\/(?:\.\.\/){0,2}sdk\.mk)/.test(
fs.readFileSync(filename, 'utf8'));
} catch (_) {
return false;
+11 -7
View File
@@ -1,6 +1,6 @@
const path = require('path');
const vscode = require('vscode');
const {resolvePython, pyenvEnvironment} = require('./runtime');
const {resolvePython, pyenvEnvironment, resolveSdkRoot} = require('./runtime');
const {projectForLaunch, isSprinterMakefile, taskLabel, makeCommand} =
require('./build');
@@ -13,10 +13,9 @@ class SprinterAdapterFactory {
const settings = vscode.workspace.getConfiguration('sprinterDebugger');
const folder = session.workspaceFolder || vscode.workspace.workspaceFolders?.[0];
const configured = session.configuration.adapterPath;
if (!configured && !folder) {
throw new Error('Откройте workspace C-Compiler или задайте adapterPath');
}
const adapter = configured || path.join(folder.uri.fsPath, 'toolchain', 'sdbg_dap.py');
const sdk = configured ? null : resolveSdkRoot(
settings.get('sdkRoot'), folder?.uri.fsPath);
const adapter = configured || path.join(sdk, 'toolchain', 'sdbg_dap.py');
const runtime = resolvePython({
command: settings.get('pythonCommand', 'auto'),
args: settings.get('pythonArguments', []),
@@ -45,8 +44,10 @@ class SprinterTaskProvider {
const definition = definitionOverride ||
{type: 'sprinter', project: relative || '.'};
const options = {cwd: project};
const env = pyenvEnvironment(runtime.command, project) ||
pyenvEnvironment(runtime.command, folder.uri.fsPath);
const sdk = resolveSdkRoot(settings.get('sdkRoot'), folder.uri.fsPath);
const env = {SPRINTER_ROOT: sdk, ...(
pyenvEnvironment(runtime.command, project) ||
pyenvEnvironment(runtime.command, folder.uri.fsPath) || {})};
if (env) options.env = env;
const task = new vscode.Task(
definition, folder, taskLabel(project, folder.uri.fsPath), 'sprinter',
@@ -134,6 +135,9 @@ class SprinterConfigurationProvider {
const workspace = folder?.uri.fsPath ||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspace) return configuration;
const mameHome = vscode.workspace.getConfiguration('sprinterDebugger')
.get('mameHome');
if (!configuration.mameHome && mameHome) configuration.mameHome = mameHome;
try {
const project = projectForLaunch(configuration, workspace);
if (project && isSprinterMakefile(path.join(project, 'Makefile'))) {
+37 -2
View File
@@ -52,6 +52,16 @@
"items": {"type": "string"},
"default": [],
"description": "Аргументы пользовательской Python-команды перед путём DAP-адаптера"
},
"sprinterDebugger.sdkRoot": {
"type": "string",
"default": "",
"description": "Путь к Sprinter-CC для внешнего проекта; можно задать SPRINTER_ROOT"
},
"sprinterDebugger.mameHome": {
"type": "string",
"default": "",
"description": "Подготовленная среда MAME для launch, если не указана в launch.json"
}
}
},
@@ -84,7 +94,32 @@
},
"mame": {
"type": "string",
"description": "Путь к mame.arm"
"description": "Совместимый alias пути к mame.arm"
},
"mameHome": {
"type": "string",
"description": "Среда MAME: стандартные пути к бинарнику, ROM и DSS"
},
"mameBin": {
"type": "string",
"description": "Переопределить бинарник MAME (stock или sdbg)"
},
"mameRompath": {
"type": "string",
"description": "Переопределить каталог ROM/BIOS MAME"
},
"mameDssImage": {
"type": "string",
"description": "Переопределить образ DSS-дискеты"
},
"mameSystemHddImage": {
"type": "string",
"description": "Переопределить системный HDD-образ Sprinter"
},
"mameBios": {
"type": "string",
"default": "v3.06",
"description": "Имя варианта BIOS для -bios"
},
"debugger": {
"type": "string",
@@ -166,5 +201,5 @@
"check": "node --check extension.js && node --check build.js && node --test runtime.test.js build.test.js extension.test.js"
},
"files": ["extension.js", "runtime.js", "build.js", "README.md"],
"license": "BSD-3-Clause"
"license": "MIT"
}
+14 -1
View File
@@ -80,5 +80,18 @@ function pyenvEnvironment(command, workspace, home = os.homedir()) {
return version ? {PYENV_VERSION: version} : undefined;
}
function resolveSdkRoot(configured, workspace, environment = process.env) {
const candidates = [configured, environment.SPRINTER_ROOT, workspace]
.filter(Boolean).map(value => path.resolve(value));
const found = candidates.find(root =>
fs.existsSync(path.join(root, 'toolchain', 'sdbg_dap.py')) &&
fs.existsSync(path.join(root, 'bin', 'sprinter-cc')));
if (!found) {
throw new Error('Не найден Sprinter-CC: задайте sprinterDebugger.sdkRoot ' +
'или SPRINTER_ROOT');
}
return found;
}
module.exports = {resolvePython, localPythonVersion, installedPyenvPythons,
pyenvEnvironment};
pyenvEnvironment, resolveSdkRoot};