diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6d79636f..a8848514 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,7 @@ concurrency:
jobs:
test:
name: Test
- runs-on: macos-15
+ runs-on: macos-26
timeout-minutes: 30
steps:
diff --git a/Makefile b/Makefile
index 6681a92a..c7430537 100644
--- a/Makefile
+++ b/Makefile
@@ -47,7 +47,7 @@ help:
doctor:
@[[ "$$(uname -s)" == Darwin ]] || { echo 'error: macOS is required' >&2; exit 1; }
@[[ "$$(uname -m)" == arm64 ]] || { echo 'error: an Apple Silicon Mac is required' >&2; exit 1; }
- @major=$$(sw_vers -productVersion | cut -d. -f1); (( major >= 15 )) || { echo 'error: macOS 15 or newer is required' >&2; exit 1; }
+ @major=$$(sw_vers -productVersion | cut -d. -f1); (( major >= 26 )) || { echo 'error: macOS 26 or newer is required' >&2; exit 1; }
@for tool in curl docker pkg-config python3 swift xcrun; do command -v "$$tool" >/dev/null || { echo "error: $$tool is required" >&2; exit 1; }; done
@docker info >/dev/null 2>&1 || { echo 'error: Docker is installed but not running' >&2; exit 1; }
@printf 'Toolchain ready: %s (%s)\n' "$$(sw_vers -productVersion)" "$$(uname -m)"
diff --git a/README.md b/README.md
index 50b99c54..f3fb6a31 100644
--- a/README.md
+++ b/README.md
@@ -325,7 +325,7 @@ safety floor.
## Requirements
- Apple Silicon Mac (`arm64`)
-- macOS 15 or newer
+- macOS 26 or newer
- At least 8 GB free initially
On M3 and newer Apple Silicon, Try Omarchy also exposes ARM EL2 to Linux, so
@@ -458,7 +458,7 @@ brew install pkg-config
```
`make doctor` performs the basic preflight. `make runtime` downloads a
-checksum-pinned `arm64_sequoia` dependency set, builds QEMU for macOS 15.0,
+checksum-pinned dependency set, builds QEMU for macOS 26.0,
and rejects any runtime image that raises that minimum or strongly imports an
API unavailable on the declared platform. Installed Homebrew library versions
are never copied into the app.
diff --git a/docs/alacritty-migration.md b/docs/alacritty-migration.md
new file mode 100644
index 00000000..b74763ec
--- /dev/null
+++ b/docs/alacritty-migration.md
@@ -0,0 +1,66 @@
+# Enable accelerated Alacritty in an existing guest
+
+New factory images use the packaged Alacritty binary directly. An existing VM
+may still contain the factory `/usr/local/bin/alacritty` wrapper that forces
+software rendering. Updating the Mac app does not replace files in an existing
+VM, so retiring that wrapper is a one-time, opt-in step. No factory reset or
+package removal is needed.
+
+The migration helper only runs when the current boot advertises
+`omarchy.virgl_dual_source=1`, which the launcher supplies with the fixed VirGL
+runtime. This marker identifies that specific fix; it is not a claim that every
+OpenGL application is compatible. Kitty's separate workaround is unaffected.
+
+## Copy the helper from the installed app
+
+On the Mac, choose the actual location of the updated application, then copy its
+bundled helper to a folder you share with the VM. For example, with Downloads
+selected as the VM's shared folder:
+
+```sh
+APP="/Applications/Try Omarchy.app"
+cp -n "$APP/Contents/Resources/scripts/try-omarchy-migrate-alacritty" "$HOME/Downloads/"
+```
+
+Restart the VM using the updated app so it receives the runtime marker. Inside
+Omarchy, run the copied helper from the shared folder:
+
+```sh
+sudo /usr/bin/python3 -I /mnt/mac/try-omarchy-migrate-alacritty
+```
+
+Alternatively, copy the same bundled file into the guest using an existing SSH
+connection and run it with `sudo /usr/bin/python3 -I /path/to/try-omarchy-migrate-alacritty`.
+SSH access and sharing are not enabled automatically by this migration.
+
+## What the helper changes
+
+The helper recognizes the exact factory wrapper by its SHA-256 digest
+`9f2da34ccfbbf5402233c1e19ca09197c03c8a7fda3369e4adaaff2d5df7c67e`.
+It preserves the original file and its metadata as
+`/usr/local/bin/.alacritty.try-omarchy-software-backup`, then removes only the
+`/usr/local/bin/alacritty` PATH entry. An existing backup is never overwritten.
+
+Custom contents, symbolic links, hard-linked files, and unprotected files or
+directories are left unchanged. A repeated successful run is a no-op. A
+“Preserved” result means no wrapper was removed; inspect the reported condition
+before making any manual change.
+
+Close and reopen Alacritty afterward. Existing terminal processes keep the
+environment they started with. This helper does not change terminal selection,
+Alacritty configuration, or the packaged `/usr/bin/alacritty` executable.
+
+The new factory image also includes a marker-gated service that invokes the
+helper before the graphical login manager. That service is **not automatically
+installed into older guests**; the one-time copied helper is the existing-guest
+migration path.
+
+If reverting to an older host runtime, the retained wrapper can be restored
+without overwriting a newly created custom wrapper:
+
+```sh
+sudo mv -n -- /usr/local/bin/.alacritty.try-omarchy-software-backup /usr/local/bin/alacritty
+```
+
+The factory service will retire an exact restored wrapper again on a subsequent
+boot with the fixed-runtime marker.
diff --git a/docs/releasing.md b/docs/releasing.md
index c078c90e..73f97251 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -1,6 +1,6 @@
# Releasing
-Releases are Apple Silicon-only and require macOS 15 or newer.
+Releases are Apple Silicon-only and require macOS 26 or newer.
## Build and verify
diff --git a/guest/native-overlay/usr/lib/systemd/system/try-omarchy-migrate-alacritty.service b/guest/native-overlay/usr/lib/systemd/system/try-omarchy-migrate-alacritty.service
new file mode 100644
index 00000000..873f2c59
--- /dev/null
+++ b/guest/native-overlay/usr/lib/systemd/system/try-omarchy-migrate-alacritty.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Retire the factory Alacritty software-rendering workaround
+ConditionKernelCommandLine=omarchy.virgl_dual_source=1
+After=local-fs.target
+Before=sddm.service display-manager.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/sbin/try-omarchy-migrate-alacritty
+
+[Install]
+WantedBy=multi-user.target
diff --git a/guest/native-overlay/usr/local/sbin/try-omarchy-migrate-alacritty b/guest/native-overlay/usr/local/sbin/try-omarchy-migrate-alacritty
new file mode 100755
index 00000000..47e320ea
--- /dev/null
+++ b/guest/native-overlay/usr/local/sbin/try-omarchy-migrate-alacritty
@@ -0,0 +1,93 @@
+#!/usr/bin/python3 -I
+"""Retire the known factory workaround after the host enables fixed VirGL."""
+
+import errno
+import hashlib
+import os
+from pathlib import Path
+import stat
+import sys
+
+MARKER = "omarchy.virgl_dual_source=1"
+WRAPPER_SHA256 = "9f2da34ccfbbf5402233c1e19ca09197c03c8a7fda3369e4adaaff2d5df7c67e"
+WRAPPER_SIZE = 688
+BACKUP = ".alacritty.try-omarchy-software-backup"
+
+
+def open_directory(path):
+ descriptor = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
+ try:
+ for part in Path(path).parts[1:]:
+ child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
+ dir_fd=descriptor)
+ os.close(descriptor)
+ descriptor = child
+ return descriptor
+ except OSError:
+ os.close(descriptor)
+ raise
+
+
+def migrate(cmdline=Path("/proc/cmdline"), directory=Path("/usr/local/bin"), trusted_uid=0):
+ if MARKER not in cmdline.read_text().split():
+ return "Skipped: this boot does not advertise the fixed VirGL runtime."
+
+ parent = open_directory(directory)
+ try:
+ info = os.fstat(parent)
+ if info.st_uid != trusted_uid or info.st_mode & 0o022:
+ return "Preserved: the wrapper directory is not protected."
+ try:
+ source = os.open("alacritty", os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK,
+ dir_fd=parent)
+ except FileNotFoundError:
+ return "No migration needed: no Alacritty wrapper is installed."
+ except OSError as error:
+ if error.errno == errno.ELOOP:
+ return "Preserved: the Alacritty wrapper is a symbolic link."
+ raise
+ try:
+ original = os.fstat(source)
+ if (not stat.S_ISREG(original.st_mode) or original.st_uid != trusted_uid
+ or original.st_mode & 0o022 or original.st_nlink != 1):
+ return "Preserved: the Alacritty wrapper is not an unmodified factory file."
+ content = os.read(source, WRAPPER_SIZE + 1)
+ if hashlib.sha256(content).hexdigest() != WRAPPER_SHA256:
+ return "Preserved: the Alacritty wrapper has custom contents."
+ try:
+ # An exclusive hard link preserves the original bytes and metadata
+ # before removing the PATH entry, without overwriting any backup.
+ os.link("alacritty", BACKUP, src_dir_fd=parent, dst_dir_fd=parent,
+ follow_symlinks=False)
+ except FileExistsError:
+ return "Preserved: an Alacritty migration backup already exists."
+ for name in ("alacritty", BACKUP):
+ current = os.stat(name, dir_fd=parent, follow_symlinks=False)
+ if (current.st_dev, current.st_ino) != (original.st_dev, original.st_ino):
+ raise RuntimeError("Wrapper changed during migration; no PATH entry was removed.")
+ os.unlink("alacritty", dir_fd=parent)
+ os.fsync(parent)
+ return "Migrated: the factory Alacritty wrapper was backed up; accelerated rendering is enabled."
+ finally:
+ os.close(source)
+ finally:
+ os.close(parent)
+
+
+def main():
+ if len(sys.argv) != 1:
+ print("Usage: sudo try-omarchy-migrate-alacritty", file=sys.stderr)
+ return 2
+ if os.geteuid() != 0:
+ print("Run this migration with sudo.", file=sys.stderr)
+ return 1
+ try:
+ print(migrate())
+ except (OSError, RuntimeError) as error:
+ print(f"Alacritty migration failed: {error}", file=sys.stderr)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/guest/packages.lock.json b/guest/packages.lock.json
index 0f1eec7a..3479e285 100644
--- a/guest/packages.lock.json
+++ b/guest/packages.lock.json
@@ -398,8 +398,8 @@
"libzip": "1.11.4-1",
"licenses": "20240728-1",
"lilv": "0.28.0-1",
- "linux-aarch64": "7.2.3-2",
- "linux-aarch64-headers": "7.2.3-2",
+ "linux-aarch64": "7.2.4-1",
+ "linux-aarch64-headers": "7.2.4-1",
"linux-api-headers": "7.2-1",
"llhttp": "9.3.1-1",
"llvm-libs": "22.1.8-2",
@@ -453,7 +453,7 @@
"openssh": "10.5p1-1",
"openssl": "3.6.4-1",
"opus": "1.6.1-1",
- "orc": "0.4.43-1",
+ "orc": "0.4.44-1",
"osinfo-db": "20260812-1",
"p11-kit": "0.26.5-1",
"pacman": "7.1.0.r9.g54d9411-2",
diff --git a/guest/scripts/configure-rootfs.sh b/guest/scripts/configure-rootfs.sh
index 3603a7f7..2b80db97 100755
--- a/guest/scripts/configure-rootfs.sh
+++ b/guest/scripts/configure-rootfs.sh
@@ -76,7 +76,6 @@ chmod 0755 \
"$root/usr/bin/omarchy-audio-input-set-default" \
"$root/usr/bin/omarchy-screensaver" \
"$root/usr/bin/omarchy-theme-bg-switcher" \
- "$root/usr/local/bin/alacritty" \
"$root/usr/local/bin/xdg-terminal-exec" \
"$root/usr/local/bin/kitty" \
"$root/usr/local/bin/omarchy-arch-aarch64" \
@@ -92,6 +91,7 @@ chmod 0755 \
"$root/usr/local/bin/try-omarchy-touch-id-test" \
"$root/usr/local/lib/try-omarchy/native-authentication-broker" \
"$root/usr/local/sbin/try-omarchy-touch-id-control" \
+ "$root/usr/local/sbin/try-omarchy-migrate-alacritty" \
"$root/usr/local/sbin/try-omarchy-touch-id-enroll" \
"$root/usr/local/lib/try-omarchy/install-vivaldi-arm64" \
"$root/usr/lib/systemd/system-generators/try-omarchy-ssh-access"
diff --git a/guest/scripts/finalize-rootfs.sh b/guest/scripts/finalize-rootfs.sh
index 013beae2..4e5b2a2f 100755
--- a/guest/scripts/finalize-rootfs.sh
+++ b/guest/scripts/finalize-rootfs.sh
@@ -103,6 +103,7 @@ printf '%s %s\n' "$expected_vivaldi_key_sha256" "$vivaldi_key" | sha256sum -c -
systemctl enable omarchy-provision-owner.service
systemctl enable sddm.service
systemctl enable omarchy-native-mac-share.service
+systemctl enable try-omarchy-migrate-alacritty.service
# The app expands only the writable APFS clone to 24 GiB. Grow ext4 online so
# Omarchy's update-safety check sees that working capacity.
diff --git a/guest/native-overlay/usr/local/bin/alacritty b/guest/tests/fixtures/alacritty-software-wrapper
old mode 100755
new mode 100644
similarity index 100%
rename from guest/native-overlay/usr/local/bin/alacritty
rename to guest/tests/fixtures/alacritty-software-wrapper
diff --git a/guest/tests/test_alacritty_migration.py b/guest/tests/test_alacritty_migration.py
new file mode 100644
index 00000000..75c558e0
--- /dev/null
+++ b/guest/tests/test_alacritty_migration.py
@@ -0,0 +1,144 @@
+import hashlib
+import importlib.machinery
+import importlib.util
+import os
+from pathlib import Path
+import tempfile
+import unittest
+
+GUEST = Path(__file__).resolve().parents[1]
+HELPER = GUEST / "native-overlay/usr/local/sbin/try-omarchy-migrate-alacritty"
+loader = importlib.machinery.SourceFileLoader("alacritty_migration", str(HELPER))
+spec = importlib.util.spec_from_loader(loader.name, loader)
+migration = importlib.util.module_from_spec(spec)
+loader.exec_module(migration)
+
+
+class AlacrittyMigrationTests(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory()
+ self.root = Path(self.temporary.name).resolve()
+ self.directory = self.root / "bin"
+ self.directory.mkdir(mode=0o755)
+ self.cmdline = self.root / "cmdline"
+ self.cmdline.write_text("quiet omarchy.virgl_dual_source=1\n")
+ self.original = (GUEST / "tests/fixtures/alacritty-software-wrapper").read_bytes()
+ self.wrapper = self.directory / "alacritty"
+ self.wrapper.write_bytes(self.original)
+ self.wrapper.chmod(0o755)
+ self.backup = self.directory / migration.BACKUP
+ self.addCleanup(self.temporary.cleanup)
+
+ def run_migration(self, directory=None, uid=None):
+ return migration.migrate(self.cmdline, directory or self.directory,
+ os.getuid() if uid is None else uid)
+
+ def test_factory_fixture_matches_allowlisted_digest(self):
+ self.assertEqual(hashlib.sha256(self.original).hexdigest(), migration.WRAPPER_SHA256)
+ self.assertEqual(len(self.original), migration.WRAPPER_SIZE)
+
+ def test_exact_factory_wrapper_backed_up_with_metadata(self):
+ original = self.wrapper.stat()
+ self.assertIn("Migrated:", self.run_migration())
+ self.assertFalse(self.wrapper.exists())
+ self.assertEqual(self.backup.read_bytes(), self.original)
+ self.assertEqual(self.backup.stat().st_ino, original.st_ino)
+ self.assertEqual(self.backup.stat().st_mode, original.st_mode)
+
+ def test_marker_must_be_exact(self):
+ for marker in ("quiet", "omarchy.virgl_dual_source=10",
+ "xomarchy.virgl_dual_source=1", "omarchy.qemu_virgl=1"):
+ with self.subTest(marker=marker):
+ self.cmdline.write_text(marker)
+ self.assertIn("Skipped:", self.run_migration())
+ self.assertEqual(self.wrapper.read_bytes(), self.original)
+ self.assertFalse(self.backup.exists())
+
+ def test_custom_wrapper_preserved(self):
+ self.wrapper.write_bytes(self.original + b"\n# custom\n")
+ self.assertIn("custom contents", self.run_migration())
+ self.assertTrue(self.wrapper.exists())
+ self.assertFalse(self.backup.exists())
+
+ def test_symlink_wrapper_preserved(self):
+ target = self.root / "custom"
+ self.wrapper.rename(target)
+ self.wrapper.symlink_to(target)
+ self.assertIn("symbolic link", self.run_migration())
+ self.assertTrue(self.wrapper.is_symlink())
+ self.assertEqual(target.read_bytes(), self.original)
+
+ def test_symlink_directory_refused(self):
+ link = self.root / "linked-bin"
+ link.symlink_to(self.directory)
+ with self.assertRaises(OSError):
+ self.run_migration(link)
+ self.assertTrue(self.wrapper.exists())
+
+ def test_symlink_ancestor_refused(self):
+ nested = self.directory / "nested"
+ nested.mkdir()
+ link = self.root / "ancestor"
+ link.symlink_to(self.directory)
+ with self.assertRaises(OSError):
+ self.run_migration(link / "nested")
+ self.assertTrue(self.wrapper.exists())
+
+ def test_existing_backup_not_overwritten(self):
+ self.backup.write_text("prior backup")
+ self.assertIn("backup already exists", self.run_migration())
+ self.assertEqual(self.backup.read_text(), "prior backup")
+ self.assertTrue(self.wrapper.exists())
+
+ def test_backup_symlink_not_followed(self):
+ target = self.root / "untouched"
+ target.write_text("custom")
+ self.backup.symlink_to(target)
+ self.assertIn("backup already exists", self.run_migration())
+ self.assertEqual(target.read_text(), "custom")
+ self.assertTrue(self.wrapper.exists())
+
+ def test_second_run_is_noop(self):
+ self.run_migration()
+ inode = self.backup.stat().st_ino
+ self.assertIn("No migration needed", self.run_migration())
+ self.assertEqual(self.backup.stat().st_ino, inode)
+
+ def test_untrusted_owner_preserved(self):
+ self.assertIn("not protected", self.run_migration(uid=os.getuid() + 1))
+ self.assertTrue(self.wrapper.exists())
+
+ def test_writable_directory_preserved(self):
+ self.directory.chmod(0o777)
+ self.assertIn("not protected", self.run_migration())
+ self.assertTrue(self.wrapper.exists())
+
+ def test_writable_wrapper_preserved(self):
+ self.wrapper.chmod(0o777)
+ self.assertIn("not an unmodified factory", self.run_migration())
+ self.assertTrue(self.wrapper.exists())
+
+ def test_hardlinked_wrapper_preserved(self):
+ os.link(self.wrapper, self.root / "other-link")
+ self.assertIn("not an unmodified factory", self.run_migration())
+ self.assertTrue(self.wrapper.exists())
+
+ def test_fifo_not_opened_for_blocking_read(self):
+ self.wrapper.unlink()
+ os.mkfifo(self.wrapper)
+ self.assertIn("not an unmodified factory", self.run_migration())
+ self.assertTrue(self.wrapper.exists())
+
+ def test_service_and_factory_hooks(self):
+ service = (GUEST / "native-overlay/usr/lib/systemd/system/try-omarchy-migrate-alacritty.service").read_text()
+ self.assertIn("ConditionKernelCommandLine=" + migration.MARKER, service)
+ self.assertIn("Before=sddm.service display-manager.service", service)
+ self.assertIn("ExecStart=/usr/local/sbin/try-omarchy-migrate-alacritty", service)
+ self.assertIn('"$root/usr/local/sbin/try-omarchy-migrate-alacritty"',
+ (GUEST / "scripts/configure-rootfs.sh").read_text())
+ self.assertIn("systemctl enable try-omarchy-migrate-alacritty.service",
+ (GUEST / "scripts/finalize-rootfs.sh").read_text())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/guest/tests/test_xdg_terminal_exec.py b/guest/tests/test_xdg_terminal_exec.py
index 364d4cbd..5108f3fe 100644
--- a/guest/tests/test_xdg_terminal_exec.py
+++ b/guest/tests/test_xdg_terminal_exec.py
@@ -164,7 +164,7 @@ def test_factory_wrappers_require_executable_package_binary(self) -> None:
)
helper.chmod(0o755)
self.write_command("foot")
- for name, desktop_id in (("alacritty", "Alacritty.desktop"), ("kitty", "kitty.desktop")):
+ for name, desktop_id in (("kitty", "kitty.desktop"),):
wrapper = self.commands / name
real = package_bin / name
source = HELPER.parents[4] / f"native-overlay/usr/local/bin/{name}"
diff --git a/guest/tests/verify.py b/guest/tests/verify.py
index cfac9174..fac13f3a 100755
--- a/guest/tests/verify.py
+++ b/guest/tests/verify.py
@@ -1469,16 +1469,10 @@ def main() -> None:
"native background picker override is executable",
)
check(cursor_restore.stat().st_mode & stat.S_IXUSR != 0, "native cursor restore helper is executable")
- alacritty_wrapper = GUEST / "native-overlay/usr/local/bin/alacritty"
- alacritty_wrapper_text = read(alacritty_wrapper)
- check(alacritty_wrapper.stat().st_mode & stat.S_IXUSR != 0, "Alacritty VirGL wrapper is executable")
check(
- 'real=/usr/bin/alacritty' in alacritty_wrapper_text
- and "export LIBGL_ALWAYS_SOFTWARE=1" in alacritty_wrapper_text
- and "omarchy.qemu_virgl=1" in alacritty_wrapper_text
- and 'exec "$real" "$@"' in alacritty_wrapper_text
- and '"$root/usr/local/bin/alacritty"' in configure,
- "Alacritty VirGL wrapper forces software GL onto the pacman binary",
+ not (GUEST / "native-overlay/usr/local/bin/alacritty").exists()
+ and '"$root/usr/local/bin/alacritty"' not in configure,
+ "Alacritty uses the accelerated pacman binary without a software GL wrapper",
)
xdg_terminal = GUEST / "factory-overlay/usr/local/bin/xdg-terminal-exec"
xdg_terminal_text = read(xdg_terminal)
@@ -1617,7 +1611,6 @@ def main() -> None:
screensaver_override,
background_switcher_override,
cursor_restore,
- alacritty_wrapper,
kitty_wrapper,
display_sync,
mac_share,
diff --git a/macos/Info.plist b/macos/Info.plist
index 6325fef2..33f574fe 100644
--- a/macos/Info.plist
+++ b/macos/Info.plist
@@ -29,7 +29,7 @@
LSUIElement
LSMinimumSystemVersion
- 15.0
+ 26.0
NSHighResolutionCapable
NSDesktopFolderUsageDescription
diff --git a/macos/Package.swift b/macos/Package.swift
index d24fbf57..0cafc88c 100644
--- a/macos/Package.swift
+++ b/macos/Package.swift
@@ -3,7 +3,7 @@ import PackageDescription
let package = Package(
name: "OmarchyVMHelper",
- platforms: [.macOS(.v15)],
+ platforms: [.macOS("26.0")],
products: [
.executable(name: "omarchy-vm-helper", targets: ["OmarchyVMHelper"]),
],
diff --git a/macos/README.md b/macos/README.md
index 16ce49c1..3de7d315 100644
--- a/macos/README.md
+++ b/macos/README.md
@@ -21,7 +21,7 @@ make test
`make build` creates both first.
The staged runtime is a complete, checksum-pinned Apple Silicon closure built
-for macOS 15.0. Runtime and app assembly do not resolve libraries or `zstd`
+for macOS 26.0. Runtime and app assembly do not resolve libraries or `zstd`
from the host Homebrew prefix, so building on a newer macOS release cannot
silently raise the app's deployment target.
diff --git a/macos/Tests/macos-compatibility.test.sh b/macos/Tests/macos-compatibility.test.sh
index 51271b92..676dbbc7 100755
--- a/macos/Tests/macos-compatibility.test.sh
+++ b/macos/Tests/macos-compatibility.test.sh
@@ -42,12 +42,12 @@ fixture_root="$test_root/fixtures with spaces"
mkdir -p "$fixture_root/nested"
printf '%s\n' 'not a Mach-O image' >"$fixture_root/readme.txt"
-clang -arch arm64 -mmacosx-version-min=15.0 -x c - \
+clang -arch arm64 -mmacosx-version-min=26.0 -x c - \
-o "$fixture_root/nested/compatible" <<'EOF'
int main(void) { return 0; }
EOF
-clang -arch arm64 -mmacosx-version-min=15.0 \
+clang -arch arm64 -mmacosx-version-min=26.0 \
-Werror=unguarded-availability-new -Wl,-undefined,dynamic_lookup -x c - \
-o "$fixture_root/nested/guarded-weak-import" <<'EOF'
#include
@@ -63,33 +63,45 @@ EOF
"$verifier" "$fixture_root" >/dev/null
-clang -arch arm64 -mmacosx-version-min=15.1 -x c - \
+clang -arch arm64 -mmacosx-version-min=26.1 -x c - \
-o "$fixture_root/nested/too-new" <<'EOF'
int main(void) { return 0; }
EOF
-assert_fails_with 'requires macOS 15.1' "$verifier" "$fixture_root"
+assert_fails_with 'requires macOS 26.1' "$verifier" "$fixture_root"
rm -f "$fixture_root/nested/too-new"
-clang -arch arm64 -mmacosx-version-min=15.0 \
+clang -arch arm64 -mmacosx-version-min=26.0 \
-Wl,-undefined,dynamic_lookup -x c - \
-o "$fixture_root/nested/strong-import" <<'EOF'
#include
extern char *strchrnul(const char *, int);
int main(void) { return strchrnul("compatibility", 'x') == NULL; }
EOF
-assert_fails_with 'strongly imports _strchrnul' "$verifier" "$fixture_root"
+"$verifier" "$fixture_root" >/dev/null
rm -f "$fixture_root/nested/strong-import"
-clang -arch arm64 -mmacosx-version-min=15.0 -x c - \
+clang -arch arm64 -mmacosx-version-min=26.0 -x c - \
-o "$test_root/arm64-compatible" <<'EOF'
int main(void) { return 0; }
EOF
-clang -arch x86_64 -mmacosx-version-min=15.1 -x c - \
+clang -arch x86_64 -mmacosx-version-min=26.1 -x c - \
-o "$test_root/x86_64-too-new" <<'EOF'
int main(void) { return 0; }
EOF
lipo -create "$test_root/arm64-compatible" "$test_root/x86_64-too-new" \
-output "$fixture_root/nested/universal-too-new"
-assert_fails_with 'requires macOS 15.1' "$verifier" "$fixture_root"
+assert_fails_with 'requires macOS 26.1' "$verifier" "$fixture_root"
+
+# Reject older hosts before app assembly or launcher state handling begins.
+mkdir "$test_root/old-host-bin"
+cat >"$test_root/old-host-bin/sw_vers" <<'EOF'
+#!/bin/bash
+printf '%s\n' '25.5'
+EOF
+chmod 0755 "$test_root/old-host-bin/sw_vers"
+assert_fails_with 'requires macOS 26 or newer' env \
+ PATH="$test_root/old-host-bin:$PATH" "$native_dir/run-qemu-gpu.sh"
+assert_fails_with 'macOS 26 or newer is required' env \
+ PATH="$test_root/old-host-bin:$PATH" "$native_dir/build-app.sh"
echo 'macos compatibility tests passed'
diff --git a/macos/Tests/qemu-memory-contract.test.sh b/macos/Tests/qemu-memory-contract.test.sh
index fc9b21c0..c9b62c33 100755
--- a/macos/Tests/qemu-memory-contract.test.sh
+++ b/macos/Tests/qemu-memory-contract.test.sh
@@ -41,7 +41,13 @@ mkdir -p \
"$resources/scripts" \
"$shim_dir"
-/bin/cp "$macos_dir/run-qemu-gpu.sh" "$resources/scripts/run-qemu-gpu.sh"
+# The copied launcher must never reap another app's live run directories.
+# Keep the unique prefix directly in /private/tmp for short Unix socket paths.
+sed "s|/private/tmp/omarchy-qemu-gpu\\.|/private/tmp/${test_root##*/}-run.|g" \
+ "$macos_dir/run-qemu-gpu.sh" >"$resources/scripts/run-qemu-gpu.sh"
+if grep -Fq '/private/tmp/omarchy-qemu-gpu.' "$resources/scripts/run-qemu-gpu.sh"; then
+ fail "test launcher still refers to production run directories"
+fi
/bin/cp "$macos_dir/qemu-port-forwarding.sh" "$resources/scripts/qemu-port-forwarding.sh"
chmod 755 "$resources/scripts/run-qemu-gpu.sh"
chmod 644 "$resources/scripts/qemu-port-forwarding.sh"
diff --git a/macos/Tests/qemu-persistent-storage.test.sh b/macos/Tests/qemu-persistent-storage.test.sh
index b4f46bd7..699993c0 100755
--- a/macos/Tests/qemu-persistent-storage.test.sh
+++ b/macos/Tests/qemu-persistent-storage.test.sh
@@ -189,6 +189,12 @@ printf '\x28\xb5\x2f\xfdzstd-initramfs\n' >"$initramfs_zstd"
printf '\x28\xb5\x2f\xfd' >"$initramfs_zstd_truncated"
kernel_command_line_a='root=/dev/vda rw rootwait console=tty0 console=hvc0 loglevel=4'
kernel_command_line_b='root=/dev/vda rw rootwait console=tty0 console=hvc0 loglevel=5'
+assert _qps_validate_kernel_command_line "$kernel_command_line_a"
+assert_fails _qps_validate_kernel_command_line \
+ "$kernel_command_line_a omarchy.virgl_dual_source=1"
+assert_fails _qps_validate_kernel_command_line \
+ "$kernel_command_line_a omarchy.virgl_dual_source=0"
+
# Inspecting a new location reports "missing" without asking for, copying, or
# materializing a factory disk. A full selection then creates the VM and pairs
diff --git a/macos/Tests/qemu-power-actions.test.sh b/macos/Tests/qemu-power-actions.test.sh
index 593e574d..0b1ceee2 100755
--- a/macos/Tests/qemu-power-actions.test.sh
+++ b/macos/Tests/qemu-power-actions.test.sh
@@ -25,6 +25,11 @@ action_count=$(printf '%s\n' "$qemu_arguments" | grep -Ec -- \
fail 'QEMU must not remain open after a guest shutdown'
}
+capability_argument_count=$(printf '%s\n' "$qemu_arguments" | grep -Fc -- \
+ 'omarchy.qemu_virgl=1 omarchy.virgl_dual_source=1' || true)
+[[ $capability_argument_count == 1 ]] || \
+ fail 'QEMU must advertise the corrected VirGL capability exactly once'
+
qmp_argument_count=$(printf '%s\n' "$qemu_arguments" | grep -Fxc -- \
' -qmp "unix:$qmp_socket,server=on,wait=off"' || true)
[[ $qmp_argument_count == 1 ]] || {
diff --git a/macos/Tests/run-qemu-ssh-contract.test.sh b/macos/Tests/run-qemu-ssh-contract.test.sh
index 54a9cde0..0b2a99d3 100755
--- a/macos/Tests/run-qemu-ssh-contract.test.sh
+++ b/macos/Tests/run-qemu-ssh-contract.test.sh
@@ -45,7 +45,13 @@ mkdir -p \
"$resources/scripts" \
"$shim_dir"
-/bin/cp "$macos_dir/run-qemu-gpu.sh" "$resources/scripts/run-qemu-gpu.sh"
+# The copied launcher must never reap another app's live run directories.
+# Keep the unique prefix directly in /private/tmp for short Unix socket paths.
+sed "s|/private/tmp/omarchy-qemu-gpu\\.|/private/tmp/${test_root##*/}-run.|g" \
+ "$macos_dir/run-qemu-gpu.sh" >"$resources/scripts/run-qemu-gpu.sh"
+if grep -Fq '/private/tmp/omarchy-qemu-gpu.' "$resources/scripts/run-qemu-gpu.sh"; then
+ fail "test launcher still refers to production run directories"
+fi
/bin/cp "$macos_dir/qemu-port-forwarding.sh" "$resources/scripts/qemu-port-forwarding.sh"
chmod 755 "$resources/scripts/run-qemu-gpu.sh"
chmod 644 "$resources/scripts/qemu-port-forwarding.sh"
@@ -57,6 +63,9 @@ if [[ ${1:-} == --bridge-native-audio \
|| ${1:-} == --bridge-native-authentication \
|| ${1:-} == --bridge-native-clipboard \
|| ${1:-} == --bridge-native-camera ]]; then
+ if [[ $1 == --bridge-native-audio && ${FAKE_AUDIO_EXIT_EARLY:-0} == 1 ]]; then
+ exit 0
+ fi
while kill -0 "$2" 2>/dev/null; do
sleep 0.02
done
@@ -113,6 +122,7 @@ arguments = sys.argv[1:]
is_recovery = "Try Omarchy Boot Recovery" in arguments
log_variable = "FAKE_QEMU_RECOVERY_LOG" if is_recovery else "FAKE_QEMU_LOG"
Path(os.environ[log_variable]).write_text("\n".join(arguments) + "\n")
+Path(os.environ[log_variable] + ".pid").write_text(str(os.getpid()))
if is_recovery:
export_path = None
@@ -288,6 +298,18 @@ if [[ $# == 2 && $1 == -n && $2 == hw.memsize ]]; then
fi
exec /usr/sbin/sysctl "$@"
SH
+cat >"$shim_dir/ps" <<'SH'
+#!/bin/bash
+if [[ ${FAKE_SHUTDOWN_RACE:-0} == 1 && -f ${FAKE_QEMU_LOG:-}.pid && $* == "-p $(cat "$FAKE_QEMU_LOG.pid") -o state=" && ! -e $FAKE_QEMU_LOG.raced ]]; then
+ state=$(/bin/ps "$@" 2>/dev/null) || exit $?
+ touch "$FAKE_QEMU_LOG.raced"
+ # Return a stale live snapshot only after QEMU and its bridges have exited.
+ sleep 0.5
+ printf '%s\n' "$state"
+ exit 0
+fi
+exec /bin/ps "$@"
+SH
chmod 755 "$shim_dir"/*
guest="$resources/guest"
@@ -453,6 +475,11 @@ assert_contains "$(<"$test_root/disabled/storage.log")" create
assert_line_pair "$test_root/disabled/qemu.log" -smp '8,sockets=1,cores=8,threads=1'
assert_line_pair "$test_root/disabled/qemu.log" -m 4096M
+run_scenario shutdown-race 0 '' FAKE_SHUTDOWN_RACE=1
+[[ -e $test_root/shutdown-race/qemu.log.raced ]] || fail 'shutdown race was not exercised'
+run_scenario audio-exits-early 1 '' FAKE_AUDIO_EXIT_EARLY=1 FAKE_QEMU_LIFETIME=2
+assert_contains "$(<"$test_root/audio-exits-early/stderr")" 'native audio bridge exited while QEMU was running'
+
# Exercise resource values through the real launcher and its QEMU boundary.
run_scenario resources 0 '' FAKE_HOST_CPUS=18 \
OMARCHY_QEMU_GPU_CPUS=18 OMARCHY_QEMU_GPU_MEMORY_MIB=12288
diff --git a/macos/Tests/runtime-relocation.test.sh b/macos/Tests/runtime-relocation.test.sh
index 559f2775..f9a9343c 100755
--- a/macos/Tests/runtime-relocation.test.sh
+++ b/macos/Tests/runtime-relocation.test.sh
@@ -43,7 +43,7 @@ runtime="$test_root/runtime with spaces"
missing_runtime="$test_root/missing-runtime"
mkdir -p "$runtime/bin" "$runtime/lib" "$missing_runtime/bin" "$missing_runtime/lib"
-clang -arch arm64 -mmacosx-version-min=15.0 -dynamiclib \
+clang -arch arm64 -mmacosx-version-min=26.0 -dynamiclib \
-Wl,-headerpad_max_install_names \
-Wl,-install_name,/private/omarchy-build/libfixture.dylib \
-x c - -o "$runtime/lib/libfixture.dylib" <<'EOF'
@@ -51,7 +51,7 @@ int omarchy_fixture_value(void) { return 42; }
EOF
for output in "$runtime/bin/fixture" "$missing_runtime/bin/fixture"; do
- clang -arch arm64 -mmacosx-version-min=15.0 \
+ clang -arch arm64 -mmacosx-version-min=26.0 \
-Wl,-headerpad_max_install_names \
-Wl,-rpath,/private/omarchy-build \
"$runtime/lib/libfixture.dylib" -x c - -o "$output" <<'EOF'
diff --git a/macos/build-app.sh b/macos/build-app.sh
index ee996975..c8ad8412 100755
--- a/macos/build-app.sh
+++ b/macos/build-app.sh
@@ -44,6 +44,12 @@ while (($#)); do
esac
done
+macos_major=$(sw_vers -productVersion | cut -d. -f1)
+[[ $macos_major =~ ^[0-9]+$ ]] && (( macos_major >= 26 )) || {
+ echo "build-app: macOS 26 or newer is required" >&2
+ exit 1
+}
+
macos_dir=$(cd "$(dirname "$0")" && pwd)
repo_dir=$(cd "$macos_dir/.." && pwd -P)
helper="$macos_dir/.build/release/omarchy-vm-helper"
@@ -123,7 +129,7 @@ cd "$macos_dir"
mkdir -p "$module_cache/swift" "$module_cache/clang" "$module_cache/icon"
export SWIFT_MODULECACHE_PATH="$module_cache/swift"
export CLANG_MODULE_CACHE_PATH="$module_cache/clang"
-export MACOSX_DEPLOYMENT_TARGET=15.0
+export MACOSX_DEPLOYMENT_TARGET=26.0
swift build --disable-sandbox -c release -debug-info-format none
rm -rf "$iconset"
@@ -168,6 +174,8 @@ install -m 0644 "$macos_dir/Info.plist" "$contents/Info.plist"
install -m 0644 "$generated_icon" "$contents/Resources/TryOmarchy.icns"
ditto "$runtime_source" "$contents/Resources/runtime"
install -m 0755 "$macos_dir/run-qemu-gpu.sh" "$contents/Resources/scripts/run-qemu-gpu.sh"
+install -m 0755 "$repo_dir/guest/native-overlay/usr/local/sbin/try-omarchy-migrate-alacritty" \
+ "$contents/Resources/scripts/try-omarchy-migrate-alacritty"
install -m 0644 "$macos_dir/qemu-persistent-storage.sh" \
"$contents/Resources/scripts/qemu-persistent-storage.sh"
install -m 0644 "$macos_dir/qemu-port-forwarding.sh" \
diff --git a/macos/build-qemu-gpu-runtime.sh b/macos/build-qemu-gpu-runtime.sh
index a96ce3ce..4b4aa15e 100755
--- a/macos/build-qemu-gpu-runtime.sh
+++ b/macos/build-qemu-gpu-runtime.sh
@@ -73,7 +73,7 @@ pinch_patch_sha256=37acb8895dddd35fc66812d0c49ec5fc697f9127e9e12ed2e60d17999bf32
audio_device_patch_sha256=03aca71c26163c337338cc3b2013c35430690fc0e8b66c5ce92a42f59a9b3334
shared_folder_patch_sha256=41247692501655393ae3a40f56915472ab29b6e89c5173e33db1f62cca56632f
strchrnul_patch_sha256=ec1048dd0e8ebe53bf7e8a3bca9bf2f5f4336cd607d4cd077437470e9a32094a
-macos_deployment_target=15.0
+macos_deployment_target=26.0
keycodemap_commit=f5772a62ec52591ff6870b7e8ef32482371f22c6
keycodemap_root="keycodemapdb-$keycodemap_commit"
@@ -92,7 +92,7 @@ ninja_archive_name=ninja-1.13.0-py3-none-macosx_10_9_universal2.whl
ninja_url="https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/$ninja_archive_name"
ninja_sha256=fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1
-virgl_version=1.0.33
+virgl_version=1.0.42
setuptools_archive_name=setuptools-84.0.0-py3-none-any.whl
setuptools_url="https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/$setuptools_archive_name"
setuptools_sha256=51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670
@@ -105,19 +105,19 @@ pip_archive_name=pip-26.2.1-py3-none-any.whl
pip_url="https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/$pip_archive_name"
pip_sha256=71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e
-virgl_archive_name=virglrenderer-1.0.33.arm64_sequoia.bottle.tar.gz
-virgl_url="https://github.com/startergo/homebrew-virglrenderer/releases/download/v1.0.33/$virgl_archive_name"
-virgl_sha256=26ad3e927d300587024cd92276d38bf813f6228d130a1800c97f1c18688b34ba
+virgl_archive_name=virglrenderer-1.0.42.arm64_tahoe.bottle.tar.gz
+virgl_url="https://github.com/startergo/homebrew-virglrenderer/releases/download/v1.0.42/$virgl_archive_name"
+virgl_sha256=64c37340757cf300712d8e74dc43759f81058ec58cf424094d5f79c9c82c0984
-angle_version=1.0.15
-angle_archive_name=angle-1.0.15.arm64_sequoia.bottle.tar.gz
-angle_url="https://github.com/startergo/homebrew-angle/releases/download/v1.0.15/$angle_archive_name"
-angle_sha256=2b41a696f450a941016adf8b157e754c3223b6032ac9b9f0aac4216e899074c7
+angle_version=1.0.16
+angle_archive_name=angle-1.0.16.arm64_sequoia.bottle.tar.gz
+angle_url="https://github.com/startergo/homebrew-angle/releases/download/v1.0.16/$angle_archive_name"
+angle_sha256=29fe2175b157a65f12879f9a12b5c8f94d0a76fafdf41ff009a2fdb4e9df525c
-epoxy_version=1.0.4
-epoxy_archive_name=libepoxy-1.0.4.arm64_sequoia.bottle.tar.gz
-epoxy_url="https://github.com/startergo/homebrew-libepoxy/releases/download/v1.0.4/$epoxy_archive_name"
-epoxy_sha256=8787cc8c34921834665262dff4941216dd6717edddf2c6d5cdfe04f03b24c517
+epoxy_version=1.0.5
+epoxy_archive_name=libepoxy-1.0.5.arm64_sequoia.bottle.tar.gz
+epoxy_url="https://github.com/startergo/homebrew-libepoxy/releases/download/v1.0.5/$epoxy_archive_name"
+epoxy_sha256=109384a1d37edf207a9b9f3d8950710c00767635b3c7ff295e3af83611876ef2
die() {
echo "qemu-source-build: $*" >&2
@@ -144,7 +144,7 @@ done
[[ $(uname -m) == arm64 ]] || die "this source build requires Apple Silicon (arm64)"
macos_major=$(sw_vers -productVersion | awk -F. '{ print $1 }')
[[ $macos_major =~ ^[0-9]+$ ]] || die "could not determine the macOS version"
-((macos_major >= 15)) || die "the pinned GPU bottles require macOS 15 or newer"
+((macos_major >= 26)) || die "the pinned GPU bottles require macOS 26 or newer"
[[ -f $identity_patch && ! -L $identity_patch ]] || \
die "missing Cocoa product-identity patch: $identity_patch"
[[ -f $display_patch && ! -L $display_patch ]] || \
@@ -443,7 +443,7 @@ require_private_pkg_version glib-2.0 2.88.3
require_private_pkg_version pixman-1 0.46.4
require_private_pkg_version slirp 4.9.4
require_private_pkg_version sdl2 2.32.70
-require_private_pkg_version virglrenderer 1.2.0
+require_private_pkg_version virglrenderer 1.3.0
require_private_pkg_version epoxy 1.5.11
build_dir="$source_dir/build"
@@ -482,10 +482,6 @@ log "Configuring QEMU 11.1.1 (HVF-only, Cocoa/VirGL, SLIRP, SDL audio, virtio-9p
config_host="$build_dir/config-host.h"
[[ -f $config_host && ! -L $config_host ]] || die "QEMU configure did not create config-host.h"
-if grep -Eq '^[[:space:]]*#define[[:space:]]+HAVE_STRCHRNUL([[:space:]]+1)?[[:space:]]*$' \
- "$config_host"; then
- die "QEMU incorrectly enabled the macOS 15.4-only strchrnul API"
-fi
python3 - \
"$build_dir/compile_commands.json" \
"-mmacosx-version-min=$macos_deployment_target" \
diff --git a/macos/prepare-qemu-gpu-runtime.sh b/macos/prepare-qemu-gpu-runtime.sh
index 097bd128..fb77d041 100755
--- a/macos/prepare-qemu-gpu-runtime.sh
+++ b/macos/prepare-qemu-gpu-runtime.sh
@@ -51,20 +51,20 @@ dependency_bundler="$native_dir/bundle-macho-dependencies.sh"
compatibility_verifier="$native_dir/verify-macos-compatibility.sh"
runtime_manifest="$native_dir/runtime-files.txt"
-virgl_version=1.0.33
-virgl_archive_name=virglrenderer-1.0.33.arm64_sequoia.bottle.tar.gz
-virgl_url="https://github.com/startergo/homebrew-virglrenderer/releases/download/v1.0.33/$virgl_archive_name"
-virgl_sha256=26ad3e927d300587024cd92276d38bf813f6228d130a1800c97f1c18688b34ba
+virgl_version=1.0.42
+virgl_archive_name=virglrenderer-1.0.42.arm64_tahoe.bottle.tar.gz
+virgl_url="https://github.com/startergo/homebrew-virglrenderer/releases/download/v1.0.42/$virgl_archive_name"
+virgl_sha256=64c37340757cf300712d8e74dc43759f81058ec58cf424094d5f79c9c82c0984
-angle_version=1.0.15
-angle_archive_name=angle-1.0.15.arm64_sequoia.bottle.tar.gz
-angle_url="https://github.com/startergo/homebrew-angle/releases/download/v1.0.15/$angle_archive_name"
-angle_sha256=2b41a696f450a941016adf8b157e754c3223b6032ac9b9f0aac4216e899074c7
+angle_version=1.0.16
+angle_archive_name=angle-1.0.16.arm64_sequoia.bottle.tar.gz
+angle_url="https://github.com/startergo/homebrew-angle/releases/download/v1.0.16/$angle_archive_name"
+angle_sha256=29fe2175b157a65f12879f9a12b5c8f94d0a76fafdf41ff009a2fdb4e9df525c
-epoxy_version=1.0.4
-epoxy_archive_name=libepoxy-1.0.4.arm64_sequoia.bottle.tar.gz
-epoxy_url="https://github.com/startergo/homebrew-libepoxy/releases/download/v1.0.4/$epoxy_archive_name"
-epoxy_sha256=8787cc8c34921834665262dff4941216dd6717edddf2c6d5cdfe04f03b24c517
+epoxy_version=1.0.5
+epoxy_archive_name=libepoxy-1.0.5.arm64_sequoia.bottle.tar.gz
+epoxy_url="https://github.com/startergo/homebrew-libepoxy/releases/download/v1.0.5/$epoxy_archive_name"
+epoxy_sha256=109384a1d37edf207a9b9f3d8950710c00767635b3c7ff295e3af83611876ef2
die() {
echo "qemu-gpu-runtime: $*" >&2
@@ -89,7 +89,7 @@ done
[[ $(uname -m) == arm64 ]] || die "the pinned bottles require Apple Silicon (arm64)"
macos_major=$(sw_vers -productVersion | awk -F. '{ print $1 }')
[[ $macos_major =~ ^[0-9]+$ ]] || die "could not determine the macOS version"
-((macos_major >= 15)) || die "the pinned arm64_sequoia bottles require macOS 15 or newer"
+((macos_major >= 26)) || die "the pinned GPU bottles require macOS 26 or newer"
[[ -n $source_qemu ]] || die "--source-qemu is required"
[[ $source_qemu == /* ]] || die "--source-qemu must be an absolute path"
[[ -f $source_qemu && ! -L $source_qemu && -x $source_qemu ]] || \
@@ -357,7 +357,7 @@ verify_runtime_tree() {
die "could not inspect QEMU's minimum macOS version"
[[ -n $minimum_versions ]] || die "QEMU has no minimum macOS version"
while IFS= read -r minimum_version; do
- [[ $minimum_version == 15.0 ]] || \
+ [[ $minimum_version == 26.0 ]] || \
die "QEMU has unexpected minimum macOS version: $minimum_version"
done <<<"$minimum_versions"
@@ -488,4 +488,4 @@ fi
publish_dir=
log "Prepared $runtime_dir"
-log "Runtime is self-contained, targets macOS 15.0, and contains ${#runtime_files[@]} pinned Mach-O images"
+log "Runtime is self-contained, targets macOS 26.0, and contains ${#runtime_files[@]} pinned Mach-O images"
diff --git a/macos/qemu-persistent-storage.sh b/macos/qemu-persistent-storage.sh
index 08ea6321..cf7156b7 100755
--- a/macos/qemu-persistent-storage.sh
+++ b/macos/qemu-persistent-storage.sh
@@ -638,7 +638,7 @@ _qps_validate_kernel_command_line() {
rootwait) ((qps_rootwait_count += 1)) ;;
console=tty0) ((qps_console_zero_count += 1)) ;;
console=hvc0) ((qps_console_hvc_count += 1)) ;;
- omarchy.qemu_virgl=*|omarchy.shared_folder_name=*|tryomarchy.ssh_access=*|\
+ omarchy.qemu_virgl=*|omarchy.virgl_dual_source=*|omarchy.shared_folder_name=*|tryomarchy.ssh_access=*|\
tryomarchy.export_boot=*) return 1 ;;
esac
done
diff --git a/macos/run-qemu-gpu.sh b/macos/run-qemu-gpu.sh
index a10c96b6..e218b267 100755
--- a/macos/run-qemu-gpu.sh
+++ b/macos/run-qemu-gpu.sh
@@ -55,6 +55,9 @@ port_forwarding_library="$script_dir/qemu-port-forwarding.sh"
[[ $(uname -m) == arm64 ]] || fail "requires an ARM64 Mac"
[[ $(uname -s) == Darwin ]] || fail "requires macOS"
+macos_major=$(sw_vers -productVersion | cut -d. -f1)
+[[ $macos_major =~ ^[0-9]+$ ]] && (( macos_major >= 26 )) || \
+ fail "requires macOS 26 or newer"
[[ -d $guest_input && ! -L $guest_input ]] || fail "ARM guest directory is missing or unsafe: $guest_input"
guest_dir=$(cd "$guest_input" && pwd -P)
@@ -807,6 +810,8 @@ arguments = command_line.split(" ")
for required in ("root=/dev/vda", "rw", "rootwait", "console=tty0", "console=hvc0"):
if arguments.count(required) != 1:
fail(f"kernel command line must contain exactly one {required}")
+if any(argument.startswith("omarchy.virgl_dual_source=") for argument in arguments):
+ fail("kernel command line contains a launcher-owned VirGL capability argument")
if any(argument.startswith("omarchy.qemu_virgl=") for argument in arguments):
fail("kernel command line already contains a QEMU VirGL role")
if any(argument.startswith("omarchy.shared_folder_name=") for argument in arguments):
@@ -916,6 +921,9 @@ IFS=$'\t' read -r bundle_identity source_disk_sha source_disk_bytes compressed_d
(( expanded_disk_bytes >= source_disk_bytes )) || fail "working disk cannot be smaller than its source"
[[ -n $kernel_command_line ]] || fail "validated kernel command line is empty"
case " $kernel_command_line " in
+ *' omarchy.virgl_dual_source='*)
+ fail "validated kernel command line contains a launcher-owned VirGL capability argument"
+ ;;
*' tryomarchy.ssh_access='*)
fail "validated kernel command line contains a launcher-owned SSH activation argument"
;;
@@ -1429,6 +1437,11 @@ launch_kernel_command_line=$QEMU_SELECTED_KERNEL_COMMAND_LINE
[[ -n $launch_kernel && -n $launch_initramfs && -n $launch_kernel_command_line ]] || {
fail 'the selected VM has no complete boot kit'
}
+case " $launch_kernel_command_line " in
+ *' omarchy.virgl_dual_source='*)
+ fail "selected kernel command line contains a launcher-owned VirGL capability argument"
+ ;;
+esac
if ((reset_only)); then
qemu_persistent_storage_release_lock
@@ -1505,7 +1518,7 @@ qemu_args=(
-qmp "unix:$qmp_socket,server=on,wait=off"
-kernel "$launch_kernel"
-initrd "$launch_initramfs"
- -append "$launch_kernel_command_line omarchy.qemu_virgl=1$shared_folder_kernel_argument$ssh_kernel_argument"
+ -append "$launch_kernel_command_line omarchy.qemu_virgl=1 omarchy.virgl_dual_source=1$shared_folder_kernel_argument$ssh_kernel_argument"
-drive "if=none,id=omarchy-root,file=$working_disk,format=raw,media=disk,cache=writeback"
-device 'virtio-blk-pci,drive=omarchy-root,serial=omarchy-root'
-device "$gpu_device"
@@ -1654,6 +1667,9 @@ while true; do
audio_bridge_status=$?
fi
audio_bridge_pid=""
+ # QEMU can exit between the process checks, taking the bridge down normally.
+ qemu_state=$(ps -p "$qemu_pid" -o state= 2>/dev/null || true)
+ [[ -n $qemu_state && $qemu_state != *Z* ]] || break
fail "native audio bridge exited while QEMU was running (status $audio_bridge_status)"
fi
diff --git a/macos/verify-macos-compatibility.sh b/macos/verify-macos-compatibility.sh
index 7e420c5f..599c4c67 100755
--- a/macos/verify-macos-compatibility.sh
+++ b/macos/verify-macos-compatibility.sh
@@ -2,7 +2,7 @@
set -euo pipefail
-minimum_macos_version=15.0
+minimum_macos_version=26.0
usage() {
echo "Usage: macos/verify-macos-compatibility.sh ROOT" >&2
@@ -85,7 +85,8 @@ while IFS= read -r -d '' image; do
strong_imports=$(xcrun nm -u -W -j --add-dyldinfo -arch all "$image" 2>/dev/null) || \
fail "could not inspect strong imports: $image"
- if grep -Fxq '_strchrnul' <<<"$strong_imports"; then
+ if version_is_newer 15.4 "$minimum_macos_version" &&
+ grep -Fxq '_strchrnul' <<<"$strong_imports"; then
fail "$image strongly imports _strchrnul, which is unavailable before macOS 15.4"
fi
done < <(find "$compatibility_root" -type f -print0)