Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lindbergh: squashfs support #13714

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion batocera-Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
- Steering wheel support added for :
- Logitech: G923 (Xbox), PRO Racing Wheel
- Speedlink: 4in1 Leather Power Feedback Wheel
- Sega Lindbergh loader
- Sega Lindbergh loader with .squashfs support
- Variable Refresh Rate (VRR) support for modern AMD gpus
- Support of Shanwan Twin USB Joystick (new revision)
- Libretro-PS2 core
Expand Down
3 changes: 2 additions & 1 deletion package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
def main(args: argparse.Namespace, maxnbplayers: int) -> int:
# squashfs roms if squashed
if args.rom.suffix == ".squashfs":
with squashfs_rom(args.rom) as rom:
writable_rom = (args.system == "lindbergh")
with squashfs_rom(args.rom, overlay=writable_rom) as rom:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may need to be refactored as part of Generator (and a refactor of when the generator is created) because it's rare for decisions to be made on the system/emulator/core outside of Generator and its subclasses.

Before talking about this, we need to figure out if an overlay is even the right option.

return start_rom(args, maxnbplayers, rom, args.rom)
else:
return start_rom(args, maxnbplayers, args.rom, args.rom)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ def getHotkeysContext(self) -> HotkeysContext:
}

def generate(self, system, rom, playersControllers, metadata, guns, wheels, gameResolution):
# .squashfs returns mount point so rewire rom to first .game file found
# https://stackoverflow.com/a/43669828/9983389
if (rom.is_dir()):
rom = next(rom.rglob("*.game"))

romDir = rom.parent
romName = rom.name
_logger.debug("ROM path: %s", romDir)
Expand Down
96 changes: 69 additions & 27 deletions package/batocera/core/batocera-configgen/configgen/configgen/utils/squashfs.py
100644 → 100755
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use overlay?
using the /var/run/squashfs/ as rw is not an ideal location for preservation of any rom updates. why here?

Copy link
Contributor Author

@udance4ever udance4ever Mar 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to other ideas & saw this was the most straightforward to implement and get working.

The bottleneck is lindbergh doesn't run outside (eg. /usr/bin/lindbergh and it's shared .so libraries) and the config file in system/configs/lindbergh is only a template so it requires the rom directory to be writeable so the generator can copy all these files into it at runtime.

The solution squashfs already provides is the use of an overlay - this is what it was designed for as far as I can tell.

The changes are only temporary to allow the rom to boot. All changes can be safely discarded after the game exits because the config file is generated every time.

I also noticed the code can be reused as other squashfs mounts like daphne and singe want to write a .dat file into the directory if you don't remember to generate it before mksquashfs. Use of the overlay logic allows the title to boot instead of crapping out because it doesn't have write access as the .dat file can be regenerated (until the rom is upgraded to include it). This is a separate discussion but thought I'd add additional rationale.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import logging
import os
import shutil
import subprocess
from contextlib import contextmanager
from pathlib import Path
Expand All @@ -16,28 +18,63 @@

_SQUASHFS_DIR: Final = Path("/var/run/squashfs/")

def _squashfs_removemount(mount_point: Path):
if mount_point.exists():
if mount_point.is_mount():
return_code = subprocess.call(["umount", mount_point])
if return_code != 0:
_logger.debug("squashfs_rom: unmounting %s failed", mount_point)
raise BatoceraException(f"Unable to unmount the file {mount_point}")
mount_point.rmdir()

def _squashfs_rom_cleanup(mount_point: Path, overlay: bool):
_logger.debug("squashfs_rom: cleaning up %s", mount_point)
os.chdir(_SQUASHFS_DIR)
_squashfs_removemount(mount_point)

if overlay:
overlay_root = mount_point.parent
mount_point_rw = overlay_root / "overlay"

if overlay_root.exists():
_logger.debug("squashfs_rom: cleaning up overlay root %s", overlay_root)
_squashfs_removemount(mount_point_rw)
shutil.rmtree(overlay_root)

