Compare commits

..
Author SHA1 Message Date
vii e5716ab09f fix(input): advance the TextBox caret past trailing whitespace
The final cluster rect SDL_ttf reports stops at the last inked glyph, so a
caret at the end of a single-line field did not move when a trailing space
was typed. Use the laid-out width there; wrapped layouts keep the clamp
because their width is the widest line rather than the last one.
2026-09-18 21:13:53 -05:00
55 changed files with 333 additions and 1236 deletions

No files matched your search

-50
View File
@@ -8,32 +8,22 @@ on:
- 'CMakeLists.txt'
- 'Makefile'
- 'README.md'
- '.gitmodules'
- 'cmake/**'
- 'config/**'
- 'include/**'
- 'packaging/**'
- 'scripts/**'
- 'src/**'
- 'stubs/**'
- 'tests/**'
- 'third_party/**'
pull_request:
paths:
- '.github/workflows/**'
- 'CMakeLists.txt'
- 'Makefile'
- 'README.md'
- '.gitmodules'
- 'cmake/**'
- 'config/**'
- 'include/**'
- 'packaging/**'
- 'scripts/**'
- 'src/**'
- 'stubs/**'
- 'tests/**'
- 'third_party/**'
workflow_dispatch:
permissions:
@@ -75,43 +65,3 @@ jobs:
bash tests/flatpak_packaging_test.sh .
bash tests/native_packaging_test.sh .
bash tests/anylinux_dependencies_test.sh .
native-build:
name: Native build (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: ubuntu-26.04
- arch: aarch64
runner: ubuntu-26.04-arm
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: recursive
- name: Install build dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
build-essential binutils cmake git ninja-build pkg-config lld \
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
libplacebo-dev libpng-dev zlib1g-dev
- name: Build and check native executables
shell: bash
run: |
test "$(uname -m)" = "${{ matrix.arch }}"
cmake -S . -B build-ci -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF
cmake --build build-ci --parallel 4
./build-ci/mocktail --help
./build-ci/mocktail_updater status
+67 -127
View File
@@ -38,24 +38,15 @@ concurrency:
jobs:
flatpak:
name: Flatpak build (${{ matrix.arch }})
name: Flatpak build
if: >-
github.event_name != 'workflow_run' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request' &&
github.event.workflow_run.head_branch == 'main')
runs-on: ${{ matrix.runner }}
runs-on: ubuntu-24.04
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: ubuntu-24.04
- arch: aarch64
runner: ubuntu-24.04-arm
env:
FLATPAK_ARCH: ${{ matrix.arch }}
MOCKTAIL_PUBLISH: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'pull_request' && github.event.workflow_run.head_branch == 'main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
MOCKTAIL_SOURCE_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
steps:
@@ -66,92 +57,8 @@ jobs:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
submodules: recursive
- name: Install Flatpak builder
shell: bash
run: |
sudo apt-get update
sudo apt-get install --yes flatpak flatpak-builder gnupg
flatpak remote-add --user --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
- name: Build Flatpak bundle
shell: bash
env:
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
run: |
set -Eeuo pipefail
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_SHA"
builder_signing=()
bundle_keys=()
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
-n "$FLATPAK_GPG_KEY_ID" ]]
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
builder_signing=(
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
)
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
fi
flatpak-builder \
--arch="$FLATPAK_ARCH" \
--jobs=4 \
--user \
--install-deps-from=flathub \
--disable-rofiles-fuse \
--force-clean \
--default-branch=stable \
--repo=repo \
"${builder_signing[@]}" \
build-flatpak-ci \
packaging/flatpak/space.bigrat.mocktail.json
flatpak build-bundle \
--arch="$FLATPAK_ARCH" \
"${bundle_keys[@]}" \
repo \
"Mocktail-${FLATPAK_ARCH}.flatpak" \
space.bigrat.mocktail \
stable
- name: Upload Flatpak
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Mocktail-${{ matrix.arch }}-flatpak
path: Mocktail-${{ matrix.arch }}.flatpak
if-no-files-found: error
retention-days: 14
publish:
name: Assemble multi-architecture Flatpak repository
if: >-
(github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request' &&
github.event.workflow_run.head_branch == 'main') ||
(github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main')
needs: flatpak
runs-on: ubuntu-24.04
timeout-minutes: 120
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Download both Flatpak bundles
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: Mocktail-*-flatpak
path: flatpak-bundles
merge-multiple: true
- name: Download package repositories
if: env.MOCKTAIL_PUBLISH == 'true'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
@@ -177,53 +84,86 @@ jobs:
-d native-repositories/downloads && \
-s native-repositories/mocktail-packages.gpg ]]
- name: Install Flatpak repository tools
- name: Install Flatpak builder
shell: bash
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends flatpak gnupg ostree
sudo apt-get install --yes flatpak flatpak-builder gnupg
flatpak remote-add --user --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
- name: Sign and publish both architectures
- name: Build Flatpak repository and bundle
shell: bash
env:
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
run: |
set -Eeuo pipefail
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && -n "$FLATPAK_GPG_KEY_ID" ]]
[[ -s flatpak-bundles/Mocktail-x86_64.flatpak && \
-s flatpak-bundles/Mocktail-aarch64.flatpak ]]
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
ostree --repo=repo init --mode=archive
for arch in x86_64 aarch64; do
flatpak build-import-bundle \
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_SHA"
builder_signing=()
bundle_keys=()
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
-n "$FLATPAK_GPG_KEY_ID" ]]
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
builder_signing=(
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
)
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
fi
flatpak-builder \
--arch=x86_64 \
--jobs=4 \
--user \
--install-deps-from=flathub \
--disable-rofiles-fuse \
--force-clean \
--default-branch=stable \
--repo=repo \
"${builder_signing[@]}" \
build-flatpak-ci \
packaging/flatpak/space.bigrat.mocktail.json
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
flatpak build-update-repo \
--generate-static-deltas \
--static-delta-jobs=2 \
--prune \
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
--no-update-summary \
repo "flatpak-bundles/Mocktail-${arch}.flatpak"
ostree --repo=repo refs | \
grep -Fxq "app/space.bigrat.mocktail/${arch}/stable"
done
flatpak build-update-repo \
--generate-static-deltas \
--static-delta-jobs=2 \
--prune \
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
repo
./scripts/assemble_flatpak_pages.sh \
repo flatpak-bundles/Mocktail-x86_64.flatpak \
flatpak-bundles/Mocktail-aarch64.flatpak \
mocktail-flatpak.gpg public native-repositories
repo
fi
flatpak build-bundle \
--arch=x86_64 \
"${bundle_keys[@]}" \
repo \
Mocktail-x86_64.flatpak \
space.bigrat.mocktail \
stable
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
./scripts/assemble_flatpak_pages.sh \
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg \
public native-repositories
fi
- name: Upload Flatpak
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Mocktail-x86_64-flatpak
path: Mocktail-x86_64.flatpak
if-no-files-found: error
retention-days: 14
- name: Configure GitHub Pages
if: env.MOCKTAIL_PUBLISH == 'true'
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Upload GitHub Pages repository
if: env.MOCKTAIL_PUBLISH == 'true'
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: public
@@ -237,7 +177,7 @@ jobs:
github.event.workflow_run.head_branch == 'main') ||
(github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main')
needs: publish
needs: flatpak
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
+3 -115
View File
@@ -143,117 +143,6 @@ jobs:
if-no-files-found: error
retention-days: 14
appimage-aarch64:
name: AppImage (AnyLinux aarch64)
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
submodules: recursive
- name: Build aarch64 AppImage
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
docker run --rm --platform linux/arm64 \
--volume "$GITHUB_WORKSPACE:/workspace" \
--workdir /workspace \
--env GH_TOKEN \
ghcr.io/pkgforge-dev/archlinux:latest@sha256:b2507f1964270cab3cc190aa8df858521f6a7e45e969146a012877891d5dfc9b bash -Eeuo pipefail -c '
test "$(uname -m)" = aarch64
pacman-key --init
pacman -Syy --noconfirm archlinux-keyring
pacman -Syu --noconfirm --needed \
base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
vulkan-headers zlib libplacebo libpng bubblewrap xdg-dbus-proxy \
patchelf jq unzip file ca-certificates dbus zsync
./scripts/install_anylinux_dependencies.sh
curl --fail --location --retry 3 \
--output /tmp/quick-sharun \
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/quick-sharun.sh
echo "fa66d0189e1e87df781c049dfb23cbb1e753786056f693a4483d6d3af61e7ee1 /tmp/quick-sharun" | \
sha256sum --check --strict
chmod 0755 /tmp/quick-sharun
curl --fail --location --retry 3 \
--output /tmp/sharun \
https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.6/sharun-aarch64
echo "3b64db0727a9d5c8212c4b2342770583ce552deac0644d2d6033e9ec280f7635 /tmp/sharun" | \
sha256sum --check --strict
chmod 0755 /tmp/sharun
curl --fail --location --retry 3 \
--output /tmp/anylinux.c \
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/lib/anylinux.c
echo "f50650ad96d177559bdd0737df509fa20bd4af08887631c738fc1f31bfac185f /tmp/anylinux.c" | \
sha256sum --check --strict
curl --fail --location --retry 3 \
--output /tmp/appimagetool \
https://github.com/pkgforge-dev/appimagetool/releases/download/0.3.3/appimagetool-aarch64-linux
echo "298f5293c41f9b2b46290b246e6419783940824ce20b98775a5a153b18fe8422 /tmp/appimagetool" | \
sha256sum --check --strict
chmod 0755 /tmp/appimagetool
curl --fail --location --retry 3 \
--output /tmp/uruntime \
https://github.com/VHSgunzo/uruntime/releases/download/v0.5.9/uruntime-appimage-dwarfs-lite-aarch64
echo "5686f126b22d944b2b9c862ee004b4c590b5a5e5215d8790cc0420f1fbbea942 /tmp/uruntime" | \
sha256sum --check --strict
chmod 0755 /tmp/uruntime
curl --fail --location --retry 3 \
--output /tmp/mkdwarfs \
https://github.com/mhx/dwarfs/releases/download/v0.15.6/dwarfs-universal-0.15.6-Linux-aarch64
echo "2d513c43ad652163e5e31c1a96f3c736e7e188e661ff62fc9143ea7147bd936b /tmp/mkdwarfs" | \
sha256sum --check --strict
chmod 0755 /tmp/mkdwarfs
git config --global --add safe.directory /workspace
git config --global --add safe.directory /workspace/third_party/Vulkan-Headers
git config --global --add safe.directory /workspace/third_party/libjnivm
export MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1
export MOCKTAIL_ANYLINUX_SHARUN=/tmp/sharun
export MOCKTAIL_ANYLINUX_C_SOURCE=/tmp/anylinux.c
export RUNTIME=/tmp/uruntime
export DWARFS_CMD=/tmp/mkdwarfs
make appimage BUILD_DIR=build-appimage-aarch64 JOBS=4 \
APPIMAGE=dist/Mocktail-aarch64.AppImage \
PORTABLE_MODE=standalone APPIMAGE_FORMAT=anylinux \
ANYLINUX_PACKAGER=/tmp/quick-sharun \
ANYLINUX_APPIMAGETOOL=/tmp/appimagetool
test -x dist/Mocktail-aarch64.AppImage
'
- name: Smoke-test aarch64 AppImage without FUSE
shell: bash
run: |
smoke_dir="$(mktemp -d)"
trap 'rm -rf -- "$smoke_dir"' EXIT
export XDG_DATA_HOME="$smoke_dir/data"
export XDG_CONFIG_HOME="$smoke_dir/config"
export XDG_CACHE_HOME="$smoke_dir/cache"
export XDG_STATE_HOME="$smoke_dir/state"
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"
timeout 60s dist/Mocktail-aarch64.AppImage --appimage-extract-and-run --help
timeout 60s dist/Mocktail-aarch64.AppImage --appimage-extract-and-run mocktail_updater status
- name: Upload aarch64 AppImage
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Mocktail-AppImage-aarch64
path: dist/Mocktail-aarch64.AppImage
if-no-files-found: error
retention-days: 14
deb:
name: DEB (Ubuntu 26.04)
runs-on: ubuntu-24.04
@@ -397,7 +286,7 @@ jobs:
publish:
name: Publish continuous packages
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
needs: [appimage, appimage-aarch64, deb, rpm, arch]
needs: [appimage, deb, rpm, arch]
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
@@ -419,8 +308,7 @@ jobs:
shell: bash
run: |
set -Eeuo pipefail
mv -- release/Mocktail-x86_64.AppImage release/mocktail-nightly.AppImage
mv -- release/Mocktail-aarch64.AppImage release/mocktail-nightly-aarch64.AppImage
mv -- release/*.AppImage release/mocktail-nightly.AppImage
mv -- release/*.deb release/mocktail-nightly.deb
mv -- release/*.rpm release/mocktail-nightly.rpm
mv -- release/*.pkg.tar.zst release/mocktail-nightly.pkg.tar.zst
@@ -475,7 +363,7 @@ jobs:
gh release upload continuous release/* --clobber
while IFS= read -r asset; do
case "$asset" in
mocktail-nightly.AppImage|mocktail-nightly-aarch64.AppImage|mocktail-nightly.deb|mocktail-nightly.rpm|mocktail-nightly.pkg.tar.zst)
mocktail-nightly.AppImage|mocktail-nightly.deb|mocktail-nightly.rpm|mocktail-nightly.pkg.tar.zst)
;;
Mocktail-*.AppImage|mocktail_*.deb|mocktail-*.deb|mocktail-*.rpm|mocktail-*.pkg.tar.zst)
gh release delete-asset continuous "$asset" --yes
+2 -7
View File
@@ -9,7 +9,7 @@
cmake_minimum_required(VERSION 3.20)
project(Mocktail
VERSION 1.0.4
DESCRIPTION "Android Roblox compatibility runtime for Linux"
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
LANGUAGES CXX
)
@@ -648,11 +648,6 @@ target_link_options(mocktail PRIVATE
)
# Native FreeBSD helper for Linuxulator's unsupported DF socket option.
# The helper is x86-64 assembly; other host architectures have nothing to
# assemble.
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
set(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER OFF)
endif()
option(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER
"Build the native FreeBSD Linuxulator socket helper" ON)
if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER)
@@ -797,7 +792,7 @@ set(CPACK_PACKAGE_CONTACT "komaruworld <rtysus228@gmail.com>")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/komaruworld/mocktail")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}")
set(CPACK_PACKAGE_DESCRIPTION
"Runs the Android Roblox client on Linux."
"Runs the Android x86-64 Roblox client on Linux."
)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
-12
View File
@@ -32,18 +32,6 @@ attach `latest.log`:
For native installs, a custom `$XDG_STATE_HOME` replaces `~/.local/state`.
## Can I run multiple Roblox instances?
No. Mocktail won't add multi-instance launching. It would mean working around
Roblox's restrictions on running several clients. The [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use)
prohibit bypassing technical protections, and violating the terms can get your
account suspended.
Bloxstrap brought the option back in [v2.9.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.9.0)
with a "use at your own risk" warning, then removed it in
[v2.10.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.10.0)
after Roblox added measures against running multiple clients.
## Can I play in VR?
VR is experimental. Install the [build dependencies](README.md#building),
+2 -2
View File
@@ -129,8 +129,8 @@ auto-run: ## Build/run loop with crash summaries
run-gles: build ## Run strict system EGL/OpenGL ES 3 without Vulkan fallback
@$(BINARY) --graphics opengl
run-angle: build ## Run with ANGLE/Vulkan compatibility backend
@$(BINARY) --graphics angle-vulkan
run-angle: ## Run with ANGLE/Vulkan compatibility backend
@MOCKTAIL_GRAPHICS_BACKEND=angle-vulkan ./scripts/run_sober.sh
update-roblox: ## Validate and import the current x86_64 Roblox bundle cached by Sober
@./scripts/update_roblox_payload.sh
+9 -12
View File
@@ -6,11 +6,10 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Discord](https://img.shields.io/discord/1543955607869063269?label=discord)](https://discord.gg/rhgpfcmFSD)
Mocktail runs the Android `x86_64` Roblox client on Linux and has experimental
`aarch64` builds for the Android `arm64-v8a` client. It also runs in a Linux
userspace hosted by FreeBSD's Linuxulator. Mocktail provides the Android ABI
and JNI pieces the client expects, then connects them to SDL3 and Vulkan or
OpenGL on the Linux side.
Mocktail runs the Android `x86_64` Roblox client on Linux, including a Linux
userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI
pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on
the Linux side.
Mocktail is an independent community project. It is not affiliated with Roblox
Corporation or VinegarHQ and does not distribute the Roblox client.
@@ -125,12 +124,10 @@ To see the most Frequently Asked Question go check [FAQ.md](FAQ.md)
## Building
Linux `x86_64` is supported. Linux `aarch64` builds are available, with Roblox
runtime validation still experimental. FreeBSD 15.1 Linuxulator support is
experimental and has been tested with an `x86_64` Fedora 44 userspace. On
FreeBSD, Mocktail runs inside Linuxulator; it is not a native FreeBSD binary.
Building requires CMake 3.20+, Git, pkg-config, LLD, binutils, a C++17
compiler, SDL 3.4+, SDL3_ttf,
Linux `x86_64` is supported. Experimental FreeBSD 15.1 Linuxulator support has
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Mocktail runs
inside Linuxulator; it is not a native FreeBSD binary. Building requires CMake
3.20+, Git, pkg-config, LLD, binutils, a C++17 compiler, SDL 3.4+, SDL3_ttf,
Vulkan, EGL, libplacebo, fontconfig, libcurl, OpenSSL, libelf, libyaml, minizip,
Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and WebKitGTK 6.0.
@@ -146,7 +143,7 @@ sudo apt install build-essential cmake git ninja-build pkg-config lld \
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
libplacebo-dev libpng-dev zlib1g-dev
libplacebo-dev zlib1g-dev
```
</details>
-17
View File
@@ -4,7 +4,6 @@
{
"version_name": "2.721.1108",
"version_code": 2350,
"abi": "x86_64",
"elf_build_id": "50e1b0abd123350e794226062fe3a1ef360c5f0d",
"status": "legacy-researched",
"default_allowed": false,
@@ -16,7 +15,6 @@
{
"version_name": "2.725.1142",
"version_code": 2546,
"abi": "x86_64",
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
"status": "supported",
"default_allowed": true,
@@ -37,7 +35,6 @@
{
"version_name": "2.727.1199",
"version_code": 2628,
"abi": "x86_64",
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
"status": "supported",
"default_allowed": true,
@@ -49,7 +46,6 @@
{
"version_name": "2.734.917",
"version_code": 2908,
"abi": "x86_64",
"elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326",
"status": "supported",
"default_allowed": true,
@@ -70,19 +66,6 @@
{
"version_name": "2.736.1408",
"version_code": 2998,
"abi": "arm64-v8a",
"elf_build_id": "ed2b035768b1513561d0e6ea7f9bbec35b0e60e5",
"status": "supported",
"default_allowed": true,
"allow_legacy_binary_patches": false,
"allow_host_abi_bridges": true,
"allow_host_constructor_replay": true,
"reason": "AArch64 arm64-v8a payload of the same 2.736.1408 release as the verified x86-64 profile; allocator, constructor replay, and pre-JNI bootstrap RVAs were derived by cross-architecture semantic matching."
},
{
"version_name": "2.736.1408",
"version_code": 2998,
"abi": "x86_64",
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
"status": "supported",
"default_allowed": true,
-30
View File
@@ -33,23 +33,6 @@ void SetLegacyBionicDiagnosticsEnabled(bool enabled);
// guest layout explicit instead of exposing the incompatible host libc type.
using MocktailBionicMutexAttr = int64_t;
// Bionic LP64 pthread_attr_t: 56 bytes with explicit field offsets. The host
// glibc layout is opaque and wider on aarch64, so guest buffers must be read
// and written through this struct only.
struct MocktailBionicPthreadAttr {
std::uint32_t flags;
void* stack_base;
std::size_t stack_size;
std::size_t guard_size;
std::int32_t sched_policy;
std::int32_t sched_priority;
char reserved[16];
};
static_assert(sizeof(MocktailBionicPthreadAttr) == 56);
inline constexpr std::uint32_t kMocktailBionicAttrFlagDetached = 0x1;
inline constexpr std::uint32_t kMocktailBionicAttrFlagUserStack = 0x2;
extern "C" {
void mocktail_set_current_jni_env(void* env);
@@ -101,19 +84,6 @@ int mocktail_pthread_barrier_init(pthread_barrier_t* barrier,
int mocktail_pthread_barrier_destroy(pthread_barrier_t* barrier);
int mocktail_pthread_barrier_wait(pthread_barrier_t* barrier);
int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr,
size_t stack_size);
int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr,
int detach_state);
int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr,
const struct sched_param* parameters);
int mocktail_pthread_getattr_np(pthread_t thread,
MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr,
void** stack_base, size_t* stack_size);
extern void* mocktail_gameactivity_on_start_native;
extern void* mocktail_gameactivity_on_resume_native;
extern void* mocktail_gameactivity_on_surface_created_native;
@@ -25,12 +25,11 @@ void ConfigureBionicPthreadThreadInitializer(
} // namespace mocktail::compat
struct MocktailBionicPthreadAttr;
// Rebuilds attributes instead of passing Android pthread_attr_t to glibc.
extern "C" int mocktail_bionic_pthread_create(
pthread_t* thread, const MocktailBionicPthreadAttr* attr,
void* (*start_routine)(void*), void* argument);
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
const pthread_attr_t* attr,
void* (*start_routine)(void*),
void* argument);
// Guest real-time scheduling is denied to avoid the host RLIMIT_RTTIME watchdog.
extern "C" int mocktail_bionic_pthread_setschedparam(
-38
View File
@@ -1,38 +0,0 @@
#ifndef MOCKTAIL_COMPAT_GUEST_ABI_H_
#define MOCKTAIL_COMPAT_GUEST_ABI_H_
#include <elf.h>
#include <string_view>
namespace mocktail::compat {
#if defined(__aarch64__)
// Android ABI name as it appears in APK lib/ directories and build metadata.
inline constexpr std::string_view kGuestAbi = "arm64-v8a";
// APK split-config name carrying the guest native libraries.
inline constexpr std::string_view kGuestApkSplitName = "config.arm64_v8a";
// File name the payload store uses for the split APK.
inline constexpr std::string_view kGuestSplitApkFile =
"split_config.arm64_v8a.apk";
// sha256 map key holding the split APK digest in payload metadata.
inline constexpr std::string_view kGuestSplitApkHashKey = "arm64_v8a_split_apk";
// Short CPU label for device metadata and user-facing strings.
inline constexpr std::string_view kGuestCpuName = "aarch64";
inline constexpr int kGuestElfMachine = EM_AARCH64;
#elif defined(__x86_64__)
inline constexpr std::string_view kGuestAbi = "x86_64";
inline constexpr std::string_view kGuestApkSplitName = "config.x86_64";
inline constexpr std::string_view kGuestSplitApkFile = "split_config.x86_64.apk";
inline constexpr std::string_view kGuestSplitApkHashKey = "x86_64_split_apk";
inline constexpr std::string_view kGuestCpuName = "x86_64";
inline constexpr int kGuestElfMachine = EM_X86_64;
#else
#error "mocktail only supports aarch64 and x86_64 hosts"
#endif
// APK zip entry holding the guest libroblox.so.
inline constexpr std::string_view kGuestLibApkEntryPrefix = "lib/";
} // namespace mocktail::compat
#endif // MOCKTAIL_COMPAT_GUEST_ABI_H_
-6
View File
@@ -48,15 +48,9 @@ struct PlatformIdentity {
std::string manufacturer = "Mocktail";
std::string model = "Mocktail Headless";
std::string brand = "Mocktail";
#if defined(__aarch64__)
std::string device_code = "linux-arm64";
std::string device_sku = "mocktail-arm64";
std::string soc_model = "aarch64";
#else
std::string device_code = "linux-x86_64";
std::string device_sku = "mocktail-x86_64";
std::string soc_model = "x86_64";
#endif
};
struct RobloxCredentialView {
@@ -6,9 +6,6 @@
#if defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if defined(__aarch64__)
#include <cstdint>
#endif
namespace mocktail::audio {
@@ -25,23 +22,16 @@ class FmodThreadFloatingPointMode final {
delete;
bool Enable(const char* thread_name) noexcept {
#if defined(__x86_64__) || defined(__aarch64__)
#if defined(__x86_64__)
if (active_ || thread_name == nullptr) return false;
const std::string_view name(thread_name);
if (name != "FMOD mixer thread" && name != "FMOD feeder thread" &&
name != "FMOD Convolution thread" && name != "FMOD Worker Thread") {
return false;
}
#if defined(__x86_64__)
const unsigned int control = _mm_getcsr();
original_mode_ = control & kModeMask;
_mm_setcsr(control | kModeMask);
#else
std::uint64_t control;
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
original_mode_ = control & kModeMask;
__asm__ volatile("msr fpcr, %0" ::"r"(control | kModeMask));
#endif
active_ = true;
return true;
#else
@@ -51,18 +41,11 @@ class FmodThreadFloatingPointMode final {
}
void Restore() noexcept {
#if defined(__x86_64__) || defined(__aarch64__)
#if defined(__x86_64__)
if (active_) {
// Restore only our two bits, preserving rounding, exception masks and
// status flags set by the guest while the thread was attached.
#if defined(__x86_64__)
_mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_);
#else
std::uint64_t control;
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
__asm__ volatile("msr fpcr, %0" ::"r"((control & ~kModeMask) |
original_mode_));
#endif
active_ = false;
}
#endif
@@ -73,13 +56,6 @@ class FmodThreadFloatingPointMode final {
static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6);
unsigned int original_mode_ = 0;
bool active_ = false;
#elif defined(__aarch64__)
// FPCR FZ flushes single/double subnormals; FZ16 covers the half-precision
// path used by some voice DSP filters.
static constexpr std::uint64_t kModeMask =
(std::uint64_t{1} << 24) | (std::uint64_t{1} << 19);
std::uint64_t original_mode_ = 0;
bool active_ = false;
#endif
};
-7
View File
@@ -19,13 +19,6 @@ inline constexpr std::size_t kMaximumRobloxLaunchUriBytes = 64 * 1024;
Status ParseRobloxLaunchUri(std::string_view uri,
RobloxExperienceLaunchRequest* request);
// Converts a WebView launchGame command
// ({"functionName":"launchGame","params":{"request":{...}}}) into the same
// contract. Returns kUnsupported for other commands and kInvalidArgument for
// a malformed launchGame request.
Status ParseRobloxWebViewLaunchGame(std::string_view command,
RobloxExperienceLaunchRequest* request);
} // namespace runtime
} // namespace mocktail
+1 -1
View File
@@ -14,7 +14,7 @@ struct ActivePayloadPaths {
std::filesystem::path root;
std::filesystem::path roblox_library;
std::filesystem::path base_apk;
std::filesystem::path arch_split_apk;
std::filesystem::path x86_64_split_apk;
std::filesystem::path assets_content;
std::filesystem::path compatibility_manifest;
std::filesystem::path host_abi_profile;
@@ -1,10 +0,0 @@
# Allowed reduced aarch64 packages from pkgforge-dev/archlinux-pkgs-debloated.
# The installer verifies the rolling release asset IDs and SHA256 digests.
ffmpeg-mini-aarch64.pkg.tar.zst
gdk-pixbuf2-mini-aarch64.pkg.tar.xz
gtk4-mini-aarch64.pkg.tar.zst
icu-mini-aarch64.pkg.tar.zst
librsvg-mini-aarch64.pkg.tar.zst
libxml2-mini-aarch64.pkg.tar.zst
mesa-mini-aarch64.pkg.tar.zst
opus-mini-aarch64.pkg.tar.zst
+10 -14
View File
@@ -6,11 +6,10 @@ set -Eeuo pipefail
umask 022
readonly REPOSITORY="${1:?Flatpak repository path is required}"
readonly X86_64_BUNDLE="${2:?x86_64 Flatpak bundle path is required}"
readonly AARCH64_BUNDLE="${3:?aarch64 Flatpak bundle path is required}"
readonly PUBLIC_KEY="${4:?GPG public key path is required}"
readonly OUTPUT="${5:?Pages output path is required}"
readonly NATIVE_REPOSITORIES="${6:?Native repositories path is required}"
readonly BUNDLE="${2:?Flatpak bundle path is required}"
readonly PUBLIC_KEY="${3:?GPG public key path is required}"
readonly OUTPUT="${4:?Pages output path is required}"
readonly NATIVE_REPOSITORIES="${5:?Native repositories path is required}"
readonly BASE_URL="https://mocktail.bigrat.space"
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
@@ -19,12 +18,10 @@ readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
printf 'Flatpak repository is missing or unsafe: %s\n' "${REPOSITORY}" >&2
exit 1
}
for bundle in "${X86_64_BUNDLE}" "${AARCH64_BUNDLE}"; do
[[ -f "${bundle}" && ! -L "${bundle}" ]] || {
printf 'Flatpak bundle is missing or unsafe: %s\n' "${bundle}" >&2
exit 1
}
done
[[ -f "${BUNDLE}" && ! -L "${BUNDLE}" ]] || {
printf 'Flatpak bundle is missing or unsafe: %s\n' "${BUNDLE}" >&2
exit 1
}
[[ -s "${PUBLIC_KEY}" && ! -L "${PUBLIC_KEY}" ]] || {
printf 'Flatpak public key is missing or unsafe: %s\n' "${PUBLIC_KEY}" >&2
exit 1
@@ -43,8 +40,7 @@ done
mkdir -p -- "${OUTPUT}"
cp -a -- "${REPOSITORY}" "${OUTPUT}/repo"
install -m 0644 -- "${X86_64_BUNDLE}" "${OUTPUT}/Mocktail-x86_64.flatpak"
install -m 0644 -- "${AARCH64_BUNDLE}" "${OUTPUT}/Mocktail-aarch64.flatpak"
install -m 0644 -- "${BUNDLE}" "${OUTPUT}/Mocktail-x86_64.flatpak"
install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/mocktail-flatpak.gpg"
install -m 0644 -- \
"${PROJECT_DIR}/packaging/space.bigrat.mocktail.svg" \
@@ -73,7 +69,7 @@ Title=Mocktail Nightly
Url=${BASE_URL}/repo/
Homepage=https://github.com/komaruworld/mocktail
Comment=Nightly builds of Mocktail
Description=Signed x86_64 and aarch64 nightly builds published from the latest Mocktail main branch
Description=Signed x86_64 nightly builds published from the latest Mocktail main branch
Icon=${BASE_URL}/mocktail.svg
GPGKey=${GPG_KEY}
EOF
-2
View File
@@ -461,7 +461,6 @@ LatestSupportedBootstrapProfile() {
.version_code <= ($remote_version_code | tonumber)) and
.status == "supported" and .default_allowed == true and
.allow_legacy_binary_patches == false and
(.abi // "x86_64") == "x86_64" and
(.version_name | type) == "string" and
(.version_name | length) > 0)] |
group_by(.version_code) |
@@ -496,7 +495,6 @@ SelectSupportedBootstrapFallback() {
bootstrap_source="$(jq -er --argjson version_code "${fallback_version_code}" '
[.sources[]? |
select(.version_code == $version_code and
(.abi // "x86_64") == "x86_64" and
.provider == "uptodown")] |
if length == 1 then .[0].provider else empty end
' "${BOOTSTRAP_SOURCES_PATH}" 2>/dev/null || true)"
+1 -2
View File
@@ -329,7 +329,6 @@ def run_trial(args, max_ctors, skipped_offsets, trial_id):
log_path = args.log_dir / f"trial_{trial_id:03d}_max_{max_ctors}.log"
env = os.environ.copy()
env.update({
"ROBLOX_LIB_PATH": str(args.libroblox),
"MOCKTAIL_RUN_LIBROBLOX_CTORS": "1",
"MOCKTAIL_LIBROBLOX_CTOR_POLICY": args.ctor_policy,
"MOCKTAIL_GRAPHICS_BACKEND": args.graphics_backend,
@@ -501,7 +500,7 @@ def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--project-root", type=Path, default=project_root)
parser.add_argument("--run-script", type=Path,
default=project_root / "build" / "mocktail")
default=project_root / "scripts" / "run_sober.sh")
parser.add_argument("--libroblox", type=Path,
default=project_root / "rbx_bin" / "libroblox.so")
parser.add_argument("--log-dir", type=Path,
+4 -7
View File
@@ -15,7 +15,7 @@ Usage() {
cat <<'EOF'
Usage: scripts/build_flatpak.sh [OPTIONS]
Build and install Mocktail as a per-user Flatpak for the host architecture.
Build and install Mocktail as a per-user x86-64 Flatpak.
Options:
--build-dir DIR Builder output directory (default: build-flatpak).
@@ -92,11 +92,8 @@ done
[[ "${JOBS}" =~ ^[1-9][0-9]*$ && "${JOBS}" -le 256 ]] ||
Die "--jobs must be an integer between 1 and 256"
HOST_ARCH="$(uname -m)"
case "${HOST_ARCH}" in
x86_64|aarch64) ;;
*) Die "Flatpak builds require an x86_64 or aarch64 Linux host" ;;
esac
[[ "$(uname -m)" == x86_64 ]] ||
Die "the Roblox payload and Flatpak manifest currently require x86-64"
command -v flatpak >/dev/null 2>&1 || Die "flatpak is required"
MANIFEST="$(realpath -e -- "${MANIFEST}")" ||
@@ -125,7 +122,7 @@ CleanupStaleBuilderMounts
cd -- "${PROJECT_ROOT}"
exec "${builder[@]}" \
--arch="${HOST_ARCH}" \
--arch=x86_64 \
--jobs="${JOBS}" \
--force-clean \
--install-deps-from=flathub \
-1
View File
@@ -61,7 +61,6 @@ def supported_profiles(catalog: dict) -> list[dict]:
if isinstance(profile, dict)
and profile.get("status") == "supported"
and profile.get("default_allowed") is True
and profile.get("abi", ARCHITECTURE) == ARCHITECTURE
and isinstance(profile.get("version_name"), str)
and isinstance(profile.get("version_code"), int)
]
+3 -12
View File
@@ -18,15 +18,12 @@ AnyLinuxDependencyRequireContainer() {
}
AnyLinuxDependencySnapshot() {
local manifest="$1" release="$2" host_arch="${3:-$(uname -m)}"
local filename remainder names_json
[[ "${host_arch}" == x86_64 || "${host_arch}" == aarch64 ]] ||
AnyLinuxDependencyDie 'unsupported AppImage architecture'
local manifest="$1" release="$2" filename remainder names_json
local -a names=()
local -A seen=()
while read -r filename remainder || [[ -n "${filename}" ]]; do
[[ -n "${filename}" && "${filename}" != \#* ]] || continue
[[ "${filename}" =~ ^[a-z0-9-]+-mini-${host_arch}\.pkg\.tar\.(zst|xz)$ &&
[[ "${filename}" =~ ^[a-z0-9-]+-mini-x86_64\.pkg\.tar\.zst$ &&
-z "${remainder}" && -z "${seen[${filename}]:-}" ]] ||
AnyLinuxDependencyDie 'invalid or duplicate dependency manifest entry'
seen["${filename}"]=1
@@ -111,13 +108,7 @@ AnyLinuxDependenciesMain() (
AnyLinuxDependencyRequireContainer
(( $# == 0 )) || AnyLinuxDependencyDie 'no arguments are supported'
readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
local host_arch="$(uname -m)" manifest_name
case "${host_arch}" in
x86_64) manifest_name=anylinux-dependencies.txt ;;
aarch64) manifest_name=anylinux-dependencies-aarch64.txt ;;
*) AnyLinuxDependencyDie "unsupported AppImage architecture: ${host_arch}" ;;
esac
readonly MANIFEST="${ROOT}/packaging/${manifest_name}"
readonly MANIFEST="${ROOT}/packaging/anylinux-dependencies.txt"
readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")"
trap 'rm -rf -- "${DOWNLOADS}"' EXIT
AnyLinuxDownloadDependencies "${MANIFEST}" "${DOWNLOADS}"
+2 -8
View File
@@ -110,14 +110,8 @@ for elf in "$@"; do
fi
machine="$(LC_ALL=C readelf --file-header "${elf}" 2>/dev/null |
sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')"
case "$(uname -m)" in
x86_64) expected_machine='X86-64' ;;
aarch64) expected_machine='AArch64' ;;
*) expected_machine='X86-64' ;;
esac
if [[ "${machine}" != *"${expected_machine}"* ]]; then
printf 'ABI check: %s is not %s (%s)\n' "${elf}" "${expected_machine}" \
"${machine}" >&2
if [[ "${machine}" != *X86-64* ]]; then
printf 'ABI check: %s is not x86-64 (%s)\n' "${elf}" "${machine}" >&2
status=1
fi
+3 -8
View File
@@ -10,7 +10,7 @@
<body><main>
<img src="../packaging/space.bigrat.mocktail.svg" alt="Mocktail logo">
<h1>Mocktail</h1>
<p>Play Roblox on Linux. Use the stable Flathub release, or try the latest nightly build from main. Nightly Flatpaks are available for x86_64 and aarch64; Roblox runtime support on aarch64 is experimental.</p>
<p>Play Roblox on Linux. Use the stable Flathub release, or try the latest nightly build from main.</p>
<a class="button" href="https://flathub.org/apps/space.bigrat.mocktail">Stable on Flathub</a>
<a class="button secondary" href="https://mocktail.bigrat.space/mocktail.flatpakref">Nightly build</a>
<div class="commands">
@@ -19,10 +19,6 @@
<p>Nightly</p>
<code>flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref</code>
</div>
<p class="links">
<a href="Mocktail-x86_64.flatpak">x86_64 nightly Flatpak</a>
<a href="Mocktail-aarch64.flatpak">aarch64 nightly Flatpak</a>
</p>
<h2>AUR packages</h2>
<p>Available from the AUR. Choose one package:</p>
<p class="links">
@@ -55,7 +51,7 @@ sudo apt update</code>
<code>sudo apt install mocktail-nightly</code>
</div>
<h2>Direct downloads</h2>
<p>Prebuilt Linux packages. DEB and RPM are for x86_64; the nightly AppImage also has an aarch64 build.</p>
<p>Prebuilt packages for Linux x86_64.</p>
<p class="download-label">Stable</p>
<p class="links">
<a href="https://github.com/komaruworld/mocktail/releases/latest/download/Mocktail-x86_64.AppImage">AppImage</a>
@@ -64,8 +60,7 @@ sudo apt update</code>
</p>
<p class="download-label">Nightly</p>
<p class="links">
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.AppImage">AppImage (x86_64)</a>
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly-aarch64.AppImage">AppImage (aarch64)</a>
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.AppImage">AppImage</a>
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.deb">DEB</a>
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.rpm">RPM</a>
</p>
+2 -16
View File
@@ -16,8 +16,6 @@
#include <cstdlib>
#include <cstring>
#include "compat/bionic_pthread_create_runtime.h"
#include "compat/guest_abi.h"
#include "compat/http_client_spin_guard.h"
#include "window/window.h"
@@ -33,14 +31,6 @@ const char* EnvironmentOr(const char* name, const char* fallback) {
return value != nullptr && value[0] != '\0' ? value : fallback;
}
const char* GuestAbiPropertyValue() {
#if defined(__aarch64__)
return "arm64-v8a";
#else
return "x86_64";
#endif
}
int CallHostVsnprintf(char* dst, size_t count, const char* format,
va_list args) {
using HostVsnprintf = int (*)(char*, size_t, const char*, va_list);
@@ -183,12 +173,8 @@ int __system_property_get(const char* name, char* value) {
result = "35";
} else if (std::strcmp(name, "ro.build.version.release") == 0) {
result = "15";
} else if (std::strcmp(name, "ro.product.cpu.abi") == 0 ||
std::strcmp(name, "ro.product.cpu.abilist") == 0 ||
std::strcmp(name, "ro.product.cpu.abilist64") == 0) {
result = GuestAbiPropertyValue();
} else if (std::strcmp(name, "ro.product.cpu.abilist32") == 0) {
result = "";
} else if (std::strcmp(name, "ro.product.cpu.abi") == 0) {
result = "x86_64";
} else if (std::strcmp(name, "ro.product.manufacturer") == 0) {
result = EnvironmentOr("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail");
} else if (std::strcmp(name, "ro.product.model") == 0) {
+70 -166
View File
@@ -1,14 +1,10 @@
#include "compat/bionic_pthread_create_runtime.h"
#include "compat/bionic_abi_exports.h"
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <algorithm>
#include <atomic>
#include <cstring>
#include <limits>
#include <new>
@@ -73,28 +69,24 @@ size_t HostStackSizeForGuest(size_t guest_stack_size) noexcept {
return guest_stack_size;
}
// Guest pthread_attr_t buffers use the Bionic LP64 layout; they are never a
// host pthread_attr_t, so decode the fixed fields directly.
int GuestDetachState(const MocktailBionicPthreadAttr& source) noexcept {
return (source.flags & kMocktailBionicAttrFlagDetached) != 0
? PTHREAD_CREATE_DETACHED
: PTHREAD_CREATE_JOINABLE;
}
size_t GuestStackSize(const MocktailBionicPthreadAttr& source) noexcept {
return source.stack_size != 0 ? source.stack_size
: kBionicLp64DefaultThreadStackSize;
}
int CopySupportedThreadAttributes(const MocktailBionicPthreadAttr& source,
int CopySupportedThreadAttributes(const pthread_attr_t& source,
pthread_attr_t* destination) noexcept {
int result =
pthread_attr_setdetachstate(destination, GuestDetachState(source));
int detach_state = PTHREAD_CREATE_JOINABLE;
int result = pthread_attr_getdetachstate(&source, &detach_state);
if (result != 0) {
return result;
}
result = pthread_attr_setdetachstate(destination, detach_state);
if (result != 0) {
return result;
}
result = pthread_attr_setguardsize(destination, source.guard_size);
size_t guard_size = 0;
result = pthread_attr_getguardsize(&source, &guard_size);
if (result != 0) {
return result;
}
result = pthread_attr_setguardsize(destination, guard_size);
if (result != 0) {
return result;
}
@@ -103,38 +95,53 @@ int CopySupportedThreadAttributes(const MocktailBionicPthreadAttr& source,
// pthread_attr_setstack. Copying pthread_attr_getstack's address is unsafe:
// glibc represents an automatically allocated stack with a synthetic
// address derived from its size.
result = pthread_attr_setstacksize(
destination, HostStackSizeForGuest(GuestStackSize(source)));
size_t stack_size = 0;
result = pthread_attr_getstacksize(&source, &stack_size);
if (result != 0) {
return result;
}
result = pthread_attr_setstacksize(destination,
HostStackSizeForGuest(stack_size));
if (result != 0) {
return result;
}
// Bionic applies the attribute's scheduling policy at creation whenever it
// differs from the normal policy.
if (RejectRealtimeScheduling(source.sched_policy)) {
return EPERM;
int inherit_scheduler = PTHREAD_INHERIT_SCHED;
result = pthread_attr_getinheritsched(&source, &inherit_scheduler);
if (result != 0) {
return result;
}
if (source.sched_policy != SCHED_NORMAL) {
result = pthread_attr_setschedpolicy(destination, source.sched_policy);
if (result != 0) {
return result;
}
if (inherit_scheduler == PTHREAD_EXPLICIT_SCHED) {
int scheduler_policy = 0;
sched_param scheduler_parameters{};
scheduler_parameters.sched_priority = source.sched_priority;
result = pthread_attr_setschedparam(destination, &scheduler_parameters);
result = pthread_attr_getschedpolicy(&source, &scheduler_policy);
if (result == 0) {
result = pthread_attr_getschedparam(&source, &scheduler_parameters);
}
if (result == 0 && RejectRealtimeScheduling(scheduler_policy)) {
return EPERM;
}
if (result == 0) {
result = pthread_attr_setschedpolicy(destination, scheduler_policy);
}
if (result == 0) {
result = pthread_attr_setschedparam(destination, &scheduler_parameters);
}
if (result != 0) {
return result;
}
return pthread_attr_setinheritsched(destination, PTHREAD_EXPLICIT_SCHED);
}
return pthread_attr_setinheritsched(destination, PTHREAD_INHERIT_SCHED);
return pthread_attr_setinheritsched(destination, inherit_scheduler);
}
int CopyRequiredThreadAttributes(const MocktailBionicPthreadAttr& source,
int CopyRequiredThreadAttributes(const pthread_attr_t& source,
pthread_attr_t* destination) noexcept {
int result =
pthread_attr_setdetachstate(destination, GuestDetachState(source));
int detach_state = PTHREAD_CREATE_JOINABLE;
int result = pthread_attr_getdetachstate(&source, &detach_state);
if (result != 0) {
return result;
}
result = pthread_attr_setdetachstate(destination, detach_state);
if (result != 0) {
return result;
}
@@ -142,20 +149,32 @@ int CopyRequiredThreadAttributes(const MocktailBionicPthreadAttr& source,
// The retry must retain an explicitly requested guest stack. Falling back
// to a null/default host attr is unsafe on musl, whose default stack is much
// smaller than Bionic's and can be exhausted by a single libroblox frame.
return pthread_attr_setstacksize(
destination, HostStackSizeForGuest(GuestStackSize(source)));
size_t stack_size = 0;
result = pthread_attr_getstacksize(&source, &stack_size);
if (result != 0) {
return result;
}
return pthread_attr_setstacksize(destination,
HostStackSizeForGuest(stack_size));
}
int ConfigureHostSafeStackFallback(const MocktailBionicPthreadAttr* source,
int ConfigureHostSafeStackFallback(const pthread_attr_t* source,
pthread_attr_t* destination) noexcept {
size_t guest_stack_size = 0;
if (source != nullptr) {
int result =
pthread_attr_setdetachstate(destination, GuestDetachState(*source));
int detach_state = PTHREAD_CREATE_JOINABLE;
int result = pthread_attr_getdetachstate(source, &detach_state);
if (result != 0) {
return result;
}
result = pthread_attr_setdetachstate(destination, detach_state);
if (result != 0) {
return result;
}
result = pthread_attr_getstacksize(source, &guest_stack_size);
if (result != 0) {
return result;
}
guest_stack_size = GuestStackSize(*source);
}
size_t host_default_stack_size = 0;
@@ -188,8 +207,7 @@ void ConfigureBionicPthreadThreadInitializer(
g_thread_initializer.store(initializer, std::memory_order_release);
}
int CreateBionicPthread(pthread_t* thread,
const MocktailBionicPthreadAttr* attr,
int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr,
void* (*start_routine)(void*), void* argument) {
if (thread == nullptr || start_routine == nullptr) {
return EINVAL;
@@ -271,124 +289,10 @@ int CreateBionicPthread(pthread_t* thread,
} // namespace mocktail::compat
extern "C" {
int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr) {
if (attr == nullptr) {
return EINVAL;
}
attr->flags = 0;
attr->stack_base = nullptr;
attr->stack_size =
mocktail::compat::kBionicLp64DefaultThreadStackSize;
const long page_size = sysconf(_SC_PAGESIZE);
attr->guard_size = page_size > 0 ? static_cast<size_t>(page_size) : 0;
attr->sched_policy = SCHED_OTHER;
attr->sched_priority = 0;
std::memset(attr->reserved, 0, sizeof(attr->reserved));
return 0;
}
int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr) {
(void)attr;
return 0;
}
int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr,
size_t stack_size) {
if (attr == nullptr || stack_size < PTHREAD_STACK_MIN) {
return EINVAL;
}
attr->stack_size = stack_size;
attr->flags &= ~kMocktailBionicAttrFlagUserStack;
return 0;
}
int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr,
int detach_state) {
if (attr == nullptr || (detach_state != PTHREAD_CREATE_DETACHED &&
detach_state != PTHREAD_CREATE_JOINABLE)) {
return EINVAL;
}
if (detach_state == PTHREAD_CREATE_DETACHED) {
attr->flags |= kMocktailBionicAttrFlagDetached;
} else {
attr->flags &= ~kMocktailBionicAttrFlagDetached;
}
return 0;
}
int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr,
const struct sched_param* parameters) {
if (attr == nullptr || parameters == nullptr) {
return EINVAL;
}
attr->sched_priority = parameters->sched_priority;
return 0;
}
int mocktail_pthread_getattr_np(pthread_t thread,
MocktailBionicPthreadAttr* attr) {
if (attr == nullptr) {
return EINVAL;
}
pthread_attr_t host_attr;
int result = pthread_getattr_np(thread, &host_attr);
if (result != 0) {
return result;
}
void* stack_base = nullptr;
size_t stack_size = 0;
result = pthread_attr_getstack(&host_attr, &stack_base, &stack_size);
size_t guard_size = 0;
if (result == 0) {
result = pthread_attr_getguardsize(&host_attr, &guard_size);
}
int detach_state = PTHREAD_CREATE_JOINABLE;
if (result == 0) {
result = pthread_attr_getdetachstate(&host_attr, &detach_state);
}
sched_param parameters{};
int policy = SCHED_OTHER;
if (result == 0) {
result = pthread_getschedparam(thread, &policy, &parameters);
}
pthread_attr_destroy(&host_attr);
if (result != 0) {
return result;
}
attr->flags = detach_state == PTHREAD_CREATE_DETACHED
? kMocktailBionicAttrFlagDetached
: 0;
attr->stack_base = stack_base;
attr->stack_size = stack_size;
attr->guard_size = guard_size;
attr->sched_policy = policy;
attr->sched_priority = parameters.sched_priority;
std::memset(attr->reserved, 0, sizeof(attr->reserved));
return 0;
}
int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr,
void** stack_base, size_t* stack_size) {
if (attr == nullptr) {
return EINVAL;
}
if (stack_base != nullptr) {
*stack_base = attr->stack_base;
}
if (stack_size != nullptr) {
*stack_size = attr->stack_size;
}
return 0;
}
} // extern "C"
extern "C" int mocktail_bionic_pthread_create(
pthread_t* thread, const MocktailBionicPthreadAttr* attr,
void* (*start_routine)(void*), void* argument) {
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
const pthread_attr_t* attr,
void* (*start_routine)(void*),
void* argument) {
return mocktail::compat::CreateBionicPthread(thread, attr, start_routine,
argument);
}
-11
View File
@@ -39,23 +39,12 @@ bool PatchAbsoluteJump(uintptr_t address, void *target) noexcept {
return false;
}
#if defined(__aarch64__)
// ldr x16, #8 ; br x16 ; .quad target
// x16 is the intra-procedure-call scratch register, free to clobber.
unsigned char patch[] = {
0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6,
0, 0, 0, 0, 0, 0, 0, 0,
};
const uintptr_t target_address = reinterpret_cast<uintptr_t>(target);
std::memcpy(patch + 8, &target_address, sizeof(target_address));
#else
unsigned char patch[] = {
0x48, 0xb8, // movabs rax, imm64
0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0, // jmp rax
};
const uintptr_t target_address = reinterpret_cast<uintptr_t>(target);
std::memcpy(patch + 2, &target_address, sizeof(target_address));
#endif
const uintptr_t page_mask = static_cast<uintptr_t>(page_size) - 1;
const uintptr_t page = address & ~page_mask;
-28
View File
@@ -6,34 +6,6 @@ namespace mocktail::compat {
namespace {
constexpr HostAbiProfile kProfiles[] = {
// Roblox 2.736.1408 arm64-v8a. Every RVA was derived from the AArch64
// payload of the same release as the x86-64 reference above, matched by
// allocator internals, init_array call graphs, and static state layout.
// Constructor policy mirrors x86-64 by semantic role: the pre-init
// entries 0..3 stay skipped, index 4 is mi_process_load (thread init
// checkpoint), index 5 is allocator wiring and index 6 is the lazy clock
// singleton getter, both excluded under host bridges only.
{"ed2b035768b1513561d0e6ea7f9bbec35b0e60e5",
{{{0x1d53840, HostBridgeKind::kAllocate, "small-allocate"},
{0x1d56c34, HostBridgeKind::kUsableSize, "usable-size"},
{0x1d6ef80, HostBridgeKind::kReallocate, "reallocate"},
{0x1d53568, HostBridgeKind::kAllocate, "allocate"},
{0x61d3850, HostBridgeKind::kAlignedAllocate,
"aligned-allocate-direct"},
{0x1d57328, HostBridgeKind::kFree, "free"}}},
6,
{0, 0, 0x7296c88, 0x400, 0x1d53c54, 0x1dc76a0,
0x71d6c90, 0x71d2c40, 0x400000},
{0x1d53568, 0x1d57328},
0x6733128,
3565,
{{{4, 5}, {7, 3565}}},
2,
{{{4, 3565}}},
1,
4,
{0x2132c5c, 0x71d6d30},
HostAllocatorStrategy::kNativeMimalloc},
// Roblox 2.736.1408. Derived from the verified 2908 profile and validated
// by two isolated no-recovery Vulkan canaries and real game sessions.
{"ade08266c67aee88ec9c1d00902150e1684dad3a",
+10 -19
View File
@@ -32,7 +32,6 @@
#include <openssl/evp.h>
#include "compat/elf_build_id.h"
#include "compat/guest_abi.h"
namespace mocktail::compat {
namespace {
@@ -612,26 +611,19 @@ std::unique_ptr<OwnedHostAbiProfile> ParseProfile(const Json& root,
*error = "external host ABI profile has inconsistent constructor policy";
return nullptr;
}
// The verified constructor layout is the same on both guest ABIs: a single
// process-load constructor at the thread-initializer checkpoint, two wiring
// entries excluded under host bridges, and one contiguous native-mimalloc
// replay from the checkpoint to the end of .init_array. Only the checkpoint
// index itself differs between payloads.
const std::size_t checkpoint =
owned->profile.native_mimalloc_thread_initializer_after_constructor;
if (owned->profile.init_array_count <= checkpoint + 3 ||
if (owned->profile.init_array_count <= 5 ||
owned->profile.constructor_run_range_count != 2 ||
owned->profile.constructor_run_ranges[0].begin != checkpoint ||
owned->profile.constructor_run_ranges[0].end_exclusive != checkpoint + 1 ||
owned->profile.constructor_run_ranges[1].begin != checkpoint + 3 ||
owned->profile.constructor_run_ranges[0].begin != 2 ||
owned->profile.constructor_run_ranges[0].end_exclusive != 3 ||
owned->profile.constructor_run_ranges[1].begin != 5 ||
owned->profile.constructor_run_ranges[1].end_exclusive !=
owned->profile.init_array_count ||
owned->profile.native_mimalloc_constructor_run_range_count != 1 ||
owned->profile.native_mimalloc_constructor_run_ranges[0].begin !=
checkpoint ||
owned->profile.native_mimalloc_constructor_run_ranges[0].begin != 2 ||
owned->profile.native_mimalloc_constructor_run_ranges[0].end_exclusive !=
owned->profile.init_array_count ||
(kGuestElfMachine == EM_X86_64 && checkpoint != 2)) {
owned->profile.native_mimalloc_thread_initializer_after_constructor !=
2) {
*error =
"external host ABI profile changed the verified constructor policy";
return nullptr;
@@ -677,10 +669,9 @@ bool ValidateProfileAgainstElf(const std::string& payload_path,
}
GElf_Ehdr header{};
if (gelf_getehdr(elf.get(), &header) == nullptr ||
gelf_getclass(elf.get()) != ELFCLASS64 ||
header.e_machine != kGuestElfMachine || header.e_type != ET_DYN) {
*error = "payload must be a " + std::string(kGuestAbi) +
" ET_DYN ELF object";
gelf_getclass(elf.get()) != ELFCLASS64 || header.e_machine != EM_X86_64 ||
header.e_type != ET_DYN) {
*error = "payload must be an x86-64 ET_DYN ELF object";
return false;
}
+8 -53
View File
@@ -36,9 +36,7 @@
#include <fcntl.h>
#include <unistd.h>
#include <sys/prctl.h>
#if defined(__x86_64__)
#include <asm/prctl.h>
#endif
#include <sys/syscall.h>
#include <vector>
@@ -51,7 +49,6 @@
#include "compat/bionic_socket_runtime.h"
#include "compat/build_profile.h"
#include "compat/elf_build_id.h"
#include "compat/guest_abi.h"
#include "compat/host_abi_experiment.h"
#include "compat/host_abi_profile.h"
#include "compat/host_allocator_bridge.h"
@@ -1009,7 +1006,7 @@ struct BionicAddrInfo {
};
static_assert(sizeof(BionicAddrInfo) == 48,
"unexpected bionic addrinfo size");
"unexpected x86_64 bionic addrinfo size");
bool DnsTraceEnabled() {
return IsEnabled("MOCKTAIL_DNS_TRACE");
@@ -1374,31 +1371,15 @@ bool IsUnsafeSoftTimeoutModule(void* rip) {
void JniOnLoadTimeoutAlarm(int, siginfo_t* info, void* context) {
static_cast<void>(info);
auto* uc = static_cast<ucontext_t*>(context);
#if defined(__x86_64__)
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]) : 0;
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RSP]) : 0;
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]) : 0;
auto rax = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RAX]) : 0;
const char* regs_format =
" [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx "
"RAX=0x%016llx\n";
#elif defined(__aarch64__)
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.pc) : 0;
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.sp) : 0;
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[29]) : 0;
auto rax = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[0]) : 0;
const char* regs_format =
" [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx "
"X0=0x%016llx\n";
#else
uintptr_t rip = 0, rsp = 0, rbp = 0, rax = 0;
const char* regs_format =
" [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx "
"X0=0x%016llx\n";
#endif
char regs_msg[192];
int len = std::snprintf(
regs_msg, sizeof(regs_msg), regs_format,
regs_msg, sizeof(regs_msg),
" [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx "
"RAX=0x%016llx\n",
static_cast<unsigned long long>(rip), static_cast<unsigned long long>(rsp),
static_cast<unsigned long long>(rbp), static_cast<unsigned long long>(rax));
write(2, regs_msg, static_cast<size_t>(len));
@@ -2441,12 +2422,10 @@ jobject BuildDeviceParams(JNIEnv* env) {
GetEnvString("MOCKTAIL_DEVICE_NAME", "Mocktail Linux");
const std::string manufacturer =
GetEnvString("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail");
const std::string device_sku = GetEnvString(
"MOCKTAIL_DEVICE_SKU",
("mocktail-" + std::string(mocktail::compat::kGuestCpuName)).c_str());
const std::string device_sku =
GetEnvString("MOCKTAIL_DEVICE_SKU", "mocktail-x86_64");
const std::string soc_model =
GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL",
std::string(mocktail::compat::kGuestCpuName).c_str());
GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL", "x86_64");
SetStringField(env, params, "osVersion", "33");
SetStringField(env, params, "deviceName", device_name.c_str());
const std::string app_version =
@@ -4642,14 +4621,12 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
// Roblox internal threads trigger SI_KERNEL traps from CET shadow-stack
// return mismatches. Unsupported kernels ignore this request.
#if defined(__x86_64__)
{
long r = syscall(SYS_arch_prctl, ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK);
if (r == 0) {
std::cout << " [cet] shadow-stack (SHSTK) disabled\n";
}
}
#endif
if (build_profile.allow_legacy_binary_patches) {
std::cerr
@@ -5568,27 +5545,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
reinterpret_cast<void*>(mocktail_pthread_spin_trylock));
linker::RegisterSymbol("pthread_spin_unlock",
reinterpret_cast<void*>(mocktail_pthread_spin_unlock));
linker::RegisterSymbol(
"pthread_attr_init",
reinterpret_cast<void*>(mocktail_pthread_attr_init));
linker::RegisterSymbol(
"pthread_attr_destroy",
reinterpret_cast<void*>(mocktail_pthread_attr_destroy));
linker::RegisterSymbol(
"pthread_attr_setstacksize",
reinterpret_cast<void*>(mocktail_pthread_attr_setstacksize));
linker::RegisterSymbol(
"pthread_attr_setdetachstate",
reinterpret_cast<void*>(mocktail_pthread_attr_setdetachstate));
linker::RegisterSymbol(
"pthread_attr_setschedparam",
reinterpret_cast<void*>(mocktail_pthread_attr_setschedparam));
linker::RegisterSymbol(
"pthread_getattr_np",
reinterpret_cast<void*>(mocktail_pthread_getattr_np));
linker::RegisterSymbol(
"pthread_attr_getstack",
reinterpret_cast<void*>(mocktail_pthread_attr_getstack));
linker::RegisterSymbol("pthread_barrier_init",
reinterpret_cast<void*>(mocktail_pthread_barrier_init));
linker::RegisterSymbol(
@@ -5708,8 +5664,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
if (roblox_handle == nullptr) {
std::cerr << "\n[FATAL] Could not load '" << library_path << "'.\n"
<< " Extract lib/" << compat::kGuestAbi
<< "/libroblox.so from a Roblox APK and\n"
<< " Extract lib/x86_64/libroblox.so from a Roblox APK and\n"
<< " place it at the path above (or set ROBLOX_LIB_PATH).\n";
return EXIT_FAILURE;
}
+9 -4
View File
@@ -1,5 +1,7 @@
#include "mocktail/platform/sdl_event_converter.h"
#include <SDL3/SDL_mouse.h>
#include <cstdint>
namespace mocktail {
@@ -69,11 +71,14 @@ bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
source.button.x, source.button.y};
return true;
case SDL_EVENT_MOUSE_WHEEL: {
// SDL already applies the host's natural-scrolling preference to x/y.
// Undoing SDL_MOUSEWHEEL_FLIPPED here would ignore the OS setting.
float delta_x = source.wheel.x;
float delta_y = source.wheel.y;
if (source.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
delta_x = -delta_x;
delta_y = -delta_y;
}
destination->payload = MouseWheelEvent{
source.wheel.x, source.wheel.y, source.wheel.mouse_x,
source.wheel.mouse_y};
delta_x, delta_y, source.wheel.mouse_x, source.wheel.mouse_y};
return true;
}
case SDL_EVENT_FINGER_DOWN:
+3 -5
View File
@@ -5,7 +5,6 @@
#include <string>
#include <string_view>
#include "compat/guest_abi.h"
#include "runtime/roblox_launch_uri.h"
namespace mocktail {
@@ -242,12 +241,11 @@ std::string CommandLineUsage(const std::string& program_name) {
usage
<< "Usage: " << (program_name.empty() ? "mocktail" : program_name)
<< " [options] [roblox-uri]\n\n"
<< "Normal startup downloads and activates a verified managed "
<< compat::kGuestAbi
<< " Roblox payload automatically.\n\n"
<< "Normal startup downloads and activates a verified managed x86_64 "
"Roblox payload automatically.\n\n"
<< "Options:\n"
<< " --roblox-lib <path> Development override for a specific "
<< compat::kGuestAbi << " libroblox.so\n"
"x86_64 libroblox.so\n"
<< " --headless Run without creating an SDL window\n"
<< " --windowed Force windowed startup (default)\n"
<< " --graphics <backend> direct-vulkan | opengl | system | "
-9
View File
@@ -27,21 +27,12 @@ constexpr const char* kIcdDirectories[] = {
"/usr/local/share/vulkan/icd.d",
};
#if defined(__aarch64__)
constexpr char kNativeArchitecture[] = "aarch64";
constexpr const char* kForeignArchitectures[] = {
"i686", "i586", "i486", "i386", "x86_64", "x86.",
"amd64", "armhf", "armv7", "ppc64", "riscv64", "s390x",
};
#else
constexpr char kNativeArchitecture[] = "x86_64";
constexpr const char* kForeignArchitectures[] = {
"i686", "i586", "i486", "i386", "x86.", "aarch64",
"arm64", "armhf", "armv7", "ppc64", "riscv64", "s390x",
};
#endif
struct HostGpus {
bool intel = false;
+3 -18
View File
@@ -11,7 +11,6 @@
#include "jnivm/jnivm.h"
#include "runtime/external_launch_broker.h"
#include "runtime/roblox_launch_uri.h"
#include "runtime/webview_helper_launcher.h"
#include "window/window.h"
@@ -952,23 +951,9 @@ Status RobloxExperienceComposition::RouteCurrentWebSurfaceEvent(
route = web_surface_route_;
web_view_bridge = web_view_bridge_.get();
}
if (event.type == WebViewHelperEventType::kRobloxCookie) {
return AcceptWebViewRobloxCookie(event.payload);
}
if (event.type == WebViewHelperEventType::kExecuteRoblox) {
// Roblox defers a signalled launchGame until the Lua app next navigates.
// A parsed launchGame is launched here and never forwarded, so a deferred
// copy cannot start a second join later.
RobloxExperienceLaunchRequest request;
if (ParseRobloxWebViewLaunchGame(event.payload, &request).ok() &&
Dispatch(request).ok()) {
std::fprintf(stderr,
"[experience] WebView launchGame queued place_id=%lld\n",
static_cast<long long>(request.place_id));
return CloseWebSurface();
}
}
return RouteWebSurfaceEvent(route, event, web_view_bridge);
return event.type == WebViewHelperEventType::kRobloxCookie
? AcceptWebViewRobloxCookie(event.payload)
: RouteWebSurfaceEvent(route, event, web_view_bridge);
}
Status RobloxExperienceComposition::DrainPlatformEvents() {
-59
View File
@@ -640,64 +640,5 @@ Status ParseRobloxLaunchUri(std::string_view uri,
return MakeRequest(fields, request);
}
Status ParseRobloxWebViewLaunchGame(std::string_view command,
RobloxExperienceLaunchRequest* request) {
if (request == nullptr) {
return Invalid("Roblox launchGame output is null");
}
*request = {};
const nlohmann::json document =
command.size() <= kMaximumRobloxLaunchUriBytes
? nlohmann::json::parse(command, nullptr, false)
: nlohmann::json();
const auto function_name = document.is_object()
? document.find("functionName")
: document.end();
if (!document.is_object() || function_name == document.end() ||
*function_name != "launchGame") {
return Status::Error(StatusCode::kUnsupported,
"WebView command is not launchGame");
}
const auto params = document.find("params");
if (params == document.end() || !params->is_object()) {
return Invalid("Roblox launchGame has no params");
}
const auto launch = params->find("request");
if (launch == params->end() || !launch->is_object()) {
return Invalid("Roblox launchGame has no request");
}
if (launch->size() > kMaximumParameterCount) {
return Invalid("Roblox launchGame request has too many fields");
}
LaunchFields fields;
for (const auto& [key, value] : launch->items()) {
std::string text;
if (value.is_string()) {
text = value.get<std::string>();
} else if (value.is_number_unsigned()) {
text = std::to_string(value.get<uint64_t>());
} else {
continue;
}
if (text.size() > kMaximumParameterValueBytes ||
ContainsControlBytes(text)) {
return Invalid("Roblox launchGame field is invalid");
}
Status status = AssignField(key == "requestType" ? "request" : key,
std::move(text), false, &fields);
if (!status.ok()) {
return status;
}
}
if (fields.request_type.empty()) {
return Invalid("Roblox launchGame request type is missing");
}
Status status = MakeRequest(fields, request);
if (!status.ok()) {
*request = {};
}
return status;
}
} // namespace runtime
} // namespace mocktail
+11 -6
View File
@@ -244,7 +244,7 @@ struct TextBytePosition {
};
TextBytePosition TextPosition(TTF_Text* layout, std::size_t byte_offset,
int text_width) {
int text_width, bool single_line) {
if (layout == nullptr) {
return {};
}
@@ -259,7 +259,11 @@ TextBytePosition TextPosition(TTF_Text* layout, std::size_t byte_offset,
position.y = substring.rect.y;
position.height = substring.rect.h;
if ((substring.flags & TTF_SUBSTRING_TEXT_END) != 0) {
position.x = std::min(text_width, substring.rect.x + substring.rect.w);
// Trailing whitespace has no ink, so the final cluster rect stops short of
// the laid-out width that a caret at the end of a single line sits behind.
position.x = single_line ? text_width
: std::min(text_width,
substring.rect.x + substring.rect.w);
}
return position;
}
@@ -501,11 +505,12 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() {
TextBytePosition selection_end_position;
if (layout != nullptr) {
(void)TTF_GetTextSize(layout, &text_width, &text_height);
caret_position = TextPosition(layout, caret_byte, text_width);
selection_begin_position =
TextPosition(layout, selection_begin_byte, text_width);
caret_position =
TextPosition(layout, caret_byte, text_width, !wrapped_layout);
selection_begin_position = TextPosition(layout, selection_begin_byte,
text_width, !wrapped_layout);
selection_end_position =
TextPosition(layout, selection_end_byte, text_width);
TextPosition(layout, selection_end_byte, text_width, !wrapped_layout);
}
if (!text.value.empty()) {
// SDL_ttf implementations have historically differed on whether fg.a is
+4 -7
View File
@@ -14,8 +14,6 @@
#include <unistd.h>
#include <utility>
#include "compat/guest_abi.h"
namespace mocktail {
namespace runtime {
namespace {
@@ -440,8 +438,8 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
const std::filesystem::path library = canonical_root / "libroblox.so";
const std::filesystem::path package_root = canonical_root / "sober_apk";
const std::filesystem::path base_apk = package_root / "base.apk";
const std::filesystem::path arch_split_apk =
package_root / compat::kGuestSplitApkFile;
const std::filesystem::path x86_64_split_apk =
package_root / "split_config.x86_64.apk";
const std::filesystem::path assets_root = canonical_root / "assets";
const std::filesystem::path assets = canonical_root / "assets/content";
const std::filesystem::file_status library_status =
@@ -462,8 +460,7 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
for (const auto& [path, name] :
std::array<std::pair<std::filesystem::path, const char*>, 2>{
std::pair{base_apk, "base.apk"},
std::pair{arch_split_apk,
compat::kGuestSplitApkFile.data()}}) {
std::pair{x86_64_split_apk, "split_config.x86_64.apk"}}) {
const std::filesystem::file_status status =
std::filesystem::symlink_status(path, filesystem_error);
if (filesystem_error || !std::filesystem::is_regular_file(status) ||
@@ -492,7 +489,7 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
result.root = canonical_root;
result.roblox_library = library;
result.base_apk = base_apk;
result.arch_split_apk = arch_split_apk;
result.x86_64_split_apk = x86_64_split_apk;
result.assets_content = assets;
const auto compatibility_manifest =
+13 -25
View File
@@ -28,7 +28,6 @@
#include <vector>
#include "compat/elf_build_id.h"
#include "compat/guest_abi.h"
#include "update/android_manifest.h"
#include "update/apk_signature.h"
#include "update/payload_integrity.h"
@@ -174,15 +173,10 @@ bool EndsWith(std::string_view value, std::string_view suffix) {
struct ApkCandidate {
std::filesystem::path path;
AndroidManifestIdentity identity;
bool has_guest_library = false;
bool has_x86_64_library = false;
bool has_assets = false;
};
std::string GuestLibEntryName() {
return std::string(compat::kGuestLibApkEntryPrefix) +
std::string(compat::kGuestAbi) + "/libroblox.so";
}
bool CollectCandidates(const std::vector<std::filesystem::path>& archives,
const std::filesystem::path& directory,
std::vector<std::filesystem::path>* candidates,
@@ -256,7 +250,7 @@ bool InspectCandidates(const std::vector<std::filesystem::path>& paths,
ApkCandidate candidate;
candidate.path = path;
candidate.identity = std::move(identity);
candidate.has_guest_library = HasEntry(list, GuestLibEntryName());
candidate.has_x86_64_library = HasEntry(list, "lib/x86_64/libroblox.so");
candidate.has_assets = std::any_of(
list.entries.begin(), list.entries.end(), [](const ZipEntry& entry) {
return entry.name.size() > 7 && entry.name.substr(0, 7) == "assets/";
@@ -320,8 +314,7 @@ std::vector<std::string> CommonTrustedCertificates(
const ApkSignatureResult split_signature =
base == split ? base_signature : VerifyApkSignature(split);
if (!split_signature) {
*error = std::string(compat::kGuestAbi) +
" APK signature: " + split_signature.error;
*error = "x86_64 APK signature: " + split_signature.error;
return {};
}
std::vector<std::string> accepted;
@@ -363,11 +356,9 @@ bool ValidateElf(const std::filesystem::path& path, std::string* build_id,
}
GElf_Ehdr header{};
if (gelf_getehdr(elf.get(), &header) == nullptr ||
header.e_machine != compat::kGuestElfMachine ||
gelf_getclass(elf.get()) != ELFCLASS64) {
header.e_machine != EM_X86_64 || gelf_getclass(elf.get()) != ELFCLASS64) {
close_elf();
*error = "libroblox.so is not a " + std::string(compat::kGuestAbi) +
" ELF";
*error = "libroblox.so is not an x86-64 ELF";
return false;
}
std::size_t section_count = 0;
@@ -473,18 +464,16 @@ PreparedPayload PreparePayloadFromArchives(
base = &candidate;
}
}
const std::string guest_split_name(compat::kGuestApkSplitName);
for (const ApkCandidate& candidate : candidates) {
if (candidate.has_guest_library &&
(candidate.identity.split_name == guest_split_name ||
if (candidate.has_x86_64_library &&
(candidate.identity.split_name == "config.x86_64" ||
candidate.identity.split_name.empty()) &&
split == nullptr) {
split = &candidate;
}
}
if (base == nullptr || split == nullptr) {
result.error = "no matching Roblox base and " +
std::string(compat::kGuestAbi) + " APK pair was found";
result.error = "no matching Roblox base and x86_64 APK pair was found";
return result;
}
const std::set<std::string> trusted =
@@ -499,13 +488,12 @@ PreparedPayload PreparePayloadFromArchives(
if (filesystem_error ||
!CopyRegular(base->path, prepared / "sober_apk/base.apk",
&result.error) ||
!CopyRegular(split->path,
prepared / "sober_apk" / compat::kGuestSplitApkFile,
!CopyRegular(split->path, prepared / "sober_apk/split_config.x86_64.apk",
&result.error)) {
if (result.error.empty()) result.error = "cannot create prepared payload";
return result;
}
if (!ExtractZipEntry(split->path, GuestLibEntryName(),
if (!ExtractZipEntry(split->path, "lib/x86_64/libroblox.so",
prepared / "libroblox.so", kMaximumApkBytes,
&result.error)) {
return result;
@@ -539,7 +527,7 @@ PreparedPayload PreparePayloadFromArchives(
const std::string base_hash =
HashRegularFile(prepared / "sober_apk/base.apk", &hash_error);
const std::string split_hash = HashRegularFile(
prepared / "sober_apk" / compat::kGuestSplitApkFile, &hash_error);
prepared / "sober_apk/split_config.x86_64.apk", &hash_error);
if (!hash_error.empty()) {
result.error = hash_error;
return result;
@@ -549,12 +537,12 @@ PreparedPayload PreparePayloadFromArchives(
{"package", "com.roblox.client"},
{"version_name", expected.version_name},
{"version_code", expected.version_code},
{"abi", std::string(compat::kGuestAbi)},
{"abi", "x86_64"},
{"elf_build_id", build_id},
{"sha256",
{{"libroblox", library_hash},
{"base_apk", base_hash},
{std::string(compat::kGuestSplitApkHashKey), split_hash}}},
{"x86_64_split_apk", split_hash}}},
{"signing_certificates_sha256", accepted},
{"assets",
{{"file_count", asset_count_verified},
+2 -4
View File
@@ -9,7 +9,6 @@
#include <string>
#include <utility>
#include "compat/guest_abi.h"
#include "update/http_download.h"
namespace mocktail::update {
@@ -200,8 +199,7 @@ std::vector<std::string> ParseApkPureExactDownloadUrls(
}
ProviderVersion ApkPureProvider::CheckLatest() const {
const HttpBytesResult metadata =
FetchMetadata(std::string(compat::kGuestAbi));
const HttpBytesResult metadata = FetchMetadata("x86_64");
if (!metadata) return ProviderVersion{{}, 0, metadata.error};
return ParseApkPureLatestMetadata(metadata.bytes);
}
@@ -225,7 +223,7 @@ ProviderDownloadResult ApkPureProvider::DownloadExact(
result.error = "APKPure output directory must exist and be empty";
return result;
}
HttpBytesResult metadata = FetchMetadata(std::string(compat::kGuestAbi));
HttpBytesResult metadata = FetchMetadata("x86_64");
if (!metadata) {
result.error = metadata.error;
return result;
+1 -2
View File
@@ -19,7 +19,6 @@
#include <string_view>
#include "compat/elf_build_id.h"
#include "compat/guest_abi.h"
#include "update/payload_integrity.h"
namespace mocktail::update {
@@ -261,7 +260,7 @@ std::string PayloadRuntimeFingerprint(
evidence.append("base_apk=")
.append(payload.metadata.base_apk_sha256)
.append("\n");
evidence.append(std::string(compat::kGuestAbi) + "_split=")
evidence.append("x86_64_split=")
.append(payload.metadata.split_apk_sha256)
.append("\n");
evidence.append("assets=")
-13
View File
@@ -11,9 +11,6 @@
#include <cerrno>
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
#include "compat/guest_abi.h"
namespace mocktail::update {
namespace {
@@ -84,16 +81,6 @@ CompatibilityCatalogResult LoadCompatibilityCatalog(
profile.value("allow_legacy_binary_patches", true)) {
continue;
}
const auto declared_abi = profile.find("abi");
if (declared_abi != profile.end() && !declared_abi->is_string()) {
result.error = "supported compatibility profile is incomplete";
return result;
}
const std::string_view profile_abi =
declared_abi == profile.end()
? std::string_view("x86_64")
: std::string_view(declared_abi->get_ref<const std::string&>());
if (profile_abi != compat::kGuestAbi) continue;
if (!profile.contains("version_name") ||
!profile["version_name"].is_string() ||
!profile.contains("version_code") ||
+3 -5
View File
@@ -28,7 +28,6 @@
#include "compat/elf_build_id.h"
#include "compat/fmod_output_device_contract.h"
#include "compat/guest_abi.h"
#include "update/payload_integrity.h"
namespace mocktail::update {
@@ -221,10 +220,9 @@ class ElfImage final {
if (elf_ == nullptr || elf_kind(elf_) != ELF_K_ELF ||
gelf_getehdr(elf_, &header) == nullptr ||
gelf_getclass(elf_) != ELFCLASS64 || header.e_type != ET_DYN ||
header.e_machine != compat::kGuestElfMachine ||
header.e_version != EV_CURRENT ||
header.e_machine != EM_X86_64 || header.e_version != EV_CURRENT ||
header.e_ident[EI_DATA] != ELFDATA2LSB) {
*error = "candidate must be a little-endian guest-abi ET_DYN ELF";
*error = "candidate must be a little-endian x86-64 ET_DYN ELF";
return false;
}
std::size_t program_count = 0;
@@ -1881,7 +1879,7 @@ std::optional<Json> LoadCandidateMetadata(const std::filesystem::path& root,
metadata->contains("sha256") ? &(*metadata)["sha256"] : nullptr;
if (metadata->value("schema_version", 0) != 1 ||
metadata->value("package", "") != "com.roblox.client" ||
metadata->value("abi", "") != compat::kGuestAbi || version_name.empty() ||
metadata->value("abi", "") != "x86_64" || version_name.empty() ||
version_code == 0 ||
metadata->value("elf_build_id", "") != candidate.build_id() ||
hashes == nullptr || !hashes->is_object() ||
+3 -6
View File
@@ -18,8 +18,6 @@
#include <thread>
#include <vector>
#include "compat/guest_abi.h"
namespace mocktail::update {
namespace {
@@ -293,7 +291,7 @@ PayloadIntegrityResult InspectPreparedPayload(
for (const std::filesystem::path& relative : {
std::filesystem::path("libroblox.so"),
std::filesystem::path("sober_apk/base.apk"),
std::filesystem::path("sober_apk") / compat::kGuestSplitApkFile,
std::filesystem::path("sober_apk/split_config.x86_64.apk"),
std::filesystem::path("roblox_payload.json"),
}) {
const auto status =
@@ -337,7 +335,7 @@ PayloadIntegrityResult InspectPreparedPayload(
result.metadata.library_sha256 = document["sha256"].value("libroblox", "");
result.metadata.base_apk_sha256 = document["sha256"].value("base_apk", "");
result.metadata.split_apk_sha256 =
document["sha256"].value(std::string(compat::kGuestSplitApkHashKey), "");
document["sha256"].value("x86_64_split_apk", "");
if (!document["assets"].contains("file_count") ||
!document["assets"]["file_count"].is_number_unsigned()) {
result.error = "payload metadata asset count is invalid";
@@ -370,8 +368,7 @@ PayloadIntegrityResult VerifyPreparedPayload(
const std::array<std::pair<std::filesystem::path, std::string>, 3> files = {{
{"libroblox.so", result.metadata.library_sha256},
{"sober_apk/base.apk", result.metadata.base_apk_sha256},
{std::filesystem::path("sober_apk") / compat::kGuestSplitApkFile,
result.metadata.split_apk_sha256},
{"sober_apk/split_config.x86_64.apk", result.metadata.split_apk_sha256},
}};
for (const auto& [relative, expected] : files) {
const std::string actual =
+3 -40
View File
@@ -45,8 +45,6 @@ jq --arg hash "${UPDATED_FFMPEG_HASH}" '
mkdir -p "${TEMP_DIR}/project/scripts" "${TEMP_DIR}/project/packaging"
cp "${ROOT}/scripts/install_anylinux_dependencies.sh" "${TEMP_DIR}/project/scripts/"
cp "${TEMP_DIR}/manifest.txt" "${TEMP_DIR}/project/packaging/anylinux-dependencies.txt"
cp "${ROOT}/packaging/anylinux-dependencies-aarch64.txt" \
"${TEMP_DIR}/project/packaging/"
# Record requests without any network access or changes to host packages.
curl() {
@@ -123,13 +121,13 @@ ExpectRejectedSnapshot() {
local description="$1" filter="$2"
jq "${filter}" "${TEMP_DIR}/release.json" >"${TEMP_DIR}/invalid.json"
if (AnyLinuxDependencySnapshot "${TEMP_DIR}/manifest.txt" \
"${TEMP_DIR}/invalid.json" x86_64) >"${TEMP_DIR}/invalid.log" 2>&1; then
"${TEMP_DIR}/invalid.json") >"${TEMP_DIR}/invalid.log" 2>&1; then
Fail "accepted ${description}"
fi
}
(AnyLinuxDependencySnapshot "${TEMP_DIR}/manifest.txt" \
"${TEMP_DIR}/release.json" x86_64) >"${TEMP_DIR}/snapshot.tsv" ||
"${TEMP_DIR}/release.json") >"${TEMP_DIR}/snapshot.tsv" ||
Fail 'rejected valid dependency snapshot'
printf '%s\t%s\t%s\n' \
"${FFMPEG_HASH}" "${FFMPEG}" "${API}/assets/10" \
@@ -138,40 +136,6 @@ printf '%s\t%s\t%s\n' \
cmp "${TEMP_DIR}/expected-snapshot.tsv" "${TEMP_DIR}/snapshot.tsv" ||
Fail 'incorrect dependency snapshot'
readonly ARM_PACKAGE=gdk-pixbuf2-mini-aarch64.pkg.tar.xz
printf '%s\n' "${ARM_PACKAGE}" >"${TEMP_DIR}/arm-manifest.txt"
jq --null-input --arg api "${API}" --arg name "${ARM_PACKAGE}" \
--arg hash "${FFMPEG_HASH}" '
{tag_name: "continuous", assets: [
{id: 40, name: $name, state: "uploaded",
digest: ("sha256:" + $hash), url: ($api + "/assets/40")}
]}
' >"${TEMP_DIR}/arm-release.json"
AnyLinuxDependencySnapshot "${TEMP_DIR}/arm-manifest.txt" \
"${TEMP_DIR}/arm-release.json" aarch64 >"${TEMP_DIR}/arm-snapshot.tsv" ||
Fail 'rejected valid aarch64 dependency snapshot'
printf '%s\t%s\t%s\n' "${FFMPEG_HASH}" "${ARM_PACKAGE}" \
"${API}/assets/40" >"${TEMP_DIR}/expected-arm-snapshot.tsv"
cmp "${TEMP_DIR}/expected-arm-snapshot.tsv" "${TEMP_DIR}/arm-snapshot.tsv" ||
Fail 'incorrect aarch64 dependency snapshot'
if (AnyLinuxDependencySnapshot "${TEMP_DIR}/arm-manifest.txt" \
"${TEMP_DIR}/arm-release.json" x86_64) >/dev/null 2>&1; then
Fail 'accepted an aarch64 dependency on an x86_64 host'
fi
bash -Eeuo pipefail -c '
source "$1/project/scripts/install_anylinux_dependencies.sh"
AnyLinuxDependencyRequireContainer() { :; }
uname() { printf "aarch64\n"; }
AnyLinuxDownloadDependencies() {
printf "%s\n" "$1" >"${TEMP_DIR}/arm-selected-manifest"
exit 0
}
AnyLinuxDependenciesMain
' _ "${TEMP_DIR}" || Fail 'aarch64 manifest selection failed'
grep -Fxq "${TEMP_DIR}/project/packaging/anylinux-dependencies-aarch64.txt" \
"${TEMP_DIR}/arm-selected-manifest" ||
Fail 'aarch64 build did not select its dependency manifest'
ExpectRejectedSnapshot 'missing asset' '.assets |= .[1:]'
ExpectRejectedSnapshot 'duplicate asset' '.assets += [.assets[0]]'
ExpectRejectedSnapshot 'absent SHA256' '.assets[0].digest = null'
@@ -185,7 +149,7 @@ for invalid_manifest in '../ffmpeg-mini-x86_64.pkg.tar.zst' \
"${FFMPEG} extra" "${FFMPEG}"$'\n'"${FFMPEG}" ''; do
printf '%s\n' "${invalid_manifest}" >"${TEMP_DIR}/invalid-manifest.txt"
if (AnyLinuxDependencySnapshot "${TEMP_DIR}/invalid-manifest.txt" \
"${TEMP_DIR}/release.json" x86_64) >"${TEMP_DIR}/invalid.log" 2>&1; then
"${TEMP_DIR}/release.json") >"${TEMP_DIR}/invalid.log" 2>&1; then
Fail 'accepted unsafe, duplicate, or empty package allowlist'
fi
done
@@ -202,7 +166,6 @@ for SCENARIO in success replaced corrupted download-failure metadata-failure inv
if GH_TOKEN=fixture-token bash -Eeuo pipefail -c '
source "$1/project/scripts/install_anylinux_dependencies.sh"
AnyLinuxDependencyRequireContainer() { :; }
uname() { printf "x86_64\n"; }
AnyLinuxDependenciesMain
' _ "${TEMP_DIR}" \
>"${TEMP_DIR}/${SCENARIO}.log" 2>&1; then
+2 -6
View File
@@ -1,7 +1,5 @@
#include "compat/bionic_abi_exports.h"
#include "compat/guest_abi.h"
#include <gtest/gtest.h>
#include <fcntl.h>
@@ -39,10 +37,8 @@ TEST(BionicAbiExportsTest, ReportsStableAndroidSystemProperties) {
EXPECT_STREQ("35", value.data());
value.fill('\0');
EXPECT_EQ(static_cast<int>(mocktail::compat::kGuestAbi.size()),
__system_property_get("ro.product.cpu.abi", value.data()));
EXPECT_STREQ(std::string(mocktail::compat::kGuestAbi).c_str(),
value.data());
EXPECT_EQ(6, __system_property_get("ro.product.cpu.abi", value.data()));
EXPECT_STREQ("x86_64", value.data());
value.fill('x');
EXPECT_EQ(0, __system_property_get("mocktail.unknown", value.data()));
+14 -18
View File
@@ -5,8 +5,6 @@
#include <sched.h>
#include <unistd.h>
#include "compat/bionic_abi_exports.h"
#include <algorithm>
#include <atomic>
@@ -191,10 +189,9 @@ TEST(BionicPthreadCreateRuntimeTest, GrowsRejectedExplicitGuestStack) {
0);
ASSERT_EQ(pthread_attr_destroy(&host_defaults), 0);
MocktailBionicPthreadAttr guest_attributes;
ASSERT_EQ(mocktail_pthread_attr_init(&guest_attributes), 0);
ASSERT_EQ(mocktail_pthread_attr_setstacksize(&guest_attributes,
kRequestedStackSize),
pthread_attr_t guest_attributes;
ASSERT_EQ(pthread_attr_init(&guest_attributes), 0);
ASSERT_EQ(pthread_attr_setstacksize(&guest_attributes, kRequestedStackSize),
0);
g_intercepted_create_calls.store(0, std::memory_order_release);
@@ -209,7 +206,7 @@ TEST(BionicPthreadCreateRuntimeTest, GrowsRejectedExplicitGuestStack) {
&thread, &guest_attributes, RecordThreadStackSize, nullptr);
g_intercept_pthread_create.store(false, std::memory_order_release);
ASSERT_EQ(mocktail_pthread_attr_destroy(&guest_attributes), 0);
ASSERT_EQ(pthread_attr_destroy(&guest_attributes), 0);
ASSERT_EQ(result, 0);
ASSERT_EQ(pthread_join(thread, nullptr), 0);
EXPECT_EQ(g_intercepted_create_calls.load(std::memory_order_acquire), 3);
@@ -226,16 +223,15 @@ TEST(BionicPthreadCreateRuntimeTest, PreservesRequestedStackSize) {
constexpr size_t kRequestedStackSize = 2 * 1024 * 1024;
g_observed_stack_size.store(0, std::memory_order_release);
MocktailBionicPthreadAttr attributes;
ASSERT_EQ(mocktail_pthread_attr_init(&attributes), 0);
ASSERT_EQ(
mocktail_pthread_attr_setstacksize(&attributes, kRequestedStackSize), 0);
pthread_attr_t attributes;
ASSERT_EQ(pthread_attr_init(&attributes), 0);
ASSERT_EQ(pthread_attr_setstacksize(&attributes, kRequestedStackSize), 0);
pthread_t thread{};
ASSERT_EQ(mocktail_bionic_pthread_create(&thread, &attributes,
RecordThreadStackSize, nullptr),
0);
EXPECT_EQ(mocktail_pthread_attr_destroy(&attributes), 0);
EXPECT_EQ(pthread_attr_destroy(&attributes), 0);
ASSERT_EQ(pthread_join(thread, nullptr), 0);
ExpectRequestedStackSize(
g_observed_stack_size.load(std::memory_order_acquire),
@@ -244,13 +240,13 @@ TEST(BionicPthreadCreateRuntimeTest, PreservesRequestedStackSize) {
TEST(BionicPthreadCreateRuntimeTest, RejectsExplicitRealtimeBeforeHostCreate) {
for (const int policy : {SCHED_FIFO, SCHED_RR}) {
MocktailBionicPthreadAttr attributes;
ASSERT_EQ(mocktail_pthread_attr_init(&attributes), 0);
attributes.sched_policy = policy;
pthread_attr_t attributes;
ASSERT_EQ(pthread_attr_init(&attributes), 0);
ASSERT_EQ(pthread_attr_setinheritsched(&attributes, PTHREAD_EXPLICIT_SCHED), 0);
ASSERT_EQ(pthread_attr_setschedpolicy(&attributes, policy), 0);
sched_param parameters{};
parameters.sched_priority = 99;
ASSERT_EQ(
mocktail_pthread_attr_setschedparam(&attributes, &parameters), 0);
ASSERT_EQ(pthread_attr_setschedparam(&attributes, &parameters), 0);
g_intercepted_create_calls.store(0);
g_intercept_pthread_create.store(true);
pthread_t thread{};
@@ -259,7 +255,7 @@ TEST(BionicPthreadCreateRuntimeTest, RejectsExplicitRealtimeBeforeHostCreate) {
g_intercept_pthread_create.store(false);
EXPECT_EQ(result, EPERM);
EXPECT_EQ(g_intercepted_create_calls.load(), 0);
EXPECT_EQ(mocktail_pthread_attr_destroy(&attributes), 0);
EXPECT_EQ(pthread_attr_destroy(&attributes), 0);
}
}
+2 -6
View File
@@ -2,8 +2,6 @@
#include <gtest/gtest.h>
#include "compat/guest_abi.h"
#include <algorithm>
#include <array>
#include <string>
@@ -276,10 +274,8 @@ TEST(CommandLineTest, UsageContainsEverySupportedOption) {
EXPECT_EQ(usage.find("--login"), std::string::npos);
EXPECT_EQ(usage.find("--import-cookie"), std::string::npos);
EXPECT_NE(usage.find("native sign-in flow"), std::string::npos);
EXPECT_NE(
usage.find("verified managed " +
std::string(mocktail::compat::kGuestAbi) + " Roblox payload"),
std::string::npos);
EXPECT_NE(usage.find("verified managed x86_64 Roblox payload"),
std::string::npos);
EXPECT_EQ(usage.find("Normal startup uses rbx_bin/libroblox.so"),
std::string::npos);
EXPECT_EQ(usage.find("--launch-request-json"), std::string::npos);
-28
View File
@@ -161,12 +161,6 @@ grep -Fq -- '--default-branch=stable' "${GITHUB_CI}" ||
Fail 'GitHub Actions does not produce the stable Flatpak branch'
grep -Fq 'Mocktail-x86_64.flatpak' "${GITHUB_CI}" ||
Fail 'GitHub Actions does not build the installable Flatpak bundle'
grep -Fq 'Mocktail-aarch64.flatpak' "${GITHUB_CI}" ||
Fail 'GitHub Actions does not build the aarch64 Flatpak bundle'
grep -Fq 'ubuntu-24.04-arm' "${GITHUB_CI}" ||
Fail 'GitHub Actions does not build Flatpak on an ARM64 runner'
grep -Fq 'flatpak build-import-bundle' "${GITHUB_CI}" ||
Fail 'GitHub Actions does not merge both architectures into one repository'
grep -Fq 'actions/upload-artifact@' "${GITHUB_CI}" ||
Fail 'GitHub Actions does not publish the Flatpak artifact'
grep -Fq 'actions/upload-pages-artifact@' "${GITHUB_CI}" ||
@@ -215,35 +209,18 @@ set -e
grep -Fq 'flatpak-builder is unavailable' "${TEMP_DIR}/stderr" ||
Fail 'helper did not explain how to install flatpak-builder'
cat >"${TEMP_DIR}/bin/uname" <<'EOF'
#!/usr/bin/env bash
printf 'aarch64\n'
EOF
cat >"${TEMP_DIR}/bin/flatpak-builder" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$@" >"${MOCKTAIL_TEST_BUILDER_ARGS}"
EOF
chmod 0755 "${TEMP_DIR}/bin/flatpak-builder"
PATH="${TEMP_DIR}/bin:/usr/bin:/bin" \
MOCKTAIL_TEST_BUILDER_ARGS="${TEMP_DIR}/builder-args" \
"${BUILD_HELPER}" --build-dir "${ROOT}/build-flatpak-test"
grep -Fxq -- '--arch=aarch64' "${TEMP_DIR}/builder-args" ||
Fail 'local Flatpak helper did not select aarch64'
mkdir -p -- "${TEMP_DIR}/repo/objects"
mkdir -p -- \
"${TEMP_DIR}/native-repositories/apt" \
"${TEMP_DIR}/native-repositories/rpm" \
"${TEMP_DIR}/native-repositories/downloads"
printf 'bundle\n' >"${TEMP_DIR}/Mocktail-x86_64.flatpak"
printf 'bundle\n' >"${TEMP_DIR}/Mocktail-aarch64.flatpak"
printf 'public-key\n' >"${TEMP_DIR}/mocktail-flatpak.gpg"
printf 'public-key\n' \
>"${TEMP_DIR}/native-repositories/mocktail-packages.gpg"
"${PAGES_HELPER}" \
"${TEMP_DIR}/repo" \
"${TEMP_DIR}/Mocktail-x86_64.flatpak" \
"${TEMP_DIR}/Mocktail-aarch64.flatpak" \
"${TEMP_DIR}/mocktail-flatpak.gpg" \
"${TEMP_DIR}/public" \
"${TEMP_DIR}/native-repositories"
@@ -253,13 +230,8 @@ grep -Fq 'Url=https://mocktail.bigrat.space/repo/' \
grep -Fq 'Name=space.bigrat.mocktail' \
"${TEMP_DIR}/public/mocktail.flatpakref" ||
Fail 'published Flatpak ref has the wrong application ID'
[[ -f "${TEMP_DIR}/public/Mocktail-x86_64.flatpak" &&
-f "${TEMP_DIR}/public/Mocktail-aarch64.flatpak" ]] ||
Fail 'Pages output does not include both Flatpak bundles'
grep -Fq 'flatpak install --user' "${TEMP_DIR}/public/index.html" ||
Fail 'Pages landing page has no direct installation command'
grep -Fq 'Mocktail-aarch64.flatpak' "${TEMP_DIR}/public/index.html" ||
Fail 'Pages landing page has no aarch64 Flatpak bundle link'
grep -Fq 'roblox://experiences/start' "${TEMP_DIR}/public/join.html" ||
Fail 'Pages output has no Discord join bridge'
[[ -d "${TEMP_DIR}/public/apt" &&
-6
View File
@@ -102,16 +102,10 @@ for expected in \
'MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1' \
'--appimage-extract-and-run mocktail_updater status' \
'Mocktail-x86_64.AppImage' \
'Mocktail-aarch64.AppImage' \
'mocktail-nightly-aarch64.AppImage' \
'ubuntu-24.04-arm' \
'gh release upload continuous'; do
grep -Fq -- "${expected}" "${WORKFLOW}" ||
Fail "native package workflow is missing: ${expected}"
done
grep -Fq 'needs: [appimage, appimage-aarch64, deb, rpm, arch]' \
"${WORKFLOW}" ||
Fail 'continuous release does not wait for the aarch64 AppImage'
grep -Fq 'paths-ignore:' "${WORKFLOW}" ||
Fail 'native package workflow has no path filters'
+19 -37
View File
@@ -13,7 +13,6 @@
#include <vector>
#include "compat/build_profile.h"
#include "compat/guest_abi.h"
#include "compat/host_abi_profile.h"
#include "update/apkpure_provider.h"
#include "update/compatibility_catalog.h"
@@ -519,18 +518,6 @@ TEST(ShippedMetadataTest, ReferenceSidecarDescribesASupportedProfile) {
const HostAbiSidecarIdentity identity = ReadHostAbiSidecarIdentity(
root / "config/roblox_host_abi_reference.json");
ASSERT_TRUE(identity) << identity.error;
std::string sidecar_abi = "x86_64";
for (const auto& entry :
nlohmann::json::parse(
ReadFile(root / "config/roblox_compatibility.json"))
.at("profiles")) {
if (entry.value("elf_build_id", "") == identity.elf_build_id) {
sidecar_abi = entry.value("abi", "x86_64");
}
}
if (sidecar_abi != compat::kGuestAbi) {
GTEST_SKIP() << "shipped reference sidecar targets " << sidecar_abi;
}
const auto supported = std::find_if(
catalog.profiles.begin(), catalog.profiles.end(),
@@ -571,11 +558,6 @@ TEST(ShippedMetadataTest, DefaultPayloadHasABuiltinProfileAndMatchingReference)
const auto payload = nlohmann::json::parse(
ReadFile(root / "config/roblox_payload.json"));
if (payload.at("abi").get_ref<const std::string&>() != compat::kGuestAbi) {
EXPECT_NE(compat::FindHostAbiProfile(preferred->elf_build_id), nullptr);
GTEST_SKIP() << "shipped default payload targets "
<< payload.at("abi").get<std::string>();
}
EXPECT_EQ(payload.at("version_name"), preferred->version_name);
EXPECT_EQ(payload.at("version_code"), preferred->version_code);
EXPECT_EQ(payload.at("elf_build_id"), preferred->elf_build_id);
@@ -661,7 +643,7 @@ TEST(PayloadStoreTest, RestagingIdenticalBytesKeepsTheInstalledPayload) {
const std::filesystem::path restaged = temporary.root() / "restaged";
Write(prepared / "libroblox.so", "native-library");
Write(prepared / "sober_apk/base.apk", "base-apk");
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
Write(prepared / "assets/content/fixture", "asset");
std::string error;
std::size_t asset_count = 0;
@@ -677,8 +659,8 @@ TEST(PayloadStoreTest, RestagingIdenticalBytesKeepsTheInstalledPayload) {
{"sha256",
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
{std::string(compat::kGuestSplitApkHashKey),
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
{"x86_64_split_apk",
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
{"source", "apk-pure-native"},
{"imported_at", "2026-09-04T03:49:16Z"},
@@ -722,7 +704,7 @@ TEST(PayloadStoreTest, StagesAndPromotesVerifiedExactPayload) {
const std::filesystem::path prepared = temporary.root() / "prepared";
Write(prepared / "libroblox.so", "native-library");
Write(prepared / "sober_apk/base.apk", "base-apk");
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
Write(prepared / "assets/content/fixture", "asset");
std::string error;
std::size_t asset_count = 0;
@@ -740,8 +722,8 @@ TEST(PayloadStoreTest, StagesAndPromotesVerifiedExactPayload) {
{"sha256",
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
{std::string(compat::kGuestSplitApkHashKey),
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
{"x86_64_split_apk",
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
};
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
@@ -785,7 +767,7 @@ TEST(PayloadStoreTest, RestagesReadOnlyCorruptPayloadCollision) {
const std::filesystem::path prepared = temporary.root() / "prepared";
Write(prepared / "libroblox.so", "native-library");
Write(prepared / "sober_apk/base.apk", "base-apk");
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
Write(prepared / "assets/content/fixture", "asset");
std::string error;
std::size_t asset_count = 0;
@@ -803,8 +785,8 @@ TEST(PayloadStoreTest, RestagesReadOnlyCorruptPayloadCollision) {
{"sha256",
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
{std::string(compat::kGuestSplitApkHashKey),
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
{"x86_64_split_apk",
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
};
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
@@ -855,7 +837,7 @@ TEST(PayloadStoreTest, PromoteRehashesBytesTamperedAfterStaging) {
const std::filesystem::path prepared = temporary.root() / "prepared";
Write(prepared / "libroblox.so", "native-library");
Write(prepared / "sober_apk/base.apk", "base-apk");
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
Write(prepared / "assets/content/fixture", "asset");
std::string error;
std::size_t asset_count = 0;
@@ -871,8 +853,8 @@ TEST(PayloadStoreTest, PromoteRehashesBytesTamperedAfterStaging) {
{"sha256",
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
{std::string(compat::kGuestSplitApkHashKey),
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
{"x86_64_split_apk",
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
};
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
@@ -915,7 +897,7 @@ TEST(PayloadStoreTest, PromoteRejectsALibraryTamperedAfterStaging) {
const std::filesystem::path prepared = temporary.root() / "prepared";
Write(prepared / "libroblox.so", "native-library");
Write(prepared / "sober_apk/base.apk", "base-apk");
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
Write(prepared / "assets/content/fixture", "asset");
std::string error;
std::size_t asset_count = 0;
@@ -931,8 +913,8 @@ TEST(PayloadStoreTest, PromoteRejectsALibraryTamperedAfterStaging) {
{"sha256",
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
{std::string(compat::kGuestSplitApkHashKey),
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
{"x86_64_split_apk",
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
};
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
@@ -976,7 +958,7 @@ TEST(PayloadStoreTest, RequiresTwoRuntimeBoundCanariesForLatestCandidate) {
const std::filesystem::path prepared = temporary.root() / "prepared";
Write(prepared / "libroblox.so", "new-native-library");
Write(prepared / "sober_apk/base.apk", "new-base-apk");
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "new-split-apk");
Write(prepared / "sober_apk/split_config.x86_64.apk", "new-split-apk");
Write(prepared / "assets/content/fixture", "new-asset");
std::string error;
std::size_t asset_count = 0;
@@ -990,13 +972,13 @@ TEST(PayloadStoreTest, RequiresTwoRuntimeBoundCanariesForLatestCandidate) {
{"package", "com.roblox.client"},
{"version_name", "2.732.1043"},
{"version_code", 2814},
{"abi", std::string(compat::kGuestAbi)},
{"abi", "x86_64"},
{"elf_build_id", kBuildId},
{"sha256",
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
{std::string(compat::kGuestSplitApkHashKey),
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
{"x86_64_split_apk",
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
};
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
@@ -145,28 +145,6 @@ TEST(SdlEventConverterTest, ConvertsFocusWithoutOwningTheSdlQueue) {
EXPECT_TRUE(focus->focused);
}
TEST(SdlEventConverterTest, PreservesHostWheelDirectionAndPrecision) {
SDL_Event source{};
source.type = SDL_EVENT_MOUSE_WHEEL;
source.wheel.mouse_x = 120.0f;
source.wheel.mouse_y = 240.0f;
platform::PlatformEvent event;
auto* non_owning_window = reinterpret_cast<SDL_Window*>(0x1);
for (const auto direction : {SDL_MOUSEWHEEL_NORMAL, SDL_MOUSEWHEEL_FLIPPED}) {
source.wheel.direction = direction;
source.wheel.x = 0.25f;
source.wheel.y = -1.5f;
ASSERT_TRUE(platform::ConvertSdlEvent(non_owning_window, source, &event));
const auto* wheel = std::get_if<platform::MouseWheelEvent>(&event.payload);
ASSERT_NE(wheel, nullptr);
EXPECT_FLOAT_EQ(wheel->delta_x, 0.25f);
EXPECT_FLOAT_EQ(wheel->delta_y, -1.5f);
EXPECT_FLOAT_EQ(wheel->mouse_x, 120.0f);
EXPECT_FLOAT_EQ(wheel->mouse_y, 240.0f);
}
}
TEST(SdlEventConverterTest, RejectsInvalidArgumentsAndUnsupportedEvents) {
SDL_Event source{};
source.type = SDL_EVENT_USER;
-71
View File
@@ -322,77 +322,6 @@ TEST(RobloxLaunchUriTest, ClearsOutputAndRedactsTicketOnFailure) {
EXPECT_FALSE(ParseRobloxLaunchUri(kModernPublicLaunchUri, nullptr).ok());
}
TEST(RobloxWebViewLaunchGameTest, ParsesPrivateServerPlayButton) {
constexpr char kCommand[] =
R"({"moduleID":"Game","functionName":"launchGame",)"
R"("callbackID":"b8cf5fbd-f6a8-cfba-2cae-84bce46f46b2",)"
R"("params":{"request":{"requestType":"RequestPrivateGame",)"
R"("browserTrackerId":"1790271319250009",)"
R"("accessCode":"664b524f-19cc-4e28-a947-e8f5286f4179",)"
R"("placeId":"920587237","joinAttemptId":"attempt-1",)"
R"("joinAttemptOrigin":"privateServerListJoin",)"
R"("isPlayTogetherGame":false}}})";
RobloxExperienceLaunchRequest request;
const Status status = ParseRobloxWebViewLaunchGame(kCommand, &request);
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_EQ(request.place_id, 920587237);
EXPECT_EQ(request.access_code, "664b524f-19cc-4e28-a947-e8f5286f4179");
EXPECT_EQ(request.join_attempt_id, "attempt-1");
EXPECT_EQ(request.join_attempt_origin, "privateServerListJoin");
EXPECT_EQ(request.canonical_json.find("browserTrackerId"), std::string::npos);
ExpectCanonicalRequestMatches(request);
}
TEST(RobloxWebViewLaunchGameTest, ParsesServerJobWithNumericPlaceId) {
constexpr char kCommand[] =
R"({"moduleID":"Game","functionName":"launchGame",)"
R"("params":{"request":{"requestType":"RequestGameJob",)"
R"("placeId":1962086868,"gameId":"97ea-49e4"}}})";
RobloxExperienceLaunchRequest request;
const Status status = ParseRobloxWebViewLaunchGame(kCommand, &request);
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_EQ(request.place_id, 1962086868);
EXPECT_EQ(request.game_instance_id, "97ea-49e4");
ExpectCanonicalRequestMatches(request);
}
TEST(RobloxWebViewLaunchGameTest, ReportsOtherCommandsAsUnsupported) {
RobloxExperienceLaunchRequest request;
request.place_id = 123;
request.canonical_json = "stale";
const Status status = ParseRobloxWebViewLaunchGame(
R"({"moduleID":"Game","functionName":"getSystemInfo","params":{}})",
&request);
EXPECT_EQ(status.code(), StatusCode::kUnsupported);
EXPECT_EQ(request.place_id, 0);
EXPECT_TRUE(request.canonical_json.empty());
EXPECT_EQ(ParseRobloxWebViewLaunchGame("not json", &request).code(),
StatusCode::kUnsupported);
}
TEST(RobloxWebViewLaunchGameTest, RejectsMalformedLaunchGame) {
const char* const kCommands[] = {
R"({"functionName":"launchGame"})",
R"({"functionName":"launchGame","params":{"request":"x"}})",
R"({"functionName":"launchGame","params":{"request":{)"
R"("requestType":"RequestPrivateGame","placeId":"920587237"}}})",
R"({"functionName":"launchGame","params":{"request":{)"
R"("requestType":"RequestGame","placeId":"-1"}}})",
};
for (const char* command : kCommands) {
RobloxExperienceLaunchRequest request;
const Status status = ParseRobloxWebViewLaunchGame(command, &request);
EXPECT_EQ(status.code(), StatusCode::kInvalidArgument) << command;
EXPECT_EQ(request.place_id, 0) << command;
}
}
} // namespace
} // namespace runtime
} // namespace mocktail
+25
View File
@@ -569,6 +569,31 @@ TEST(RobloxTextDisplayStateTest, WrapsMultilineTextInsideNativeTextBox) {
EXPECT_TRUE(overlay.Shutdown().ok());
}
TEST(RobloxTextDisplayStateTest, AdvancesCaretForTrailingSpace) {
RobloxTextSurfaceOverlay overlay;
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
RobloxTextDisplaySink sink = overlay.sink();
const auto rightmost_pixel = [&](const std::string& text,
RobloxTextDisplayEvent event) {
RobloxTextDisplayUpdate update =
Show(1, text, static_cast<int32_t>(text.size()));
update.event = event;
update.font_size = 18.0F;
sink.update(sink.context, update);
MocktailTextOverlayFrameInfo frame;
EXPECT_TRUE(overlay.QueryFrame(&frame));
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
return FindAlphaBounds(frame, rgba).maximum_x;
};
const int typed = rightmost_pixel("hi", RobloxTextDisplayEvent::kShow);
const int spaced = rightmost_pixel("hi ", RobloxTextDisplayEvent::kUpdate);
EXPECT_GT(spaced, typed);
EXPECT_TRUE(overlay.Shutdown().ok());
}
} // namespace
} // namespace runtime
} // namespace mocktail
+8 -11
View File
@@ -1,7 +1,5 @@
#include "runtime/runtime_paths.h"
#include "compat/guest_abi.h"
#include <gtest/gtest.h>
#include <unistd.h>
@@ -95,8 +93,8 @@ void WriteManagedPayloadFiles(const std::filesystem::path& payload) {
ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "sober_apk"));
std::ofstream(payload / "libroblox.so") << "ELF fixture";
std::ofstream(payload / "sober_apk/base.apk") << "base APK fixture";
std::ofstream(payload / "sober_apk" / compat::kGuestSplitApkFile)
<< "split APK fixture";
std::ofstream(payload / "sober_apk/split_config.x86_64.apk")
<< "x86_64 split APK fixture";
}
TEST(RuntimePathsTest, DerivesXdgDefaults) {
@@ -176,8 +174,8 @@ TEST(RuntimePathsTest, ResolvesValidatedActivePayload) {
EXPECT_EQ(active.root, std::filesystem::canonical(payload));
EXPECT_EQ(active.roblox_library, active.root / "libroblox.so");
EXPECT_EQ(active.base_apk, active.root / "sober_apk/base.apk");
EXPECT_EQ(active.arch_split_apk,
active.root / "sober_apk" / compat::kGuestSplitApkFile);
EXPECT_EQ(active.x86_64_split_apk,
active.root / "sober_apk/split_config.x86_64.apk");
EXPECT_EQ(active.assets_content, active.root / "assets/content");
EXPECT_EQ(paths.DefaultAssetPath(), active.assets_content);
}
@@ -191,7 +189,7 @@ TEST(RuntimePathsTest, RejectsIncompleteActiveAndroidPackage) {
const std::filesystem::path payload = data / "payloads" / payload_id;
WriteManagedPayloadFiles(payload);
ASSERT_TRUE(std::filesystem::remove(
payload / "sober_apk" / compat::kGuestSplitApkFile));
payload / "sober_apk/split_config.x86_64.apk"));
std::ofstream(data / "current.json")
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
<< "\",\"payload_path\":\"payloads/" << payload_id
@@ -207,8 +205,7 @@ TEST(RuntimePathsTest, RejectsIncompleteActiveAndroidPackage) {
.ResolveActivePayload();
EXPECT_FALSE(active);
EXPECT_NE(active.error.find(std::string(compat::kGuestSplitApkFile)),
std::string::npos);
EXPECT_NE(active.error.find("split_config.x86_64.apk"), std::string::npos);
}
TEST(RuntimePathsTest, ResolvesCompleteApprovedProfileReferences) {
@@ -499,8 +496,8 @@ TEST(RuntimePathsTest, PreparesManagedPayloadRelativeAssetRoot) {
active.root = payload;
active.roblox_library = payload / "libroblox.so";
active.base_apk = payload / "sober_apk/base.apk";
active.arch_split_apk =
payload / "sober_apk" / compat::kGuestSplitApkFile;
active.x86_64_split_apk =
payload / "sober_apk/split_config.x86_64.apk";
active.assets_content = payload / "assets/content";
std::string error;
+5 -16
View File
@@ -9,8 +9,6 @@
#include "runtime/graphics_launch_policy.h"
#include "compat/guest_abi.h"
namespace mocktail {
namespace runtime {
namespace {
@@ -42,22 +40,16 @@ void WriteManifest(const std::filesystem::path& path) {
TEST(VulkanIcdSelectionTest, PrefersTheNativeArchitectureManifest) {
TemporaryDirectory temporary;
WriteManifest(temporary.root() / "nvidia_icd.i686.json");
const std::string native_name =
"nvidia_icd." + std::string(compat::kGuestCpuName) + ".json";
WriteManifest(temporary.root() / native_name);
WriteManifest(temporary.root() / "nvidia_icd.x86_64.json");
EXPECT_EQ(SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd"),
(temporary.root() / native_name).string());
(temporary.root() / "nvidia_icd.x86_64.json").string());
}
TEST(VulkanIcdSelectionTest, NeverSelectsAForeignArchitectureManifest) {
TemporaryDirectory temporary;
WriteManifest(temporary.root() / "nvidia_icd.i686.json");
#if defined(__aarch64__)
WriteManifest(temporary.root() / "radeon_icd.x86_64.json");
#else
WriteManifest(temporary.root() / "radeon_icd.aarch64.json");
#endif
EXPECT_TRUE(
SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd").empty());
@@ -78,8 +70,7 @@ TEST(VulkanIcdSelectionTest, KeepsDirectoryPrecedenceOverName) {
const std::filesystem::path first = temporary.root() / "first";
const std::filesystem::path second = temporary.root() / "second";
WriteManifest(first / "nvidia_icd.json");
WriteManifest(second / ("nvidia_icd." +
std::string(compat::kGuestCpuName) + ".json"));
WriteManifest(second / "nvidia_icd.x86_64.json");
EXPECT_EQ(SelectVulkanIcdManifest({first, second}, "nvidia_icd"),
(first / "nvidia_icd.json").string());
@@ -87,15 +78,13 @@ TEST(VulkanIcdSelectionTest, KeepsDirectoryPrecedenceOverName) {
TEST(VulkanIcdSelectionTest, IsStableWhateverTheDirectoryOrderIs) {
TemporaryDirectory temporary;
const std::string native_name =
"nvidia_icd." + std::string(compat::kGuestCpuName) + ".json";
WriteManifest(temporary.root() / native_name);
WriteManifest(temporary.root() / "nvidia_icd.x86_64.json");
WriteManifest(temporary.root() / "nvidia_icd.json");
WriteManifest(temporary.root() / "nvidia_icd.i686.json");
const std::string selected =
SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd");
EXPECT_EQ(selected, (temporary.root() / native_name).string());
EXPECT_EQ(selected, (temporary.root() / "nvidia_icd.x86_64.json").string());
EXPECT_EQ(selected, SelectVulkanIcdManifest({temporary.root()},
"nvidia_icd"));
}