@contextmanager
def squashfs_rom(rom: Path, /) -> Iterator[Path]:
def squashfs_rom(rom: Path, overlay: bool) -> Iterator[Path]:
_logger.debug("squashfs_rom(%s)", rom)
mount_point = _SQUASHFS_DIR / rom.stem

mkdir_if_not_exists(_SQUASHFS_DIR)

# first, try to clean an empty remaining directory (for example because of a crash)
if mount_point.exists() and mount_point.is_dir():
_logger.debug("squashfs_rom: %s already exists", mount_point)
# try to remove an empty directory, else, run the directory, ignoring the .squashfs
try:
mount_point.rmdir()
except (FileNotFoundError, OSError):
_logger.debug("squashfs_rom: failed to rmdir %s", mount_point)
yield mount_point
# No cleanup is necessary
return
if overlay:
overlay_root = _SQUASHFS_DIR / rom.stem
mount_point = overlay_root / "rom"
overlay_delta = overlay_root / "delta"
overlay_tmp = overlay_root / "tmp"
mount_point_rw = overlay_root / "overlay"

if overlay_root.exists() and overlay_root.is_dir():
_logger.debug("squashfs_rom: %s overlay root already exists", overlay_root)
_squashfs_rom_cleanup(mount_point, overlay)
else:
mount_point = _SQUASHFS_DIR / rom.stem

# first, try to clean an empty remaining directories (for example because of a crash)
if mount_point.exists() and mount_point.is_dir():
_logger.debug("squashfs_rom: %s already exists", mount_point)

# try to remove an empty directory, else, run the directory, ignoring the .squashfs
try:
mount_point.rmdir()
except (FileNotFoundError, OSError):
_logger.debug("squashfs_rom: failed to rmdir %s", mount_point)
yield mount_point
# No cleanup is necessary
return

# ok, the base directory doesn't exist, let's create it and mount the squashfs on it
mount_point.mkdir()
mount_point.mkdir(parents=True)

return_code = subprocess.call(["mount", rom, mount_point])
if return_code != 0:
Expand All @@ -48,28 +85,33 @@ def squashfs_rom(rom: Path, /) -> Iterator[Path]:
pass
raise BatoceraException(f"Unable to mount the file {rom}")

if overlay:
for dir in [overlay_delta, overlay_tmp, mount_point_rw]: dir.mkdir(exist_ok=True)
return_code = subprocess.call(["mount",
"-t", "overlay",
"overlay",
"-o", f"lowerdir={mount_point},upperdir={overlay_delta},workdir={overlay_tmp}",
mount_point_rw])
if return_code != 0:
_logger.debug("squashfs_rom: mounting overlay %s failed", mount_point_rw)
_squashfs_rom_cleanup(mount_point, overlay)
raise BatoceraException(f"Unable to mount the file {rom}")

try:
working_mount_point = mount_point_rw if overlay else mount_point

# if the squashfs contains a single file with the same name, take it as the rom file
rom_single = mount_point / rom.stem
rom_single = working_mount_point / rom.stem
if len(list(mount_point.iterdir())) == 1 and rom_single.exists():
_logger.debug("squashfs: single rom %s", rom_single)
yield rom_single
else:
try:
rom_linked = (mount_point / ".ROM").resolve(strict=True)
rom_linked = (working_mount_point / ".ROM").resolve(strict=True)
except OSError:
yield mount_point
yield working_mount_point
else:
_logger.debug("squashfs: linked rom %s", rom_linked)
yield rom_linked
finally:
_logger.debug("squashfs_rom: cleaning up %s", mount_point)

# unmount
return_code = subprocess.call(["umount", mount_point])
if return_code != 0:
_logger.debug("squashfs_rom: unmounting %s failed", mount_point)
raise BatoceraException(f"Unable to unmount the file {mount_point}")

# cleaning the empty directory
mount_point.rmdir()
_squashfs_rom_cleanup(mount_point, overlay)
Original file line number Diff line number Diff line change
Expand Up @@ -5651,7 +5651,7 @@ lindbergh:
manufacturer: Sega
release: 2005
hardware: arcade
extensions: [game]
extensions: [game, squashfs]
platform: lindbergh, arcade
emulators:
lindbergh-loader:
Expand Down