mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-18 01:01:36 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bdd737d10 | ||
|
|
df253e786f | ||
|
|
53cd79b7da | ||
|
|
febebef554 | ||
|
|
23fe2eeca5 | ||
|
|
6f5de3a31d | ||
|
|
ea5929faf6 |
No files matched your search
@@ -1,27 +1,9 @@
|
||||
name: Flatpak
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.github/workflows/flatpak.yml'
|
||||
- '.gitmodules'
|
||||
- 'CMakeLists.txt'
|
||||
- 'LICENSE'
|
||||
- 'assets/screenshots/**'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'packaging/discord-join.html'
|
||||
- 'packaging/icons/**'
|
||||
- 'packaging/space.bigrat.mocktail.desktop'
|
||||
- 'packaging/space.bigrat.mocktail.metainfo.xml'
|
||||
- 'packaging/space.bigrat.mocktail.svg'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'third_party/**'
|
||||
workflow_run:
|
||||
workflows: [Native packages]
|
||||
types: [completed]
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/flatpak.yml'
|
||||
@@ -39,30 +21,69 @@ on:
|
||||
- 'packaging/space.bigrat.mocktail.metainfo.xml'
|
||||
- 'packaging/space.bigrat.mocktail.svg'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'scripts/assemble_native_repositories.sh'
|
||||
- 'site/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'third_party/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: flatpak-${{ github.ref }}
|
||||
group: flatpak-${{ github.event.workflow_run.head_sha || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
flatpak:
|
||||
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: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
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:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Download package repositories
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
run_id="${{ github.event.workflow_run.id }}"
|
||||
if [[ -z "$run_id" ]]; then
|
||||
run_id="$(gh run list \
|
||||
--workflow packages.yml \
|
||||
--branch main \
|
||||
--event push \
|
||||
--status success \
|
||||
--limit 1 \
|
||||
--json databaseId \
|
||||
--jq '.[0].databaseId')"
|
||||
fi
|
||||
[[ -n "$run_id" ]]
|
||||
gh run download "$run_id" \
|
||||
--name Mocktail-native-repositories \
|
||||
--dir native-repositories
|
||||
[[ -d native-repositories/apt && \
|
||||
-d native-repositories/rpm && \
|
||||
-d native-repositories/downloads && \
|
||||
-s native-repositories/mocktail-packages.gpg ]]
|
||||
|
||||
- name: Install Flatpak builder
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -78,13 +99,10 @@ jobs:
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$GITHUB_SHA"
|
||||
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
publish=false
|
||||
if [[ "$GITHUB_REF" == refs/heads/main && \
|
||||
"$GITHUB_EVENT_NAME" != pull_request ]]; then
|
||||
publish=true
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
|
||||
-n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
@@ -110,7 +128,7 @@ jobs:
|
||||
"${builder_signing[@]}" \
|
||||
build-flatpak-ci \
|
||||
packaging/flatpak/space.bigrat.mocktail.json
|
||||
if [[ "$publish" == true ]]; then
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
@@ -126,9 +144,10 @@ jobs:
|
||||
Mocktail-x86_64.flatpak \
|
||||
space.bigrat.mocktail \
|
||||
stable
|
||||
if [[ "$publish" == true ]]; then
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg public
|
||||
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg \
|
||||
public native-repositories
|
||||
fi
|
||||
|
||||
- name: Upload Flatpak
|
||||
@@ -140,18 +159,24 @@ jobs:
|
||||
retention-days: 14
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
|
||||
- name: Upload GitHub Pages repository
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: public
|
||||
|
||||
deploy-pages:
|
||||
name: Publish Flatpak repository
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
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: 10
|
||||
|
||||
@@ -5,27 +5,21 @@ on:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.github/workflows/flatpak.yml'
|
||||
- '.gitignore'
|
||||
- 'README.md'
|
||||
- 'assets/screenshots/**'
|
||||
- 'docs/**'
|
||||
- 'packaging/aur/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'scripts/build_flatpak.sh'
|
||||
- 'tests/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.github/workflows/flatpak.yml'
|
||||
- '.gitignore'
|
||||
- 'README.md'
|
||||
- 'assets/screenshots/**'
|
||||
- 'docs/**'
|
||||
- 'packaging/aur/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'scripts/build_flatpak.sh'
|
||||
- 'tests/**'
|
||||
workflow_dispatch:
|
||||
@@ -121,9 +115,11 @@ jobs:
|
||||
- name: Build DEB
|
||||
shell: bash
|
||||
run: |
|
||||
nightly_suffix=".nightly.$(git show -s --format=%cd --date=format:%Y%m%d%H%M%S "$GITHUB_SHA").g${GITHUB_SHA:0:8}"
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION_SUFFIX="$nightly_suffix" \
|
||||
ubuntu:26.04 bash -Eeuo pipefail -c '
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
@@ -138,6 +134,8 @@ jobs:
|
||||
cmake -S . -B build-deb -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DMOCKTAIL_PACKAGE_NAME=mocktail-nightly \
|
||||
-DMOCKTAIL_PACKAGE_VERSION_SUFFIX="$MOCKTAIL_PACKAGE_VERSION_SUFFIX" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-deb --parallel 4
|
||||
cpack --config build-deb/CPackConfig.cmake \
|
||||
@@ -167,9 +165,11 @@ jobs:
|
||||
- name: Build RPM
|
||||
shell: bash
|
||||
run: |
|
||||
nightly_suffix=".nightly.$(git show -s --format=%cd --date=format:%Y%m%d%H%M%S "$GITHUB_SHA").g${GITHUB_SHA:0:8}"
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION_SUFFIX="$nightly_suffix" \
|
||||
fedora:44 bash -Eeuo pipefail -c '
|
||||
dnf install --assumeyes \
|
||||
gcc-c++ cmake git ninja-build pkgconf-pkg-config rpm-build \
|
||||
@@ -182,6 +182,8 @@ jobs:
|
||||
cmake -S . -B build-rpm -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DMOCKTAIL_PACKAGE_NAME=mocktail-nightly \
|
||||
-DMOCKTAIL_PACKAGE_VERSION_SUFFIX="$MOCKTAIL_PACKAGE_VERSION_SUFFIX" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-rpm --parallel 4
|
||||
cpack --config build-rpm/CPackConfig.cmake \
|
||||
@@ -243,7 +245,7 @@ jobs:
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
needs: [appimage, deb, rpm, arch]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -259,13 +261,52 @@ jobs:
|
||||
path: release
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare continuous packages
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
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
|
||||
find release -maxdepth 1 -type f -print
|
||||
|
||||
- name: Build package repositories
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
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" ]]
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
apt-utils createrepo-c gnupg rpm
|
||||
install -d stable/deb stable/rpm
|
||||
stable_tag="$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq .tag_name)"
|
||||
gh release download "$stable_tag" --pattern '*.deb' --dir stable/deb
|
||||
gh release download "$stable_tag" --pattern '*.rpm' --dir stable/rpm
|
||||
mapfile -t stable_debs < <(find stable/deb -maxdepth 1 -type f -name '*.deb' -print)
|
||||
mapfile -t stable_rpms < <(find stable/rpm -maxdepth 1 -type f -name '*.rpm' -print)
|
||||
[[ "${#stable_debs[@]}" -eq 1 && "${#stable_rpms[@]}" -eq 1 ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-native-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-native-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-native-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-packages.gpg
|
||||
./scripts/assemble_native_repositories.sh \
|
||||
"${stable_debs[0]}" release/mocktail-nightly.deb \
|
||||
"${stable_rpms[0]}" release/mocktail-nightly.rpm \
|
||||
mocktail-packages.gpg "$FLATPAK_GPG_KEY_ID" \
|
||||
"$RUNNER_TEMP/mocktail-native-gnupg" native-repositories
|
||||
|
||||
- name: Update continuous release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
find release -maxdepth 1 -type f -print
|
||||
git config user.name komaruworld
|
||||
git config user.email rtysus228@gmail.com
|
||||
git tag --force continuous "$GITHUB_SHA"
|
||||
@@ -277,3 +318,20 @@ jobs:
|
||||
--notes "Automated development packages from main."
|
||||
fi
|
||||
gh release upload continuous release/* --clobber
|
||||
while IFS= read -r asset; do
|
||||
case "$asset" in
|
||||
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
|
||||
;;
|
||||
esac
|
||||
done < <(gh release view continuous --json assets --jq '.assets[].name')
|
||||
|
||||
- name: Upload package repositories
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-native-repositories
|
||||
path: native-repositories
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
+71
-2
@@ -8,7 +8,7 @@
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Mocktail
|
||||
VERSION 1.0.1
|
||||
VERSION 1.0.2
|
||||
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
@@ -16,6 +16,40 @@ project(Mocktail
|
||||
include(GNUInstallDirs)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
set(MOCKTAIL_BUILD_GIT_COMMIT "unknown")
|
||||
set(MOCKTAIL_BUILD_GIT_DIRTY "unknown")
|
||||
find_package(Git QUIET)
|
||||
if(Git_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||
execute_process(
|
||||
COMMAND "${GIT_EXECUTABLE}" rev-parse --short=12 HEAD
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
RESULT_VARIABLE MOCKTAIL_GIT_REV_PARSE_RESULT
|
||||
OUTPUT_VARIABLE MOCKTAIL_GIT_REV_PARSE_OUTPUT
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
if(MOCKTAIL_GIT_REV_PARSE_RESULT EQUAL 0 AND
|
||||
MOCKTAIL_GIT_REV_PARSE_OUTPUT MATCHES "^[0-9a-fA-F]+$")
|
||||
string(TOLOWER "${MOCKTAIL_GIT_REV_PARSE_OUTPUT}"
|
||||
MOCKTAIL_BUILD_GIT_COMMIT)
|
||||
execute_process(
|
||||
COMMAND "${GIT_EXECUTABLE}" status --porcelain --untracked-files=normal
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
RESULT_VARIABLE MOCKTAIL_GIT_STATUS_RESULT
|
||||
OUTPUT_VARIABLE MOCKTAIL_GIT_STATUS_OUTPUT
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
if(MOCKTAIL_GIT_STATUS_RESULT EQUAL 0)
|
||||
if(MOCKTAIL_GIT_STATUS_OUTPUT STREQUAL "")
|
||||
set(MOCKTAIL_BUILD_GIT_DIRTY "false")
|
||||
else()
|
||||
set(MOCKTAIL_BUILD_GIT_DIRTY "true")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST
|
||||
"${CMAKE_SOURCE_DIR}/config/roblox_compatibility.json"
|
||||
CACHE FILEPATH
|
||||
@@ -259,6 +293,7 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/runtime_config_bootstrap.cc
|
||||
src/runtime/runtime_config_file.cc
|
||||
src/runtime/runtime_paths.cc
|
||||
src/runtime/session_log.cc
|
||||
src/runtime/single_instance_lock.cc
|
||||
src/runtime/startup_state_machine.cc
|
||||
src/runtime/supported_launch_policy.cc
|
||||
@@ -272,6 +307,17 @@ target_include_directories(mocktail_runtime PUBLIC
|
||||
target_compile_definitions(mocktail_runtime PRIVATE
|
||||
"MOCKTAIL_DISCORD_APPLICATION_ID=\"${MOCKTAIL_DISCORD_APPLICATION_ID}\""
|
||||
)
|
||||
set_property(SOURCE src/runtime/session_log.cc APPEND PROPERTY
|
||||
COMPILE_DEFINITIONS
|
||||
"MOCKTAIL_PROJECT_VERSION=\"${PROJECT_VERSION}\""
|
||||
"MOCKTAIL_BUILD_GIT_COMMIT=\"${MOCKTAIL_BUILD_GIT_COMMIT}\""
|
||||
"MOCKTAIL_BUILD_GIT_DIRTY=\"${MOCKTAIL_BUILD_GIT_DIRTY}\""
|
||||
"MOCKTAIL_BUILD_TYPE=\"$<CONFIG>\""
|
||||
"MOCKTAIL_BUILD_COMPILER_ID=\"${CMAKE_CXX_COMPILER_ID}\""
|
||||
"MOCKTAIL_BUILD_COMPILER_VERSION=\"${CMAKE_CXX_COMPILER_VERSION}\""
|
||||
"MOCKTAIL_BUILD_TARGET_SYSTEM=\"${CMAKE_SYSTEM_NAME}\""
|
||||
"MOCKTAIL_BUILD_TARGET_PROCESSOR=\"${CMAKE_SYSTEM_PROCESSOR}\""
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_runtime)
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
|
||||
CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16)
|
||||
@@ -671,7 +717,20 @@ install(CODE "
|
||||
endif()
|
||||
")
|
||||
|
||||
set(CPACK_PACKAGE_NAME "mocktail")
|
||||
set(MOCKTAIL_PACKAGE_NAME "mocktail" CACHE STRING
|
||||
"Native package name"
|
||||
)
|
||||
set(MOCKTAIL_PACKAGE_VERSION_SUFFIX "" CACHE STRING
|
||||
"Native package version suffix"
|
||||
)
|
||||
if(NOT MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail" AND
|
||||
NOT MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail-nightly")
|
||||
message(FATAL_ERROR
|
||||
"MOCKTAIL_PACKAGE_NAME must be mocktail or mocktail-nightly")
|
||||
endif()
|
||||
|
||||
set(CPACK_PACKAGE_NAME "${MOCKTAIL_PACKAGE_NAME}")
|
||||
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}${MOCKTAIL_PACKAGE_VERSION_SUFFIX}")
|
||||
set(CPACK_PACKAGE_VENDOR "Mocktail Project")
|
||||
set(CPACK_PACKAGE_CONTACT "komaruworld <rtysus228@gmail.com>")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/komaruworld/mocktail")
|
||||
@@ -693,6 +752,16 @@ set(CPACK_RPM_FILE_NAME RPM-DEFAULT)
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "Apache-2.0")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||
set(CPACK_RPM_PACKAGE_AUTOREQPROV ON)
|
||||
|
||||
if(MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail-nightly")
|
||||
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "mocktail")
|
||||
set(CPACK_DEBIAN_PACKAGE_REPLACES "mocktail")
|
||||
set(CPACK_RPM_PACKAGE_CONFLICTS "mocktail")
|
||||
else()
|
||||
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "mocktail-nightly")
|
||||
set(CPACK_DEBIAN_PACKAGE_REPLACES "mocktail-nightly")
|
||||
set(CPACK_RPM_PACKAGE_CONFLICTS "mocktail-nightly")
|
||||
endif()
|
||||
include(CPack)
|
||||
|
||||
add_dependencies(mocktail all_stubs)
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2026 komaruworld
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.DEFAULT_GOAL := all
|
||||
.PHONY: all build release debug test apk install register-url-handler release-runtime portable portable-test standalone appimage flatpak run-flatpak run run-smoke run-unlimited run-game run-input run-resize run-network auto-run run-gles run-angle update-roblox update-auto update-auto-launch install-updater payload-status payload-rollback support-bundle clean submodules help
|
||||
.PHONY: all build release debug test apk install register-url-handler release-runtime portable portable-test standalone appimage flatpak run-flatpak run run-smoke run-unlimited run-game run-input run-resize run-network auto-run run-gles run-angle update-roblox update-auto update-auto-launch payload-status payload-rollback support-bundle clean submodules help
|
||||
|
||||
BUILD_DIR := build
|
||||
BINARY := $(BUILD_DIR)/mocktail
|
||||
@@ -141,9 +141,6 @@ update-auto-launch: build ## Update Roblox and launch the verified current paylo
|
||||
@./$(BUILD_DIR)/mocktail_updater update
|
||||
@./$(BINARY)
|
||||
|
||||
install-updater: ## Install and enable the six-hour systemd user update timer
|
||||
@./scripts/install_user_updater.sh
|
||||
|
||||
payload-status: build ## Show current and previous-good immutable payloads
|
||||
@./$(BUILD_DIR)/mocktail_updater status
|
||||
|
||||
|
||||
@@ -24,6 +24,15 @@ an update fails.
|
||||
|
||||
## Install with Flatpak
|
||||
|
||||
Install the stable release from Flathub:
|
||||
|
||||
```bash
|
||||
flatpak install flathub space.bigrat.mocktail
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
Nightly builds are produced automatically from the latest `main` branch:
|
||||
|
||||
```bash
|
||||
flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref
|
||||
flatpak run space.bigrat.mocktail
|
||||
@@ -44,6 +53,42 @@ Use `mocktail` instead of `mocktail-git` to build the pinned release from
|
||||
source, or install the stable prebuilt package with `paru -S mocktail-bin` or
|
||||
`yay -S mocktail-bin`.
|
||||
|
||||
## Install with DNF
|
||||
|
||||
Fedora 44 users can install either the stable `mocktail` package or the
|
||||
`mocktail-nightly` package built from `main`:
|
||||
|
||||
```bash
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/rpm/mocktail.repo \
|
||||
-o /etc/yum.repos.d/mocktail.repo
|
||||
sudo dnf install mocktail
|
||||
# or
|
||||
sudo dnf install mocktail-nightly
|
||||
```
|
||||
|
||||
## Install with APT
|
||||
|
||||
Ubuntu 26.04 users can install either the stable `mocktail` package or the
|
||||
`mocktail-nightly` package built from `main`:
|
||||
|
||||
```bash
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/mocktail-packages.gpg \
|
||||
-o /etc/apt/keyrings/mocktail.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/mocktail.gpg] https://mocktail.bigrat.space/apt mocktail main" | \
|
||||
sudo tee /etc/apt/sources.list.d/mocktail.list >/dev/null
|
||||
sudo apt update
|
||||
sudo apt install mocktail
|
||||
# or
|
||||
sudo apt install mocktail-nightly
|
||||
```
|
||||
|
||||
## Direct downloads
|
||||
|
||||
Direct AppImage, DEB, and RPM downloads are available from the
|
||||
[Website](https://mocktail.bigrat.space/) and
|
||||
[GitHub Releases](https://github.com/komaruworld/mocktail/releases).
|
||||
|
||||
<details>
|
||||
<summary>Screenshots</summary>
|
||||
|
||||
@@ -55,26 +100,6 @@ source, or install the stable prebuilt package with `paru -S mocktail-bin` or
|
||||
|
||||
</details>
|
||||
|
||||
## Other Linux packages
|
||||
|
||||
The latest AppImage, DEB, RPM, and Arch packages are attached to the
|
||||
[continuous release](https://github.com/komaruworld/mocktail/releases/tag/continuous).
|
||||
|
||||
```bash
|
||||
# AppImage
|
||||
chmod +x Mocktail-x86_64.AppImage
|
||||
./Mocktail-x86_64.AppImage
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install ./mocktail_*_amd64.deb
|
||||
|
||||
# Fedora
|
||||
sudo dnf install ./mocktail-*.x86_64.rpm
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -U ./mocktail-*-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Only Linux `x86_64` is supported. Building requires CMake 3.20+, Git,
|
||||
|
||||
@@ -112,6 +112,11 @@ updates:
|
||||
# backend, and promote only on success. An existing current payload is
|
||||
# preserved when probation fails.
|
||||
automatic: true
|
||||
# Boolean (default: true): testing track. Always try provider latest, even
|
||||
# when it has no verified payload profile, and do not download or activate
|
||||
# an older supported payload in its place. Exact HostAbi derivation and two
|
||||
# canaries are still required before latest can become current.
|
||||
testing_latest_only: true
|
||||
# String (default: apk-pure): direct x86_64 APK provider.
|
||||
source: apk-pure
|
||||
# Reserved for desktop update integrations. `mocktail_updater` itself never
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[Unit]
|
||||
Description=Check, canary, and install the latest working Roblox for Mocktail
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=%h/.config/mocktail/updater.env
|
||||
ExecStart=/usr/bin/env ${MOCKTAIL_UPDATE_HELPER} update
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Periodically check Roblox updates for Mocktail
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5m
|
||||
OnUnitActiveSec=6h
|
||||
RandomizedDelaySec=15m
|
||||
Persistent=true
|
||||
Unit=mocktail-update.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -53,10 +53,8 @@ struct CgroupMemoryLimitResult {
|
||||
bool active() const { return status == CgroupMemoryLimitStatus::kActive; }
|
||||
};
|
||||
|
||||
// Replaces the current process with the same command inside a transient
|
||||
// systemd user scope when cgroup v2 delegation is available. A successful
|
||||
// re-exec does not return. Systems without systemd continue with the watchdog
|
||||
// fallback and receive kUnavailable.
|
||||
// Compatibility hook for the former cgroup re-exec path. Automatic cgroup
|
||||
// scope creation is disabled, so callers continue with the watchdog fallback.
|
||||
CgroupMemoryLimitResult MaybeReexecWithCgroupMemoryLimit(
|
||||
int argc, char* const argv[], std::uint64_t limit_bytes,
|
||||
const std::vector<std::string>* reexec_arguments = nullptr);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_SESSION_LOG_H_
|
||||
#define MOCKTAIL_RUNTIME_SESSION_LOG_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "runtime/environment.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct SessionBuildInformation {
|
||||
std::string version;
|
||||
std::string git_commit;
|
||||
std::string git_dirty;
|
||||
std::string build_type;
|
||||
std::string compiler;
|
||||
std::string target;
|
||||
};
|
||||
|
||||
SessionBuildInformation CurrentSessionBuildInformation();
|
||||
|
||||
class SessionLog final {
|
||||
public:
|
||||
SessionLog() = default;
|
||||
~SessionLog();
|
||||
|
||||
SessionLog(const SessionLog&) = delete;
|
||||
SessionLog& operator=(const SessionLog&) = delete;
|
||||
SessionLog(SessionLog&& other) noexcept;
|
||||
SessionLog& operator=(SessionLog&& other) noexcept;
|
||||
|
||||
static SessionLog Start(const Environment& environment,
|
||||
const RuntimePaths& paths,
|
||||
std::chrono::system_clock::time_point started_at =
|
||||
std::chrono::system_clock::now());
|
||||
|
||||
bool active() const { return active_; }
|
||||
bool attempted() const { return attempted_; }
|
||||
explicit operator bool() const { return active_; }
|
||||
|
||||
const std::filesystem::path& path() const { return path_; }
|
||||
const std::filesystem::path& latest_path() const { return latest_path_; }
|
||||
const std::string& error() const { return error_; }
|
||||
const std::string& warning() const { return warning_; }
|
||||
|
||||
std::string Header(const Environment& environment, const RuntimePaths& paths,
|
||||
std::string_view graphics_backend) const;
|
||||
|
||||
private:
|
||||
void Stop();
|
||||
|
||||
bool attempted_ = false;
|
||||
bool active_ = false;
|
||||
int original_stdout_ = -1;
|
||||
int original_stderr_ = -1;
|
||||
int logger_process_ = -1;
|
||||
std::chrono::system_clock::time_point started_at_{};
|
||||
std::filesystem::path path_;
|
||||
std::filesystem::path latest_path_;
|
||||
std::string error_;
|
||||
std::string warning_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_SESSION_LOG_H_
|
||||
@@ -22,6 +22,7 @@ namespace mocktail::update {
|
||||
|
||||
struct UpdateConfig {
|
||||
bool automatic = true;
|
||||
bool testing_latest_only = true;
|
||||
bool launch_after_update = false;
|
||||
std::string source = "apk-pure";
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: komaruworld <rtysus228@gmail.com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.1
|
||||
pkgver=1.0.2
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgbase = mocktail
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 1.0.1
|
||||
pkgver = 1.0.2
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
arch = x86_64
|
||||
@@ -37,7 +37,7 @@ pkgbase = mocktail
|
||||
conflicts = mocktail-bin
|
||||
conflicts = mocktail-git
|
||||
options = !debug
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.1
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.2
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.1
|
||||
pkgver=1.0.2
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
<developer id="space.bigrat">
|
||||
<name>Mocktail Project Authors</name>
|
||||
</developer>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#CEB3EF</color>
|
||||
<color type="primary" scheme_preference="dark">#2F2A6B</color>
|
||||
</branding>
|
||||
<url type="homepage">https://github.com/komaruworld/mocktail</url>
|
||||
<description>
|
||||
<p>
|
||||
@@ -35,11 +39,31 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<launchable type="desktop-id">space.bigrat.mocktail.desktop</launchable>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="violence-cartoon">moderate</content_attribute>
|
||||
<content_attribute id="violence-fantasy">moderate</content_attribute>
|
||||
<content_attribute id="violence-realistic">moderate</content_attribute>
|
||||
<content_attribute id="social-chat">moderate</content_attribute>
|
||||
<content_attribute id="social-info">moderate</content_attribute>
|
||||
<content_attribute id="social-audio">moderate</content_attribute>
|
||||
<content_attribute id="social-contacts">intense</content_attribute>
|
||||
<content_attribute id="money-purchasing">intense</content_attribute>
|
||||
<content_attribute id="money-advertising">intense</content_attribute>
|
||||
<content_attribute id="money-gambling">moderate</content_attribute>
|
||||
</content_rating>
|
||||
<provides>
|
||||
<binary>mocktail</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="1.0.2" date="2026-08-14">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed the “Roblox is out of date” bug</li>
|
||||
<li>Improved logging</li>
|
||||
<li>Fixed issues with Discord RPC where the game you were playing wasn’t displayed</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.1" date="2026-08-13">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
@@ -9,7 +9,10 @@ readonly REPOSITORY="${1:?Flatpak repository 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}")"
|
||||
|
||||
[[ -d "${REPOSITORY}" && ! -L "${REPOSITORY}" ]] || {
|
||||
printf 'Flatpak repository is missing or unsafe: %s\n' "${REPOSITORY}" >&2
|
||||
@@ -23,6 +26,13 @@ readonly BASE_URL="https://mocktail.bigrat.space"
|
||||
printf 'Flatpak public key is missing or unsafe: %s\n' "${PUBLIC_KEY}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -d "${NATIVE_REPOSITORIES}/apt" &&
|
||||
-d "${NATIVE_REPOSITORIES}/rpm" &&
|
||||
-d "${NATIVE_REPOSITORIES}/downloads" &&
|
||||
-s "${NATIVE_REPOSITORIES}/mocktail-packages.gpg" ]] || {
|
||||
printf 'Native repositories are missing: %s\n' "${NATIVE_REPOSITORIES}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ ! -e "${OUTPUT}" ]] || {
|
||||
printf 'Pages output already exists: %s\n' "${OUTPUT}" >&2
|
||||
exit 1
|
||||
@@ -33,70 +43,47 @@ cp -a -- "${REPOSITORY}" "${OUTPUT}/repo"
|
||||
install -m 0644 -- "${BUNDLE}" "${OUTPUT}/Mocktail-x86_64.flatpak"
|
||||
install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/mocktail-flatpak.gpg"
|
||||
install -m 0644 -- \
|
||||
"$(dirname -- "${BASH_SOURCE[0]}")/../packaging/space.bigrat.mocktail.svg" \
|
||||
"${PROJECT_DIR}/packaging/space.bigrat.mocktail.svg" \
|
||||
"${OUTPUT}/mocktail.svg"
|
||||
install -m 0644 -- \
|
||||
"$(dirname -- "${BASH_SOURCE[0]}")/../packaging/discord-join.html" \
|
||||
"${PROJECT_DIR}/packaging/discord-join.html" \
|
||||
"${OUTPUT}/join.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/index.html" "${OUTPUT}/index.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/styles.css" "${OUTPUT}/styles.css"
|
||||
install -d -- "${OUTPUT}/packaging"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_DIR}/packaging/space.bigrat.mocktail.svg" \
|
||||
"${OUTPUT}/packaging/space.bigrat.mocktail.svg"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/apt" "${OUTPUT}/apt"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/rpm" "${OUTPUT}/rpm"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/downloads" "${OUTPUT}/downloads"
|
||||
install -m 0644 -- \
|
||||
"${NATIVE_REPOSITORIES}/mocktail-packages.gpg" \
|
||||
"${OUTPUT}/mocktail-packages.gpg"
|
||||
|
||||
readonly GPG_KEY="$(base64 --wrap=0 "${PUBLIC_KEY}")"
|
||||
|
||||
cat >"${OUTPUT}/mocktail.flatpakrepo" <<EOF
|
||||
[Flatpak Repo]
|
||||
Title=Mocktail
|
||||
Title=Mocktail Nightly
|
||||
Url=${BASE_URL}/repo/
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Development builds of Mocktail
|
||||
Description=Signed x86_64 Flatpak builds published from Mocktail main
|
||||
Comment=Nightly builds of Mocktail
|
||||
Description=Signed x86_64 nightly builds published from the latest Mocktail main branch
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
EOF
|
||||
|
||||
cat >"${OUTPUT}/mocktail.flatpakref" <<EOF
|
||||
[Flatpak Ref]
|
||||
Title=Mocktail
|
||||
Title=Mocktail Nightly
|
||||
Name=space.bigrat.mocktail
|
||||
Branch=stable
|
||||
Url=${BASE_URL}/repo/
|
||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Play Roblox on Linux
|
||||
Comment=Nightly build from the latest main branch
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
IsRuntime=false
|
||||
EOF
|
||||
|
||||
cat >"${OUTPUT}/index.html" <<'EOF'
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>Mocktail for Linux</title>
|
||||
<style>
|
||||
:root { font: 17px/1.55 system-ui, sans-serif; color-scheme: dark; }
|
||||
body { margin: 0; background: #0d1117; color: #e6edf3; }
|
||||
main { max-width: 680px; margin: 12vh auto; padding: 32px; }
|
||||
img { width: 88px; height: 88px; }
|
||||
h1 { font-size: clamp(2.5rem, 8vw, 4.5rem); margin: .2rem 0; }
|
||||
p { color: #9da7b3; }
|
||||
a.button { display: inline-block; margin: 16px 8px 16px 0; padding: 12px 18px;
|
||||
border-radius: 9px; background: #238636; color: white; text-decoration: none;
|
||||
font-weight: 700; }
|
||||
a.secondary { background: #21262d; }
|
||||
code { background: #161b22; border: 1px solid #30363d; border-radius: 6px;
|
||||
display: block; padding: 14px; overflow-wrap: anywhere; }
|
||||
</style>
|
||||
</head>
|
||||
<body><main>
|
||||
<img src="mocktail.svg" alt="Mocktail logo">
|
||||
<h1>Mocktail</h1>
|
||||
<p>Signed development Flatpak builds for Linux x86_64.</p>
|
||||
<a class="button" href="mocktail.flatpakref">Install with Flatpak</a>
|
||||
<a class="button secondary" href="https://github.com/komaruworld/mocktail">Source</a>
|
||||
<code>flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref</code>
|
||||
<p>Mocktail is an independent community project and is not affiliated with Roblox Corporation.</p>
|
||||
</main></body>
|
||||
</html>
|
||||
EOF
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -Eeuo pipefail
|
||||
umask 022
|
||||
|
||||
readonly STABLE_DEB="${1:?Stable DEB path is required}"
|
||||
readonly NIGHTLY_DEB="${2:?Nightly DEB path is required}"
|
||||
readonly STABLE_RPM="${3:?Stable RPM path is required}"
|
||||
readonly NIGHTLY_RPM="${4:?Nightly RPM path is required}"
|
||||
readonly PUBLIC_KEY="${5:?GPG public key path is required}"
|
||||
readonly GPG_KEY_ID="${6:?GPG key ID is required}"
|
||||
readonly GPG_HOME="${7:?GPG home path is required}"
|
||||
readonly OUTPUT="${8:?Repository output path is required}"
|
||||
readonly BASE_URL="https://mocktail.bigrat.space"
|
||||
|
||||
for command in apt-ftparchive createrepo_c dpkg-deb gpg gzip rpm rpmsign; do
|
||||
command -v "${command}" >/dev/null || {
|
||||
printf 'Required command is missing: %s\n' "${command}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
for package in "${STABLE_DEB}" "${NIGHTLY_DEB}" \
|
||||
"${STABLE_RPM}" "${NIGHTLY_RPM}"; do
|
||||
[[ -s "${package}" && ! -L "${package}" ]] || {
|
||||
printf 'Package is missing or unsafe: %s\n' "${package}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
[[ -s "${PUBLIC_KEY}" && ! -L "${PUBLIC_KEY}" ]] || {
|
||||
printf 'GPG public key is missing or unsafe: %s\n' "${PUBLIC_KEY}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -d "${GPG_HOME}" && ! -L "${GPG_HOME}" ]] || {
|
||||
printf 'GPG home is missing or unsafe: %s\n' "${GPG_HOME}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ ! -e "${OUTPUT}" ]] || {
|
||||
printf 'Repository output already exists: %s\n' "${OUTPUT}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$(dpkg-deb --field "${STABLE_DEB}" Package)" == mocktail ]]
|
||||
[[ "$(dpkg-deb --field "${NIGHTLY_DEB}" Package)" == mocktail-nightly ]]
|
||||
[[ "$(rpm --query --package --queryformat '%{NAME}' "${STABLE_RPM}")" == mocktail ]]
|
||||
[[ "$(rpm --query --package --queryformat '%{NAME}' "${NIGHTLY_RPM}")" == mocktail-nightly ]]
|
||||
|
||||
readonly APT_ROOT="${OUTPUT}/apt"
|
||||
readonly APT_POOL="${APT_ROOT}/pool/main/m/mocktail"
|
||||
readonly APT_DIST="${APT_ROOT}/dists/mocktail"
|
||||
readonly RPM_ROOT="${OUTPUT}/rpm/x86_64"
|
||||
readonly DOWNLOADS="${OUTPUT}/downloads"
|
||||
|
||||
install -d -- \
|
||||
"${APT_POOL}" "${APT_DIST}/main/binary-amd64" "${RPM_ROOT}" "${DOWNLOADS}"
|
||||
install -m 0644 -- "${STABLE_DEB}" "${APT_POOL}/$(basename -- "${STABLE_DEB}")"
|
||||
install -m 0644 -- "${NIGHTLY_DEB}" "${APT_POOL}/$(basename -- "${NIGHTLY_DEB}")"
|
||||
install -m 0644 -- "${STABLE_RPM}" "${RPM_ROOT}/$(basename -- "${STABLE_RPM}")"
|
||||
install -m 0644 -- "${NIGHTLY_RPM}" "${RPM_ROOT}/$(basename -- "${NIGHTLY_RPM}")"
|
||||
install -m 0644 -- "${STABLE_DEB}" "${DOWNLOADS}/mocktail.deb"
|
||||
install -m 0644 -- "${STABLE_RPM}" "${DOWNLOADS}/mocktail.rpm"
|
||||
install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/mocktail-packages.gpg"
|
||||
|
||||
(
|
||||
cd -- "${APT_ROOT}"
|
||||
apt-ftparchive packages pool/main >dists/mocktail/main/binary-amd64/Packages
|
||||
gzip -9n -c dists/mocktail/main/binary-amd64/Packages \
|
||||
>dists/mocktail/main/binary-amd64/Packages.gz
|
||||
apt-ftparchive \
|
||||
-o APT::FTPArchive::Release::Origin=Mocktail \
|
||||
-o APT::FTPArchive::Release::Label=Mocktail \
|
||||
-o APT::FTPArchive::Release::Suite=mocktail \
|
||||
-o APT::FTPArchive::Release::Codename=mocktail \
|
||||
-o APT::FTPArchive::Release::Architectures=amd64 \
|
||||
-o APT::FTPArchive::Release::Components=main \
|
||||
release dists/mocktail >dists/mocktail/Release
|
||||
)
|
||||
|
||||
gpg --batch --yes --homedir "${GPG_HOME}" --local-user "${GPG_KEY_ID}" \
|
||||
--digest-algo SHA256 --clearsign \
|
||||
--output "${APT_DIST}/InRelease" "${APT_DIST}/Release"
|
||||
gpg --batch --yes --homedir "${GPG_HOME}" --local-user "${GPG_KEY_ID}" \
|
||||
--digest-algo SHA256 --armor --detach-sign \
|
||||
--output "${APT_DIST}/Release.gpg" "${APT_DIST}/Release"
|
||||
|
||||
for package in "${RPM_ROOT}"/*.rpm; do
|
||||
GNUPGHOME="${GPG_HOME}" rpmsign --addsign \
|
||||
--define "_gpg_name ${GPG_KEY_ID}" \
|
||||
--define "_gpg_path ${GPG_HOME}" \
|
||||
"${package}"
|
||||
done
|
||||
|
||||
createrepo_c "${RPM_ROOT}"
|
||||
gpg --batch --yes --homedir "${GPG_HOME}" --local-user "${GPG_KEY_ID}" \
|
||||
--digest-algo SHA256 --armor --detach-sign \
|
||||
--output "${RPM_ROOT}/repodata/repomd.xml.asc" \
|
||||
"${RPM_ROOT}/repodata/repomd.xml"
|
||||
|
||||
cat >"${OUTPUT}/rpm/mocktail.repo" <<EOF
|
||||
[mocktail]
|
||||
name=Mocktail
|
||||
baseurl=${BASE_URL}/rpm/\$basearch/
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
repo_gpgcheck=1
|
||||
gpgkey=${BASE_URL}/mocktail-packages.gpg
|
||||
EOF
|
||||
@@ -86,6 +86,7 @@ LAUNCH="${MOCKTAIL_UPDATE_LAUNCH:-false}"
|
||||
SKIP_BUILD=false
|
||||
SCHEDULED=false
|
||||
STARTUP_PREFLIGHT=false
|
||||
TESTING_LATEST_ONLY=true
|
||||
UPDATE_CONFIG_JSON="{}"
|
||||
STAGED_PAYLOAD_ID=""
|
||||
DOWNLOAD_STAGING_ROOT="${CACHE_ROOT}/downloads/apk-staging"
|
||||
@@ -113,7 +114,8 @@ Options:
|
||||
--skip-build Reuse the existing Release binary for the canary.
|
||||
--scheduled Run only when updates.automatic is true in YAML.
|
||||
--startup-preflight Validate a runnable local payload without checking for
|
||||
remote updates; provision one when none is available.
|
||||
remote updates, unless updates.testing_latest_only is
|
||||
enabled; provision one when none is available.
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
@@ -188,6 +190,8 @@ ParseArguments() {
|
||||
LoadYamlDefaults() {
|
||||
UPDATE_CONFIG_JSON="$(python3 "${SCRIPT_DIR}/read_update_config.py" "${CONFIG_FILE}")"
|
||||
[[ -n "${SOURCE}" ]] || SOURCE="$(jq -r '.source // "apk-pure"' <<<"${UPDATE_CONFIG_JSON}")"
|
||||
TESTING_LATEST_ONLY="$(jq -r '.testing_latest_only // true' \
|
||||
<<<"${UPDATE_CONFIG_JSON}")"
|
||||
if [[ "${LAUNCH}" != true &&
|
||||
"$(jq -r '.launch_after_update // false' <<<"${UPDATE_CONFIG_JSON}")" == true ]]; then
|
||||
LAUNCH=true
|
||||
@@ -562,6 +566,9 @@ SkipDownloadWhenCurrent() {
|
||||
Log "latest-version API check failed; preserving the runnable local payload"
|
||||
return 0
|
||||
fi
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
Die "updates.testing_latest_only requires available provider latest metadata"
|
||||
fi
|
||||
Log "latest-version API check failed and no runnable local payload exists; using the compatibility bootstrap profile"
|
||||
SelectSupportedBootstrapFallback
|
||||
return 1
|
||||
@@ -572,6 +579,9 @@ SkipDownloadWhenCurrent() {
|
||||
Log "latest-version API returned invalid metadata; preserving the runnable local payload"
|
||||
return 0
|
||||
fi
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
Die "updates.testing_latest_only requires valid provider latest metadata"
|
||||
fi
|
||||
Log "provider returned invalid version metadata and no runnable local payload exists; using the compatibility bootstrap profile"
|
||||
SelectSupportedBootstrapFallback
|
||||
return 1
|
||||
@@ -605,6 +615,11 @@ SkipRemoteUpdateForStartup() {
|
||||
Die "current payload is not runnable by this Mocktail runtime and rollback failed"
|
||||
fi
|
||||
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
Log "testing latest-only track enabled; checking provider latest during startup"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local current_payload_id
|
||||
current_payload_id="$(VerifiedCurrentPayloadId 2>/dev/null || true)"
|
||||
if [[ "${current_payload_id}" =~ ^[0-9]+-[0-9a-f]{40}$ ]]; then
|
||||
@@ -1551,6 +1566,9 @@ Main() {
|
||||
LoadYamlDefaults
|
||||
ParseArguments "$@"
|
||||
[[ "${SOURCE}" == apk-pure ]] || Die "update source must be apk-pure"
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true && -n "${VERSION}" ]]; then
|
||||
Die "--version cannot be combined with updates.testing_latest_only"
|
||||
fi
|
||||
if [[ "${SCHEDULED}" == true &&
|
||||
"$(jq -r 'if has("automatic") then .automatic else true end' \
|
||||
<<<"${UPDATE_CONFIG_JSON}")" != true ]]; then
|
||||
@@ -1585,6 +1603,10 @@ Main() {
|
||||
PreserveCurrentAfterUpdateFailure "latest payload acquisition failed" \
|
||||
"${pre_stage_payload_id}" &&
|
||||
return 0
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
Log "latest payload acquisition failed; testing latest-only track forbids a supported fallback"
|
||||
return "${stage_status}"
|
||||
fi
|
||||
local bootstrap_ceiling=""
|
||||
if [[ "${BOOTSTRAP_SELECTED_VERSION_CODE}" =~ ^[0-9]+$ ]]; then
|
||||
(( 10#${BOOTSTRAP_SELECTED_VERSION_CODE} > 1 )) ||
|
||||
@@ -1618,6 +1640,16 @@ Main() {
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
if [[ "${had_current}" == true ]]; then
|
||||
PreserveCurrentAfterUpdateFailure \
|
||||
"latest candidate ${latest_payload_id} failed probation" \
|
||||
"${LAST_ATTEMPT_BASELINE_PAYLOAD_ID}"
|
||||
return 0
|
||||
fi
|
||||
Log "latest candidate ${latest_payload_id} failed probation; testing latest-only track forbids a supported fallback"
|
||||
return "${attempt_status}"
|
||||
fi
|
||||
if [[ "${latest_needs_reference}" != true && "${had_current}" == true ]]; then
|
||||
PreserveCurrentAfterUpdateFailure \
|
||||
"latest candidate ${latest_payload_id} failed probation" \
|
||||
|
||||
@@ -1088,9 +1088,13 @@ def validate_semantic_match(
|
||||
raise AnalyzerError("registry initializer object size is inconsistent")
|
||||
|
||||
|
||||
def find_signature_match(
|
||||
reference: ElfImage, candidate: ElfImage, spec: SignatureSpec
|
||||
) -> SignatureMatch:
|
||||
def find_signature_matches(
|
||||
reference: ElfImage,
|
||||
candidate: ElfImage,
|
||||
spec: SignatureSpec,
|
||||
*,
|
||||
allow_multiple_candidates: bool = False,
|
||||
) -> tuple[SignatureMatch, ...]:
|
||||
reference_instructions = disassemble(
|
||||
reference, spec.reference_rva, spec.instruction_count
|
||||
)
|
||||
@@ -1131,18 +1135,59 @@ def find_signature_match(
|
||||
):
|
||||
matches.append(signature_offset)
|
||||
search_offset = anchor_position + 1
|
||||
if not matches:
|
||||
raise AnalyzerError(
|
||||
f"signature {spec.name} matched 0 candidate locations"
|
||||
)
|
||||
if not allow_multiple_candidates and len(matches) != 1:
|
||||
raise AnalyzerError(
|
||||
f"signature {spec.name} matched {len(matches)} candidate locations"
|
||||
)
|
||||
semantic_matches = []
|
||||
semantic_errors = []
|
||||
for match in matches:
|
||||
match_rva = text.address + match - text.offset
|
||||
candidate_instructions = disassemble(
|
||||
candidate, match_rva, spec.instruction_count
|
||||
)
|
||||
try:
|
||||
validate_semantic_match(
|
||||
reference_instructions,
|
||||
candidate_instructions,
|
||||
spec.allow_registry_object_size_change,
|
||||
)
|
||||
except AnalyzerError as error:
|
||||
semantic_errors.append(error)
|
||||
continue
|
||||
semantic_matches.append(
|
||||
SignatureMatch(
|
||||
match_rva, reference_instructions, candidate_instructions
|
||||
)
|
||||
)
|
||||
if not semantic_matches and len(matches) == 1 and semantic_errors:
|
||||
raise semantic_errors[0]
|
||||
return tuple(semantic_matches)
|
||||
|
||||
|
||||
def find_signature_match(
|
||||
reference: ElfImage, candidate: ElfImage, spec: SignatureSpec
|
||||
) -> SignatureMatch:
|
||||
matches = find_signature_matches(reference, candidate, spec)
|
||||
if len(matches) != 1:
|
||||
raise AnalyzerError(
|
||||
f"signature {spec.name} matched {len(matches)} candidate locations"
|
||||
)
|
||||
match_rva = text.address + matches[0] - text.offset
|
||||
candidate_instructions = disassemble(candidate, match_rva, spec.instruction_count)
|
||||
validate_semantic_match(
|
||||
reference_instructions,
|
||||
candidate_instructions,
|
||||
spec.allow_registry_object_size_change,
|
||||
)
|
||||
return SignatureMatch(match_rva, reference_instructions, candidate_instructions)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def direct_call_target(instruction: Any, description: str) -> int:
|
||||
if (
|
||||
instruction.mnemonic != "call"
|
||||
or len(instruction.operands) != 1
|
||||
or instruction.operands[0].type != capstone_x86.X86_OP_IMM
|
||||
):
|
||||
raise AnalyzerError(f"{description} is not one direct call")
|
||||
return instruction.operands[0].imm
|
||||
|
||||
|
||||
def rip_targets(instruction: Any) -> tuple[int, ...]:
|
||||
@@ -1256,7 +1301,13 @@ def create_signature_specs(
|
||||
24,
|
||||
),
|
||||
SignatureSpec(
|
||||
"usable-size", parse_rva(bridges["usable-size"]["rva"], "usable size"), 24
|
||||
# This bridge is an 18-instruction leaf. Keeping the signature
|
||||
# bounded to the leaf avoids coupling it to padding or the next
|
||||
# function, which changed from executable code to INT3 padding in
|
||||
# Roblox versionCode 2904.
|
||||
"usable-size",
|
||||
parse_rva(bridges["usable-size"]["rva"], "usable size"),
|
||||
18,
|
||||
),
|
||||
SignatureSpec(
|
||||
"reallocate", parse_rva(bridges["reallocate"]["rva"], "reallocate"), 24
|
||||
@@ -1277,8 +1328,12 @@ def create_signature_specs(
|
||||
),
|
||||
SignatureSpec(
|
||||
"allocator-thread-initializer",
|
||||
parse_rva(data_seeds["allocator_thread_initializer"], "thread initializer"),
|
||||
24,
|
||||
parse_rva(
|
||||
data_seeds["allocator_thread_initializer"], "thread initializer"
|
||||
),
|
||||
# The wrapper ends at instruction 19. Later instructions belong
|
||||
# to an unrelated adjacent initializer.
|
||||
19,
|
||||
),
|
||||
SignatureSpec(
|
||||
"registry-initializer",
|
||||
@@ -1362,10 +1417,47 @@ def derive_profile(
|
||||
|
||||
reference_profile = reference_sidecar["profile"]
|
||||
anchors = reference_sidecar["derivation_anchors"]
|
||||
matches = {
|
||||
spec.name: find_signature_match(reference, candidate, spec)
|
||||
for spec in create_signature_specs(reference_profile, anchors)
|
||||
}
|
||||
matches = {}
|
||||
allocate_matches = ()
|
||||
for spec in create_signature_specs(reference_profile, anchors):
|
||||
if spec.name == "allocate":
|
||||
allocate_matches = find_signature_matches(
|
||||
reference, candidate, spec, allow_multiple_candidates=True
|
||||
)
|
||||
else:
|
||||
matches[spec.name] = find_signature_match(reference, candidate, spec)
|
||||
|
||||
# The allocate signature alone is intentionally ambiguous in 2904. Resolve
|
||||
# it through an independent invariant: the registry initializer directly
|
||||
# calls the allocator wrapper that Mocktail must publish in the profile.
|
||||
registry_match = matches["registry-initializer"]
|
||||
reference_allocate = parse_rva(
|
||||
bridge_map(reference_profile)["allocate"]["rva"],
|
||||
"reference allocate",
|
||||
)
|
||||
if (
|
||||
direct_call_target(
|
||||
registry_match.reference_instructions[9],
|
||||
"reference registry allocator call",
|
||||
)
|
||||
!= reference_allocate
|
||||
):
|
||||
raise AnalyzerError(
|
||||
"reference registry initializer does not call its allocator bridge"
|
||||
)
|
||||
candidate_allocate = direct_call_target(
|
||||
registry_match.candidate_instructions[9],
|
||||
"candidate registry allocator call",
|
||||
)
|
||||
selected_allocate = tuple(
|
||||
match for match in allocate_matches if match.rva == candidate_allocate
|
||||
)
|
||||
if len(selected_allocate) != 1:
|
||||
raise AnalyzerError(
|
||||
"signature allocate candidates do not identify exactly one "
|
||||
"registry allocator call"
|
||||
)
|
||||
matches["allocate"] = selected_allocate[0]
|
||||
reference_init = reference.init_array_relocations()
|
||||
candidate_init = candidate.init_array_relocations()
|
||||
if len(reference_init) != reference_profile["init_array_count"]:
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
|
||||
CONFIG_ROOT="${XDG_CONFIG_HOME:-${HOME}/.config}/mocktail"
|
||||
UNIT_ROOT="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
|
||||
CANARY_BINARY="${MOCKTAIL_UPDATE_CANARY_BIN:-}"
|
||||
UPDATE_HELPER="${MOCKTAIL_UPDATE_HELPER:-}"
|
||||
|
||||
if [[ -z "${CANARY_BINARY}" ]]; then
|
||||
for candidate in "${PROJECT_ROOT}/build/mocktail" \
|
||||
/usr/lib/mocktail/mocktail; do
|
||||
if [[ -f "${candidate}" && ! -L "${candidate}" && -x "${candidate}" ]]; then
|
||||
CANARY_BINARY="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ -z "${UPDATE_HELPER}" ]]; then
|
||||
for candidate in "${PROJECT_ROOT}/build/mocktail_updater" \
|
||||
/usr/lib/mocktail/mocktail_updater; do
|
||||
if [[ -f "${candidate}" && ! -L "${candidate}" && -x "${candidate}" ]]; then
|
||||
UPDATE_HELPER="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[[ "${CANARY_BINARY}" == /* && -f "${CANARY_BINARY}" &&
|
||||
! -L "${CANARY_BINARY}" && -x "${CANARY_BINARY}" ]] || {
|
||||
echo "build or install Mocktail before enabling its update timer" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "${UPDATE_HELPER}" == /* && -f "${UPDATE_HELPER}" &&
|
||||
! -L "${UPDATE_HELPER}" && -x "${UPDATE_HELPER}" ]] || {
|
||||
echo "build or install the native Mocktail updater before enabling its timer" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "${PROJECT_ROOT}" != *$'\n'* && "${PROJECT_ROOT}" != *'%'* ]] || {
|
||||
echo "unsupported project path for systemd environment file" >&2
|
||||
exit 1
|
||||
}
|
||||
mkdir -p "${CONFIG_ROOT}" "${UNIT_ROOT}"
|
||||
install -m 0644 "${PROJECT_ROOT}/config/systemd/mocktail-update.service" \
|
||||
"${UNIT_ROOT}/mocktail-update.service"
|
||||
install -m 0644 "${PROJECT_ROOT}/config/systemd/mocktail-update.timer" \
|
||||
"${UNIT_ROOT}/mocktail-update.timer"
|
||||
if [[ ! -e "${CONFIG_ROOT}/config.yaml" ]]; then
|
||||
install -m 0600 "${PROJECT_ROOT}/config/mocktail.example.yaml" \
|
||||
"${CONFIG_ROOT}/config.yaml"
|
||||
fi
|
||||
temporary="$(mktemp "${CONFIG_ROOT}/.updater.env.XXXXXX")"
|
||||
trap 'rm -f -- "${temporary}"' EXIT
|
||||
printf 'MOCKTAIL_UPDATE_HELPER=%q\n' "${UPDATE_HELPER}" > "${temporary}"
|
||||
printf 'MOCKTAIL_UPDATE_CANARY_BIN=%q\n' "${CANARY_BINARY}" >> "${temporary}"
|
||||
chmod 0600 "${temporary}"
|
||||
mv -f -- "${temporary}" "${CONFIG_ROOT}/updater.env"
|
||||
trap - EXIT
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now mocktail-update.timer
|
||||
systemctl --user list-timers mocktail-update.timer --no-pager
|
||||
@@ -105,13 +105,14 @@ def main() -> None:
|
||||
fail("updates must be a mapping")
|
||||
allowed = {
|
||||
"automatic",
|
||||
"testing_latest_only",
|
||||
"source",
|
||||
"launch_after_update",
|
||||
}
|
||||
unknown = set(updates) - allowed
|
||||
if unknown:
|
||||
fail(f"unknown updates key: {sorted(unknown)[0]}")
|
||||
for key in ("automatic", "launch_after_update"):
|
||||
for key in ("automatic", "testing_latest_only", "launch_after_update"):
|
||||
if key in updates and not isinstance(updates[key], bool):
|
||||
fail(f"updates.{key} must be true or false")
|
||||
if "source" in updates and updates["source"] not in SUPPORTED_SOURCES:
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Mocktail for Linux</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<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.</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">
|
||||
<p>Stable</p>
|
||||
<code>flatpak install flathub space.bigrat.mocktail</code>
|
||||
<p>Nightly</p>
|
||||
<code>flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref</code>
|
||||
</div>
|
||||
<h2>AUR packages</h2>
|
||||
<p>Available from the AUR. Choose one package:</p>
|
||||
<p class="links">
|
||||
<a href="https://aur.archlinux.org/packages/mocktail">mocktail</a>
|
||||
<a href="https://aur.archlinux.org/packages/mocktail-bin">mocktail-bin</a>
|
||||
<a href="https://aur.archlinux.org/packages/mocktail-git">mocktail-git</a>
|
||||
</p>
|
||||
<code>yay -S mocktail
|
||||
yay -S mocktail-bin
|
||||
yay -S mocktail-git</code>
|
||||
<h2>DNF repository</h2>
|
||||
<p>Stable and nightly packages for Fedora 44. Choose one package.</p>
|
||||
<code>sudo curl -fsSL https://mocktail.bigrat.space/rpm/mocktail.repo -o /etc/yum.repos.d/mocktail.repo</code>
|
||||
<div class="commands">
|
||||
<p>Stable</p>
|
||||
<code>sudo dnf install mocktail</code>
|
||||
<p>Nightly</p>
|
||||
<code>sudo dnf install mocktail-nightly</code>
|
||||
</div>
|
||||
<h2>APT repository</h2>
|
||||
<p>Stable and nightly packages for Ubuntu 26.04. Choose one package.</p>
|
||||
<code>sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/mocktail-packages.gpg -o /etc/apt/keyrings/mocktail.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/mocktail.gpg] https://mocktail.bigrat.space/apt mocktail main" | sudo tee /etc/apt/sources.list.d/mocktail.list >/dev/null
|
||||
sudo apt update</code>
|
||||
<div class="commands">
|
||||
<p>Stable</p>
|
||||
<code>sudo apt install mocktail</code>
|
||||
<p>Nightly</p>
|
||||
<code>sudo apt install mocktail-nightly</code>
|
||||
</div>
|
||||
<h2>Direct downloads</h2>
|
||||
<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>
|
||||
<a href="https://mocktail.bigrat.space/downloads/mocktail.deb">DEB</a>
|
||||
<a href="https://mocktail.bigrat.space/downloads/mocktail.rpm">RPM</a>
|
||||
</p>
|
||||
<p class="download-label">Nightly</p>
|
||||
<p class="links">
|
||||
<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>
|
||||
<p><a class="source" href="https://github.com/komaruworld/mocktail">Source code</a></p>
|
||||
<p>Mocktail is an independent community project and is not affiliated with Roblox Corporation.</p>
|
||||
</main></body>
|
||||
</html>
|
||||
@@ -0,0 +1,88 @@
|
||||
:root {
|
||||
font: 17px/1.55 system-ui, sans-serif;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0d1117;
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 680px;
|
||||
margin: 12vh auto;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: .2rem 0;
|
||||
font-size: clamp(2.5rem, 8vw, 4.5rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 32px 0 0;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #9da7b3;
|
||||
}
|
||||
|
||||
a.button {
|
||||
display: inline-block;
|
||||
margin: 16px 8px 16px 0;
|
||||
padding: 12px 18px;
|
||||
border-radius: 9px;
|
||||
background: #238636;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
background: #21262d;
|
||||
}
|
||||
|
||||
.commands {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.commands p,
|
||||
.download-label {
|
||||
margin: 14px 0 5px;
|
||||
color: #e6edf3;
|
||||
font-size: .82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
code {
|
||||
display: block;
|
||||
padding: 14px;
|
||||
overflow-wrap: anywhere;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
background: #161b22;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin: 10px 0 14px;
|
||||
}
|
||||
|
||||
.links a {
|
||||
display: inline-block;
|
||||
margin: 0 14px 6px 0;
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
a.source {
|
||||
color: #e6edf3;
|
||||
}
|
||||
@@ -18,8 +18,13 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
|
||||
#if defined(__GLIBC__)
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace mocktail::compat {
|
||||
namespace {
|
||||
|
||||
@@ -31,6 +36,34 @@ struct ThreadStartContext {
|
||||
|
||||
std::atomic<NativeThreadInitializer> g_thread_initializer{nullptr};
|
||||
|
||||
size_t HostStackSizeForGuest(size_t guest_stack_size) noexcept {
|
||||
#if defined(__GLIBC__)
|
||||
using GetStaticTlsInfo = void (*)(size_t*, size_t*);
|
||||
static const auto get_static_tls_info =
|
||||
reinterpret_cast<GetStaticTlsInfo>(
|
||||
dlsym(RTLD_DEFAULT, "_dl_get_tls_static_info"));
|
||||
size_t tls_size = 0;
|
||||
size_t tls_alignment = 0;
|
||||
if (get_static_tls_info != nullptr) {
|
||||
get_static_tls_info(&tls_size, &tls_alignment);
|
||||
}
|
||||
|
||||
// glibc places static TLS inside the requested mapping. Compensate only
|
||||
// unusually large blocks; Mocktail's compatibility TLS is several MiB.
|
||||
constexpr size_t kLargeStaticTlsThreshold = 1U * 1024U * 1024U;
|
||||
if (tls_size > kLargeStaticTlsThreshold && tls_alignment != 0 &&
|
||||
(tls_alignment & (tls_alignment - 1)) == 0 &&
|
||||
tls_size <= std::numeric_limits<size_t>::max() - (tls_alignment - 1)) {
|
||||
const size_t reserve =
|
||||
(tls_size + tls_alignment - 1) & ~(tls_alignment - 1);
|
||||
if (guest_stack_size <= std::numeric_limits<size_t>::max() - reserve) {
|
||||
return guest_stack_size + reserve;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return guest_stack_size;
|
||||
}
|
||||
|
||||
int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
pthread_attr_t* destination) noexcept {
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
@@ -62,7 +95,8 @@ int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setstacksize(destination, stack_size);
|
||||
result = pthread_attr_setstacksize(destination,
|
||||
HostStackSizeForGuest(stack_size));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
@@ -112,7 +146,8 @@ int CopyRequiredThreadAttributes(const pthread_attr_t& source,
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return pthread_attr_setstacksize(destination, stack_size);
|
||||
return pthread_attr_setstacksize(destination,
|
||||
HostStackSizeForGuest(stack_size));
|
||||
}
|
||||
|
||||
int ConfigureHostSafeStackFallback(const pthread_attr_t* source,
|
||||
@@ -139,9 +174,11 @@ int ConfigureHostSafeStackFallback(const pthread_attr_t* source,
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
const size_t guest_floor =
|
||||
std::max(guest_stack_size, kBionicLp64FallbackThreadStackSize);
|
||||
return pthread_attr_setstacksize(
|
||||
destination, std::max({guest_stack_size, host_default_stack_size,
|
||||
kBionicLp64FallbackThreadStackSize}));
|
||||
destination,
|
||||
std::max(host_default_stack_size, HostStackSizeForGuest(guest_floor)));
|
||||
}
|
||||
|
||||
void* RunGuestThread(void* raw_context) noexcept {
|
||||
@@ -183,8 +220,9 @@ int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr,
|
||||
if (result == 0) {
|
||||
normalized_attr_initialized = true;
|
||||
if (attr == nullptr) {
|
||||
result = pthread_attr_setstacksize(&normalized_attr,
|
||||
kBionicLp64DefaultThreadStackSize);
|
||||
result = pthread_attr_setstacksize(
|
||||
&normalized_attr,
|
||||
HostStackSizeForGuest(kBionicLp64DefaultThreadStackSize));
|
||||
} else {
|
||||
result = CopySupportedThreadAttributes(*attr, &normalized_attr);
|
||||
}
|
||||
|
||||
+28
-12
@@ -7,6 +7,7 @@
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -44,6 +45,7 @@
|
||||
#include "runtime/runtime_config_bootstrap.h"
|
||||
#include "runtime/runtime_config_file.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
#include "runtime/session_log.h"
|
||||
#include "runtime/single_instance_lock.h"
|
||||
#include "runtime/support_bundle.h"
|
||||
#include "runtime/supported_launch_policy.h"
|
||||
@@ -109,6 +111,7 @@ mocktail::Status ShutdownPlatformBridges(jnivm::VM* vm) {
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
const auto process_started_at = std::chrono::system_clock::now();
|
||||
mocktail::runtime::CommandLineParseResult command_line =
|
||||
mocktail::runtime::ParseCommandLine(argc, argv);
|
||||
if (!command_line) {
|
||||
@@ -164,6 +167,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
mocktail::runtime::ScrubCommandLineLaunchArguments(&command_line.options,
|
||||
argc, argv);
|
||||
mocktail::runtime::SessionLog session_log;
|
||||
mocktail::runtime::FailureSupportBundleGuard support_bundle_guard(
|
||||
environment, paths,
|
||||
command_line.options.mode == mocktail::runtime::CommandMode::kRun);
|
||||
@@ -221,10 +225,6 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (config_bootstrap.created()) {
|
||||
std::cout << " [runtime] created first-run configuration: "
|
||||
<< paths.config_file() << '\n';
|
||||
}
|
||||
mocktail::runtime::RuntimeConfigLoadResult runtime_config =
|
||||
mocktail::runtime::LoadRuntimeConfig(environment, paths.config_file());
|
||||
if (!runtime_config) {
|
||||
@@ -266,17 +266,33 @@ int main(int argc, char* argv[]) {
|
||||
use_memory_limit
|
||||
? runtime_config.config.performance().memory_limit_bytes()
|
||||
: 0;
|
||||
mocktail::runtime::CgroupMemoryLimitResult cgroup_limit;
|
||||
if (use_memory_limit) {
|
||||
cgroup_limit = mocktail::runtime::MaybeReexecWithCgroupMemoryLimit(
|
||||
argc, argv, memory_limit_bytes, &cgroup_reexec_arguments);
|
||||
}
|
||||
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
session_log = mocktail::runtime::SessionLog::Start(
|
||||
environment, paths, process_started_at);
|
||||
if (session_log) {
|
||||
std::cout << session_log.Header(
|
||||
environment, paths,
|
||||
runtime_config.config.graphics_backend_name())
|
||||
<< std::flush;
|
||||
} else if (session_log.attempted()) {
|
||||
std::cerr << " [session] automatic logging unavailable: "
|
||||
<< session_log.error() << '\n';
|
||||
}
|
||||
}
|
||||
if (config_bootstrap.created()) {
|
||||
std::cout << " [runtime] created first-run configuration: "
|
||||
<< paths.config_file() << '\n';
|
||||
}
|
||||
if (use_memory_limit) {
|
||||
const mocktail::runtime::CgroupMemoryLimitResult cgroup_limit =
|
||||
mocktail::runtime::MaybeReexecWithCgroupMemoryLimit(
|
||||
argc, argv, memory_limit_bytes, &cgroup_reexec_arguments);
|
||||
if (cgroup_limit.active()) {
|
||||
std::cout << " [memory] hard process-tree limit active: "
|
||||
<< runtime_config.config.performance().memory_limit_mb
|
||||
<< " MiB RAM, swap disabled\n";
|
||||
} else {
|
||||
std::cerr << " [memory] cgroup hard limit unavailable: "
|
||||
<< cgroup_limit.detail << "; using the RSS+swap watchdog\n";
|
||||
}
|
||||
}
|
||||
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
@@ -289,8 +305,8 @@ int main(int argc, char* argv[]) {
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
// Register immediately after any cgroup re-exec and before starting helper
|
||||
// processes or loading the Android payload. libgamemode caches its shared
|
||||
// Register before starting helper processes or loading the Android payload.
|
||||
// libgamemode caches its shared
|
||||
// D-Bus connection, so the first use must happen in the final game process.
|
||||
const mocktail::runtime::GameModePolicy game_mode_policy =
|
||||
runtime_config.config.performance().game_mode;
|
||||
|
||||
@@ -552,10 +552,12 @@ DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
activity.details = TruncateUtf8(config.text.browsing, 128);
|
||||
break;
|
||||
case RobloxExperiencePresencePhase::kJoining:
|
||||
activity.details = TruncateUtf8(config.text.joining, 128);
|
||||
break;
|
||||
case RobloxExperiencePresencePhase::kPlaying: {
|
||||
if (place_name.empty()) {
|
||||
if (phase == RobloxExperiencePresencePhase::kJoining) {
|
||||
activity.details = TruncateUtf8(config.text.joining, 128);
|
||||
break;
|
||||
}
|
||||
place_name = config.text.unknown_place;
|
||||
}
|
||||
if (config.show_place_name) {
|
||||
@@ -564,14 +566,16 @@ DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
activity.details = activity.state;
|
||||
activity.state.clear();
|
||||
}
|
||||
if (config.show_elapsed_time && session_started_at > 0) {
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
config.show_elapsed_time && session_started_at > 0) {
|
||||
activity.start_timestamp = session_started_at;
|
||||
}
|
||||
if (IsSafeExternalImageUrl(place_icon_url)) {
|
||||
activity.large_image = std::move(place_icon_url);
|
||||
activity.large_text = TruncateUtf8(place_name, 128);
|
||||
}
|
||||
if (config.join_enabled && request != nullptr &&
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
config.join_enabled && request != nullptr &&
|
||||
(!config.public_servers_only || IsPublicDiscordJoin(*request))) {
|
||||
activity.button_url = BuildDiscordJoinUrl(*request);
|
||||
if (!activity.button_url.empty()) {
|
||||
@@ -733,7 +737,8 @@ class DiscordRpcSession::Impl final {
|
||||
std::string place_name;
|
||||
std::string place_icon_url;
|
||||
bool resolve_metadata_after_publish = false;
|
||||
if (desired.phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
if ((desired.phase == RobloxExperiencePresencePhase::kJoining ||
|
||||
desired.phase == RobloxExperiencePresencePhase::kPlaying) &&
|
||||
desired.request.place_id > 0) {
|
||||
CachedPlaceMetadata& cached =
|
||||
place_metadata_[desired.request.place_id];
|
||||
@@ -741,7 +746,6 @@ class DiscordRpcSession::Impl final {
|
||||
place_icon_url = cached.icon_url;
|
||||
if ((cached.name.empty() || cached.icon_url.empty()) &&
|
||||
std::chrono::steady_clock::now() >= cached.next_attempt) {
|
||||
// Publish fallback activity while metadata is in flight.
|
||||
resolve_metadata_after_publish = true;
|
||||
}
|
||||
}
|
||||
@@ -761,7 +765,12 @@ class DiscordRpcSession::Impl final {
|
||||
std::fprintf(stderr, " [discord-rpc] connected to Discord Desktop\n");
|
||||
published_revision = 0;
|
||||
}
|
||||
if (published_revision != desired.revision) {
|
||||
const bool wait_for_place_metadata =
|
||||
(desired.phase == RobloxExperiencePresencePhase::kJoining ||
|
||||
desired.phase == RobloxExperiencePresencePhase::kPlaying) &&
|
||||
desired.request.place_id > 0 && place_name.empty();
|
||||
if (published_revision != desired.revision &&
|
||||
!wait_for_place_metadata) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
while (!activity_updates.empty() &&
|
||||
now - activity_updates.front() >= kDiscordActivityRateWindow) {
|
||||
@@ -824,7 +833,9 @@ class DiscordRpcSession::Impl final {
|
||||
if (Snapshot().revision != desired.revision) {
|
||||
continue;
|
||||
}
|
||||
if (changed) {
|
||||
if (changed &&
|
||||
(desired.phase == RobloxExperiencePresencePhase::kJoining ||
|
||||
desired.phase == RobloxExperiencePresencePhase::kPlaying)) {
|
||||
// Publish cached metadata through the normal rate limiter.
|
||||
published_revision = 0;
|
||||
continue;
|
||||
|
||||
+4
-162
@@ -15,7 +15,6 @@
|
||||
#include "runtime/memory_limit.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
@@ -27,9 +26,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
@@ -38,7 +35,6 @@ namespace {
|
||||
constexpr std::uint64_t kBytesPerKibibyte = 1024U;
|
||||
constexpr std::uint64_t kBytesPerMebibyte = 1024U * 1024U;
|
||||
constexpr int kWatchdogPollMilliseconds = 100;
|
||||
constexpr char kCgroupMarker[] = "MOCKTAIL_MEMORY_CGROUP_LIMIT_BYTES";
|
||||
|
||||
void WriteAllBestEffort(int descriptor, std::string_view bytes) {
|
||||
std::size_t offset = 0;
|
||||
@@ -115,90 +111,6 @@ ProcessMemoryUsage ReadCurrentProcessMemory() {
|
||||
std::string_view(buffer.data(), static_cast<std::size_t>(total)));
|
||||
}
|
||||
|
||||
bool ReadTextFile(const std::string& path, std::string* value) {
|
||||
std::ifstream input(path);
|
||||
if (!input) {
|
||||
return false;
|
||||
}
|
||||
return static_cast<bool>(input >> *value);
|
||||
}
|
||||
|
||||
bool ReadUnifiedCgroupPath(std::string* path) {
|
||||
std::ifstream input("/proc/self/cgroup");
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line.rfind("0::", 0) == 0) {
|
||||
*path = line.substr(3);
|
||||
return !path->empty() && path->front() == '/';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ActiveCgroupMatches(std::uint64_t expected_bytes,
|
||||
std::string* detail) {
|
||||
std::string cgroup_path;
|
||||
if (!ReadUnifiedCgroupPath(&cgroup_path)) {
|
||||
*detail = "cannot resolve the current unified cgroup";
|
||||
return false;
|
||||
}
|
||||
const std::string root = "/sys/fs/cgroup" + cgroup_path;
|
||||
std::string memory_max;
|
||||
std::string swap_max;
|
||||
if (!ReadTextFile(root + "/memory.max", &memory_max) ||
|
||||
!ReadTextFile(root + "/memory.swap.max", &swap_max)) {
|
||||
*detail = "cannot read current cgroup memory limits";
|
||||
return false;
|
||||
}
|
||||
std::uint64_t configured_bytes = 0;
|
||||
const std::from_chars_result parsed = std::from_chars(
|
||||
memory_max.data(), memory_max.data() + memory_max.size(),
|
||||
configured_bytes);
|
||||
if (parsed.ec != std::errc() ||
|
||||
parsed.ptr != memory_max.data() + memory_max.size() ||
|
||||
configured_bytes > expected_bytes) {
|
||||
*detail = "current cgroup memory.max does not match the requested limit";
|
||||
return false;
|
||||
}
|
||||
if (swap_max != "0") {
|
||||
*detail = "current cgroup still permits swap";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CurrentExecutable(char* const argv[]) {
|
||||
std::array<char, PATH_MAX + 1> executable{};
|
||||
const ssize_t length =
|
||||
readlink("/proc/self/exe", executable.data(), PATH_MAX);
|
||||
if (length > 0) {
|
||||
executable[static_cast<std::size_t>(length)] = '\0';
|
||||
return executable.data();
|
||||
}
|
||||
return argv != nullptr && argv[0] != nullptr ? argv[0] : std::string();
|
||||
}
|
||||
|
||||
bool SystemdUserManagerAvailable(std::string* detail) {
|
||||
if (access("/sys/fs/cgroup/cgroup.controllers", R_OK) != 0) {
|
||||
*detail = "cgroup v2 is not mounted";
|
||||
return false;
|
||||
}
|
||||
if (access("/usr/bin/systemd-run", X_OK) != 0) {
|
||||
*detail = "systemd-run is not installed";
|
||||
return false;
|
||||
}
|
||||
const char* runtime_directory = getenv("XDG_RUNTIME_DIR");
|
||||
const std::string user_runtime =
|
||||
runtime_directory != nullptr && runtime_directory[0] != '\0'
|
||||
? runtime_directory
|
||||
: "/run/user/" + std::to_string(geteuid());
|
||||
if (access((user_runtime + "/systemd/private").c_str(), F_OK) != 0) {
|
||||
*detail = "systemd user manager is not running";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProcessMemoryUsage ParseProcessStatusMemory(std::string_view status) {
|
||||
@@ -221,84 +133,14 @@ CgroupMemoryLimitResult MaybeReexecWithCgroupMemoryLimit(
|
||||
int argc, char* const argv[], std::uint64_t limit_bytes,
|
||||
const std::vector<std::string>* reexec_arguments) {
|
||||
CgroupMemoryLimitResult result;
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)reexec_arguments;
|
||||
if (limit_bytes == 0) {
|
||||
result.detail = "memory limit is disabled";
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* marker = getenv(kCgroupMarker);
|
||||
if (marker != nullptr) {
|
||||
std::uint64_t marked_bytes = 0;
|
||||
const std::string_view marker_value(marker);
|
||||
const std::from_chars_result parsed = std::from_chars(
|
||||
marker_value.data(), marker_value.data() + marker_value.size(),
|
||||
marked_bytes);
|
||||
if (parsed.ec == std::errc() &&
|
||||
parsed.ptr == marker_value.data() + marker_value.size() &&
|
||||
marked_bytes == limit_bytes &&
|
||||
ActiveCgroupMatches(limit_bytes, &result.detail)) {
|
||||
result.status = CgroupMemoryLimitStatus::kActive;
|
||||
result.detail = "cgroup v2 MemoryMax is active and swap is disabled";
|
||||
} else if (result.detail.empty()) {
|
||||
result.detail = "cgroup activation marker is invalid";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!SystemdUserManagerAvailable(&result.detail)) {
|
||||
return result;
|
||||
}
|
||||
const std::string executable = CurrentExecutable(argv);
|
||||
if (executable.empty()) {
|
||||
result.status = CgroupMemoryLimitStatus::kExecFailed;
|
||||
result.detail = "cannot resolve the Mocktail executable";
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::string limit = std::to_string(limit_bytes);
|
||||
std::vector<std::string> arguments;
|
||||
arguments.reserve((reexec_arguments != nullptr
|
||||
? reexec_arguments->size()
|
||||
: static_cast<std::size_t>(argc)) +
|
||||
10U);
|
||||
arguments.emplace_back("/usr/bin/systemd-run");
|
||||
arguments.emplace_back("--user");
|
||||
arguments.emplace_back("--scope");
|
||||
arguments.emplace_back("--quiet");
|
||||
arguments.emplace_back("--same-dir");
|
||||
arguments.emplace_back("--expand-environment=no");
|
||||
arguments.emplace_back("--property=MemoryMax=" + limit);
|
||||
arguments.emplace_back("--property=MemorySwapMax=0");
|
||||
arguments.emplace_back("--");
|
||||
arguments.push_back(executable);
|
||||
if (reexec_arguments != nullptr) {
|
||||
arguments.insert(arguments.end(), reexec_arguments->begin(),
|
||||
reexec_arguments->end());
|
||||
} else {
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
arguments.emplace_back(argv[index]);
|
||||
}
|
||||
}
|
||||
std::vector<char*> exec_arguments;
|
||||
exec_arguments.reserve(arguments.size() + 1U);
|
||||
for (std::string& argument : arguments) {
|
||||
exec_arguments.push_back(argument.data());
|
||||
}
|
||||
exec_arguments.push_back(nullptr);
|
||||
|
||||
if (setenv(kCgroupMarker, limit.c_str(), 1) != 0) {
|
||||
result.status = CgroupMemoryLimitStatus::kExecFailed;
|
||||
result.detail = std::string("cannot mark cgroup re-exec: ") +
|
||||
std::strerror(errno);
|
||||
return result;
|
||||
}
|
||||
(void)fflush(nullptr);
|
||||
execv(arguments.front().c_str(), exec_arguments.data());
|
||||
const int exec_error = errno;
|
||||
(void)unsetenv(kCgroupMarker);
|
||||
result.status = CgroupMemoryLimitStatus::kExecFailed;
|
||||
result.detail = std::string("cannot execute systemd-run: ") +
|
||||
std::strerror(exec_error);
|
||||
result.detail = "automatic cgroup scopes are disabled";
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +144,11 @@ updates:
|
||||
# backend, and promote only on success. An existing current payload is
|
||||
# preserved when probation fails.
|
||||
automatic: true
|
||||
# Boolean (default: true): testing track. Always try provider latest, even
|
||||
# when it has no verified payload profile, and do not download or activate
|
||||
# an older supported payload in its place. Exact HostAbi derivation and two
|
||||
# canaries are still required before latest can become current.
|
||||
testing_latest_only: true
|
||||
# String (default: apk-pure): direct x86_64 APK provider.
|
||||
source: apk-pure
|
||||
# Reserved for desktop update integrations. `mocktail_updater` itself never
|
||||
|
||||
@@ -0,0 +1,732 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
#include "runtime/session_log.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifndef MOCKTAIL_PROJECT_VERSION
|
||||
#define MOCKTAIL_PROJECT_VERSION "unknown"
|
||||
#endif
|
||||
#ifndef MOCKTAIL_BUILD_GIT_COMMIT
|
||||
#define MOCKTAIL_BUILD_GIT_COMMIT "unknown"
|
||||
#endif
|
||||
#ifndef MOCKTAIL_BUILD_GIT_DIRTY
|
||||
#define MOCKTAIL_BUILD_GIT_DIRTY "unknown"
|
||||
#endif
|
||||
#ifndef MOCKTAIL_BUILD_TYPE
|
||||
#define MOCKTAIL_BUILD_TYPE "unknown"
|
||||
#endif
|
||||
#ifndef MOCKTAIL_BUILD_COMPILER_ID
|
||||
#define MOCKTAIL_BUILD_COMPILER_ID "unknown"
|
||||
#endif
|
||||
#ifndef MOCKTAIL_BUILD_COMPILER_VERSION
|
||||
#define MOCKTAIL_BUILD_COMPILER_VERSION "unknown"
|
||||
#endif
|
||||
#ifndef MOCKTAIL_BUILD_TARGET_SYSTEM
|
||||
#define MOCKTAIL_BUILD_TARGET_SYSTEM "unknown"
|
||||
#endif
|
||||
#ifndef MOCKTAIL_BUILD_TARGET_PROCESSOR
|
||||
#define MOCKTAIL_BUILD_TARGET_PROCESSOR "unknown"
|
||||
#endif
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
bool Enabled(const Environment& environment, std::string_view name) {
|
||||
const auto value = environment.Get(name);
|
||||
return value.has_value() && !value->empty() && *value != "0";
|
||||
}
|
||||
|
||||
std::string LocalTimestamp(std::chrono::system_clock::time_point timestamp,
|
||||
std::string_view format) {
|
||||
const std::time_t raw = std::chrono::system_clock::to_time_t(timestamp);
|
||||
std::tm local = {};
|
||||
if (localtime_r(&raw, &local) == nullptr) return "unknown";
|
||||
std::array<char, 64> output{};
|
||||
const std::string owned_format(format);
|
||||
if (std::strftime(output.data(), output.size(), owned_format.c_str(),
|
||||
&local) == 0) {
|
||||
return "unknown";
|
||||
}
|
||||
return output.data();
|
||||
}
|
||||
|
||||
std::string IsoLocalTimestamp(std::chrono::system_clock::time_point timestamp) {
|
||||
std::string formatted = LocalTimestamp(timestamp, "%Y-%m-%dT%H:%M:%S%z");
|
||||
if (formatted.size() >= 5) {
|
||||
const std::size_t offset = formatted.size() - 5;
|
||||
if ((formatted[offset] == '+' || formatted[offset] == '-') &&
|
||||
formatted[offset + 1] >= '0' && formatted[offset + 1] <= '9' &&
|
||||
formatted[offset + 2] >= '0' && formatted[offset + 2] <= '9' &&
|
||||
formatted[offset + 3] >= '0' && formatted[offset + 3] <= '9' &&
|
||||
formatted[offset + 4] >= '0' && formatted[offset + 4] <= '9') {
|
||||
formatted.insert(formatted.size() - 2, ":");
|
||||
}
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
bool EnsurePrivateDirectory(const std::filesystem::path& directory,
|
||||
std::string* error) {
|
||||
std::error_code filesystem_error;
|
||||
std::filesystem::create_directories(directory, filesystem_error);
|
||||
if (filesystem_error) {
|
||||
*error =
|
||||
"cannot create session log directory: " + filesystem_error.message();
|
||||
return false;
|
||||
}
|
||||
const auto status =
|
||||
std::filesystem::symlink_status(directory, filesystem_error);
|
||||
if (filesystem_error || !std::filesystem::is_directory(status) ||
|
||||
std::filesystem::is_symlink(status)) {
|
||||
*error = "session log directory is invalid or a symlink";
|
||||
return false;
|
||||
}
|
||||
std::filesystem::permissions(directory, std::filesystem::perms::owner_all,
|
||||
std::filesystem::perm_options::replace,
|
||||
filesystem_error);
|
||||
if (filesystem_error) {
|
||||
*error = "cannot make session log directory private: " +
|
||||
filesystem_error.message();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int CreateSessionFile(const std::filesystem::path& sessions,
|
||||
std::chrono::system_clock::time_point started_at,
|
||||
std::filesystem::path* path, std::string* error) {
|
||||
const std::string timestamp = LocalTimestamp(started_at, "%Y-%m-%d_%H-%M-%S");
|
||||
for (int collision = 1; collision <= 999; ++collision) {
|
||||
const std::string suffix =
|
||||
collision == 1 ? std::string() : "-" + std::to_string(collision);
|
||||
const std::filesystem::path candidate =
|
||||
sessions / (timestamp + suffix + ".log");
|
||||
const int descriptor =
|
||||
open(candidate.c_str(),
|
||||
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600);
|
||||
if (descriptor >= 0) {
|
||||
*path = candidate;
|
||||
return descriptor;
|
||||
}
|
||||
if (errno != EEXIST) {
|
||||
*error =
|
||||
"cannot create session log: " + std::string(std::strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
*error = "too many Mocktail sessions started in the same second";
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool UpdateLatestLink(const std::filesystem::path& logs_root,
|
||||
const std::filesystem::path& session,
|
||||
std::string* warning) {
|
||||
const std::filesystem::path latest = logs_root / "latest.log";
|
||||
const std::filesystem::path temporary =
|
||||
logs_root / (".latest.log." + std::to_string(getpid()));
|
||||
const std::filesystem::path target =
|
||||
std::filesystem::path("sessions") / session.filename();
|
||||
(void)unlink(temporary.c_str());
|
||||
if (symlink(target.c_str(), temporary.c_str()) != 0) {
|
||||
if (link(session.c_str(), temporary.c_str()) != 0) {
|
||||
*warning =
|
||||
"cannot update latest.log: " + std::string(std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (rename(temporary.c_str(), latest.c_str()) != 0) {
|
||||
*warning =
|
||||
"cannot replace latest.log: " + std::string(std::strerror(errno));
|
||||
(void)unlink(temporary.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteAll(int descriptor, const char* bytes, std::size_t size) {
|
||||
std::size_t offset = 0;
|
||||
while (offset < size) {
|
||||
const ssize_t written = write(descriptor, bytes + offset, size - offset);
|
||||
if (written > 0) {
|
||||
offset += static_cast<std::size_t>(written);
|
||||
continue;
|
||||
}
|
||||
if (written < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RunTeeProcess(int stdout_reader, int stderr_reader, int log_descriptor,
|
||||
int original_stdout, int original_stderr) {
|
||||
(void)signal(SIGPIPE, SIG_IGN);
|
||||
(void)signal(SIGINT, SIG_IGN);
|
||||
(void)signal(SIGTERM, SIG_IGN);
|
||||
(void)signal(SIGHUP, SIG_IGN);
|
||||
|
||||
struct Source {
|
||||
int reader;
|
||||
int output;
|
||||
bool active;
|
||||
bool output_active;
|
||||
};
|
||||
std::array<Source, 2> sources = {
|
||||
{{stdout_reader, original_stdout, true, true},
|
||||
{stderr_reader, original_stderr, true, true}}};
|
||||
bool log_active = true;
|
||||
std::array<char, 32768> buffer{};
|
||||
while (sources[0].active || sources[1].active) {
|
||||
std::array<pollfd, 2> descriptors = {
|
||||
{{sources[0].reader, POLLIN, 0}, {sources[1].reader, POLLIN, 0}}};
|
||||
if (!sources[0].active) descriptors[0].fd = -1;
|
||||
if (!sources[1].active) descriptors[1].fd = -1;
|
||||
const int poll_result = poll(descriptors.data(), descriptors.size(), -1);
|
||||
if (poll_result < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
for (std::size_t index = 0; index < sources.size(); ++index) {
|
||||
Source& source = sources[index];
|
||||
if (!source.active ||
|
||||
(descriptors[index].revents & (POLLIN | POLLHUP | POLLERR)) == 0) {
|
||||
continue;
|
||||
}
|
||||
const ssize_t bytes = read(source.reader, buffer.data(), buffer.size());
|
||||
if (bytes > 0) {
|
||||
const std::size_t size = static_cast<std::size_t>(bytes);
|
||||
if (log_active && !WriteAll(log_descriptor, buffer.data(), size)) {
|
||||
log_active = false;
|
||||
}
|
||||
if (source.output_active &&
|
||||
!WriteAll(source.output, buffer.data(), size)) {
|
||||
source.output_active = false;
|
||||
}
|
||||
} else if (bytes == 0 || errno != EINTR) {
|
||||
close(source.reader);
|
||||
source.reader = -1;
|
||||
source.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (log_active) (void)fsync(log_descriptor);
|
||||
close(log_descriptor);
|
||||
close(original_stdout);
|
||||
close(original_stderr);
|
||||
}
|
||||
|
||||
std::filesystem::path ExecutablePath() {
|
||||
std::array<char, 4097> path{};
|
||||
const ssize_t size = readlink("/proc/self/exe", path.data(), path.size() - 1);
|
||||
if (size <= 0 || static_cast<std::size_t>(size) >= path.size()) return {};
|
||||
path[static_cast<std::size_t>(size)] = '\0';
|
||||
return path.data();
|
||||
}
|
||||
|
||||
bool IsWithin(const std::filesystem::path& child,
|
||||
const std::filesystem::path& parent) {
|
||||
const auto relative = child.lexically_relative(parent);
|
||||
return !relative.empty() && !relative.is_absolute() &&
|
||||
*relative.begin() != "..";
|
||||
}
|
||||
|
||||
std::string DisplayPath(const std::filesystem::path& path,
|
||||
const RuntimePaths& paths) {
|
||||
if (path.empty()) return "unknown";
|
||||
if (IsWithin(path, paths.home())) {
|
||||
return (std::filesystem::path("~") / path.lexically_relative(paths.home()))
|
||||
.string();
|
||||
}
|
||||
return path.string();
|
||||
}
|
||||
|
||||
std::string PackageKind(const Environment& environment,
|
||||
const std::filesystem::path& executable) {
|
||||
if (environment.HasNonEmpty("FLATPAK_ID")) return "flatpak";
|
||||
if (environment.HasNonEmpty("APPIMAGE")) return "appimage";
|
||||
if (environment.HasNonEmpty("APPDIR")) return "portable";
|
||||
const std::string path = executable.string();
|
||||
if (path.rfind("/usr/", 0) == 0 || path.rfind("/opt/", 0) == 0 ||
|
||||
path.rfind("/app/", 0) == 0) {
|
||||
return "system-install";
|
||||
}
|
||||
return "local-build";
|
||||
}
|
||||
|
||||
std::string DisplayServer(const Environment& environment) {
|
||||
if (environment.HasNonEmpty("WAYLAND_DISPLAY")) return "wayland";
|
||||
if (environment.HasNonEmpty("DISPLAY")) return "x11";
|
||||
return "none";
|
||||
}
|
||||
|
||||
std::string Trim(std::string value) {
|
||||
const auto first = std::find_if_not(value.begin(), value.end(), [](char c) {
|
||||
return std::isspace(static_cast<unsigned char>(c)) != 0;
|
||||
});
|
||||
const auto last = std::find_if_not(value.rbegin(), value.rend(), [](char c) {
|
||||
return std::isspace(static_cast<unsigned char>(c)) != 0;
|
||||
}).base();
|
||||
if (first >= last) return {};
|
||||
value = std::string(first, last);
|
||||
for (char& character : value) {
|
||||
const unsigned char byte = static_cast<unsigned char>(character);
|
||||
if (character == '"' || std::iscntrl(byte) != 0) character = ' ';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::optional<std::string> ReadFirstLine(const std::filesystem::path& path) {
|
||||
std::ifstream input(path);
|
||||
std::string value;
|
||||
if (!std::getline(input, value)) return std::nullopt;
|
||||
value = Trim(std::move(value));
|
||||
return value.empty() ? std::nullopt
|
||||
: std::optional<std::string>(std::move(value));
|
||||
}
|
||||
|
||||
std::optional<std::string> ReadKey(const std::filesystem::path& path,
|
||||
std::string_view key) {
|
||||
std::ifstream input(path);
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
const std::size_t separator = line.find('=');
|
||||
if (separator == std::string::npos ||
|
||||
line.compare(0, separator, key) != 0 || separator != key.size()) {
|
||||
continue;
|
||||
}
|
||||
std::string value = Trim(line.substr(separator + 1));
|
||||
if (!value.empty()) return value;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string CpuModel() {
|
||||
std::ifstream input("/proc/cpuinfo");
|
||||
std::string fallback;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
const std::size_t separator = line.find(':');
|
||||
if (separator == std::string::npos) continue;
|
||||
const std::string key = Trim(line.substr(0, separator));
|
||||
std::string value = Trim(line.substr(separator + 1));
|
||||
if (value.empty()) continue;
|
||||
if (key == "model name") return value;
|
||||
if (fallback.empty() && (key == "Hardware" || key == "Processor")) {
|
||||
fallback = std::move(value);
|
||||
}
|
||||
}
|
||||
if (!fallback.empty()) return fallback;
|
||||
return ReadFirstLine("/sys/devices/soc0/machine").value_or("unknown");
|
||||
}
|
||||
|
||||
std::string TotalMemory() {
|
||||
std::ifstream input("/proc/meminfo");
|
||||
std::string key;
|
||||
std::uint64_t kibibytes = 0;
|
||||
while (input >> key) {
|
||||
if (key == "MemTotal:") {
|
||||
if (input >> kibibytes) {
|
||||
return std::to_string((kibibytes + 512U) / 1024U) + "MiB";
|
||||
}
|
||||
break;
|
||||
}
|
||||
input.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
std::string HexId(std::optional<std::string> value) {
|
||||
if (!value.has_value()) return {};
|
||||
std::string result = Trim(std::move(*value));
|
||||
if (result.size() > 2 && result[0] == '0' &&
|
||||
(result[1] == 'x' || result[1] == 'X')) {
|
||||
result.erase(0, 2);
|
||||
}
|
||||
std::transform(result.begin(), result.end(), result.begin(), [](char c) {
|
||||
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
});
|
||||
if (result.size() != 4 ||
|
||||
!std::all_of(result.begin(), result.end(), [](char c) {
|
||||
return std::isxdigit(static_cast<unsigned char>(c)) != 0;
|
||||
})) {
|
||||
return {};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct PciName {
|
||||
std::string vendor;
|
||||
std::string device;
|
||||
};
|
||||
|
||||
PciName FindPciName(std::string_view vendor_id, std::string_view device_id) {
|
||||
constexpr std::array<std::string_view, 3> kDatabases = {
|
||||
"/usr/share/hwdata/pci.ids",
|
||||
"/usr/share/misc/pci.ids",
|
||||
"/usr/share/pci.ids",
|
||||
};
|
||||
for (const std::string_view database : kDatabases) {
|
||||
std::ifstream input{std::string(database)};
|
||||
if (!input) continue;
|
||||
PciName result;
|
||||
bool matching_vendor = false;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
if (line[0] != '\t') {
|
||||
if (matching_vendor) break;
|
||||
if (line.size() >= 6 && line.substr(0, 4) == vendor_id &&
|
||||
line[4] == ' ' && line[5] == ' ') {
|
||||
matching_vendor = true;
|
||||
result.vendor = Trim(line.substr(6));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!matching_vendor || line.size() < 7 || line[1] == '\t') continue;
|
||||
if (line.substr(1, 4) == device_id && line[5] == ' ' && line[6] == ' ') {
|
||||
result.device = Trim(line.substr(7));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (!result.vendor.empty()) return result;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string PciVendorFallback(std::string_view vendor_id) {
|
||||
if (vendor_id == "1002") return "AMD";
|
||||
if (vendor_id == "10de") return "NVIDIA";
|
||||
if (vendor_id == "8086") return "Intel";
|
||||
if (vendor_id == "13b5") return "Arm";
|
||||
if (vendor_id == "5143") return "Qualcomm";
|
||||
if (vendor_id == "14e4") return "Broadcom";
|
||||
if (vendor_id == "1af4") return "Virtio";
|
||||
if (vendor_id == "15ad") return "VMware";
|
||||
return "PCI " + std::string(vendor_id);
|
||||
}
|
||||
|
||||
bool DeviceNodeName(std::string_view name, std::string_view prefix) {
|
||||
if (name.size() <= prefix.size() || name.substr(0, prefix.size()) != prefix) {
|
||||
return false;
|
||||
}
|
||||
return std::all_of(
|
||||
name.begin() + static_cast<std::ptrdiff_t>(prefix.size()), name.end(),
|
||||
[](char character) {
|
||||
return std::isdigit(static_cast<unsigned char>(character)) != 0;
|
||||
});
|
||||
}
|
||||
|
||||
struct GraphicsHardware {
|
||||
std::string devices = "unknown";
|
||||
std::string drivers = "unknown";
|
||||
};
|
||||
|
||||
GraphicsHardware DetectGraphicsHardware() {
|
||||
std::error_code error;
|
||||
std::vector<std::filesystem::path> render_nodes;
|
||||
std::vector<std::filesystem::path> card_nodes;
|
||||
for (std::filesystem::directory_iterator iterator("/sys/class/drm", error),
|
||||
end;
|
||||
!error && iterator != end; iterator.increment(error)) {
|
||||
const std::string name = iterator->path().filename().string();
|
||||
if (DeviceNodeName(name, "renderD")) {
|
||||
render_nodes.push_back(iterator->path());
|
||||
} else if (DeviceNodeName(name, "card")) {
|
||||
card_nodes.push_back(iterator->path());
|
||||
}
|
||||
}
|
||||
std::vector<std::filesystem::path>& nodes =
|
||||
render_nodes.empty() ? card_nodes : render_nodes;
|
||||
std::sort(nodes.begin(), nodes.end());
|
||||
|
||||
std::vector<std::string> seen;
|
||||
std::vector<std::string> names;
|
||||
std::vector<std::string> drivers;
|
||||
for (const auto& node : nodes) {
|
||||
error.clear();
|
||||
const std::filesystem::path device =
|
||||
std::filesystem::canonical(node / "device", error);
|
||||
if (error) continue;
|
||||
const std::string identity = device.string();
|
||||
if (std::find(seen.begin(), seen.end(), identity) != seen.end()) continue;
|
||||
seen.push_back(identity);
|
||||
|
||||
error.clear();
|
||||
std::string driver = std::filesystem::read_symlink(device / "driver", error)
|
||||
.filename()
|
||||
.string();
|
||||
if (driver.empty()) {
|
||||
driver = ReadKey(device / "uevent", "DRIVER").value_or("unknown");
|
||||
}
|
||||
const std::string vendor_id = HexId(ReadFirstLine(device / "vendor"));
|
||||
const std::string device_id = HexId(ReadFirstLine(device / "device"));
|
||||
std::string name =
|
||||
ReadFirstLine(device / "product_name").value_or(std::string());
|
||||
if (name.empty() && !vendor_id.empty() && !device_id.empty()) {
|
||||
const PciName pci = FindPciName(vendor_id, device_id);
|
||||
const std::string vendor =
|
||||
pci.vendor.empty() ? PciVendorFallback(vendor_id) : pci.vendor;
|
||||
name = vendor +
|
||||
(pci.device.empty() ? " device " + device_id : " " + pci.device);
|
||||
}
|
||||
if (name.empty()) name = driver;
|
||||
names.push_back(Trim(std::move(name)));
|
||||
drivers.push_back(Trim(std::move(driver)));
|
||||
}
|
||||
|
||||
if (names.empty()) return {};
|
||||
GraphicsHardware result;
|
||||
result.devices.clear();
|
||||
result.drivers.clear();
|
||||
for (std::size_t index = 0; index < names.size(); ++index) {
|
||||
if (index != 0) {
|
||||
result.devices += "; ";
|
||||
result.drivers += "; ";
|
||||
}
|
||||
result.devices += names[index];
|
||||
result.drivers += drivers[index];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionBuildInformation CurrentSessionBuildInformation() {
|
||||
SessionBuildInformation information;
|
||||
information.version = MOCKTAIL_PROJECT_VERSION;
|
||||
information.git_commit = MOCKTAIL_BUILD_GIT_COMMIT;
|
||||
information.git_dirty = MOCKTAIL_BUILD_GIT_DIRTY;
|
||||
information.build_type = MOCKTAIL_BUILD_TYPE;
|
||||
information.compiler = std::string(MOCKTAIL_BUILD_COMPILER_ID) + " " +
|
||||
MOCKTAIL_BUILD_COMPILER_VERSION;
|
||||
information.target = std::string(MOCKTAIL_BUILD_TARGET_SYSTEM) + "/" +
|
||||
MOCKTAIL_BUILD_TARGET_PROCESSOR;
|
||||
return information;
|
||||
}
|
||||
|
||||
SessionLog::~SessionLog() { Stop(); }
|
||||
|
||||
SessionLog::SessionLog(SessionLog&& other) noexcept {
|
||||
*this = std::move(other);
|
||||
}
|
||||
|
||||
SessionLog& SessionLog::operator=(SessionLog&& other) noexcept {
|
||||
if (this == &other) return *this;
|
||||
Stop();
|
||||
attempted_ = other.attempted_;
|
||||
active_ = other.active_;
|
||||
original_stdout_ = other.original_stdout_;
|
||||
original_stderr_ = other.original_stderr_;
|
||||
logger_process_ = other.logger_process_;
|
||||
started_at_ = other.started_at_;
|
||||
path_ = std::move(other.path_);
|
||||
latest_path_ = std::move(other.latest_path_);
|
||||
error_ = std::move(other.error_);
|
||||
warning_ = std::move(other.warning_);
|
||||
other.attempted_ = false;
|
||||
other.active_ = false;
|
||||
other.original_stdout_ = -1;
|
||||
other.original_stderr_ = -1;
|
||||
other.logger_process_ = -1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SessionLog SessionLog::Start(const Environment& environment,
|
||||
const RuntimePaths& paths,
|
||||
std::chrono::system_clock::time_point started_at) {
|
||||
SessionLog result;
|
||||
result.started_at_ = started_at;
|
||||
if (Enabled(environment, "MOCKTAIL_DISABLE_SESSION_LOG") ||
|
||||
Enabled(environment, "MOCKTAIL_ISOLATED_CANARY")) {
|
||||
return result;
|
||||
}
|
||||
result.attempted_ = true;
|
||||
std::cout.flush();
|
||||
std::cerr.flush();
|
||||
(void)fflush(nullptr);
|
||||
|
||||
const std::filesystem::path sessions = paths.logs_root() / "sessions";
|
||||
if (!EnsurePrivateDirectory(paths.logs_root(), &result.error_) ||
|
||||
!EnsurePrivateDirectory(sessions, &result.error_)) {
|
||||
return result;
|
||||
}
|
||||
const int log_descriptor =
|
||||
CreateSessionFile(sessions, started_at, &result.path_, &result.error_);
|
||||
if (log_descriptor < 0) return result;
|
||||
result.latest_path_ = paths.logs_root() / "latest.log";
|
||||
|
||||
std::array<int, 2> stdout_pipe = {-1, -1};
|
||||
std::array<int, 2> stderr_pipe = {-1, -1};
|
||||
result.original_stdout_ = fcntl(STDOUT_FILENO, F_DUPFD_CLOEXEC, 3);
|
||||
result.original_stderr_ = fcntl(STDERR_FILENO, F_DUPFD_CLOEXEC, 3);
|
||||
if (result.original_stdout_ < 0 || result.original_stderr_ < 0 ||
|
||||
pipe2(stdout_pipe.data(), O_CLOEXEC) != 0 ||
|
||||
pipe2(stderr_pipe.data(), O_CLOEXEC) != 0) {
|
||||
result.error_ = "cannot prepare session log streams: " +
|
||||
std::string(std::strerror(errno));
|
||||
if (stdout_pipe[0] >= 0) close(stdout_pipe[0]);
|
||||
if (stdout_pipe[1] >= 0) close(stdout_pipe[1]);
|
||||
if (stderr_pipe[0] >= 0) close(stderr_pipe[0]);
|
||||
if (stderr_pipe[1] >= 0) close(stderr_pipe[1]);
|
||||
if (result.original_stdout_ >= 0) close(result.original_stdout_);
|
||||
if (result.original_stderr_ >= 0) close(result.original_stderr_);
|
||||
result.original_stdout_ = -1;
|
||||
result.original_stderr_ = -1;
|
||||
close(log_descriptor);
|
||||
(void)unlink(result.path_.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
const pid_t child = fork();
|
||||
if (child < 0) {
|
||||
result.error_ =
|
||||
"cannot start session log writer: " + std::string(std::strerror(errno));
|
||||
close(stdout_pipe[0]);
|
||||
close(stdout_pipe[1]);
|
||||
close(stderr_pipe[0]);
|
||||
close(stderr_pipe[1]);
|
||||
close(result.original_stdout_);
|
||||
close(result.original_stderr_);
|
||||
result.original_stdout_ = -1;
|
||||
result.original_stderr_ = -1;
|
||||
close(log_descriptor);
|
||||
(void)unlink(result.path_.c_str());
|
||||
return result;
|
||||
}
|
||||
if (child == 0) {
|
||||
close(stdout_pipe[1]);
|
||||
close(stderr_pipe[1]);
|
||||
RunTeeProcess(stdout_pipe[0], stderr_pipe[0], log_descriptor,
|
||||
result.original_stdout_, result.original_stderr_);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
close(stdout_pipe[0]);
|
||||
close(stderr_pipe[0]);
|
||||
close(log_descriptor);
|
||||
int redirect_error = 0;
|
||||
const bool stdout_redirected =
|
||||
dup2(stdout_pipe[1], STDOUT_FILENO) == STDOUT_FILENO;
|
||||
if (!stdout_redirected) redirect_error = errno;
|
||||
const bool stderr_redirected =
|
||||
dup2(stderr_pipe[1], STDERR_FILENO) == STDERR_FILENO;
|
||||
if (!stderr_redirected && redirect_error == 0) redirect_error = errno;
|
||||
close(stdout_pipe[1]);
|
||||
close(stderr_pipe[1]);
|
||||
if (!stdout_redirected || !stderr_redirected) {
|
||||
if (stdout_redirected) {
|
||||
(void)dup2(result.original_stdout_, STDOUT_FILENO);
|
||||
}
|
||||
if (stderr_redirected) {
|
||||
(void)dup2(result.original_stderr_, STDERR_FILENO);
|
||||
}
|
||||
close(result.original_stdout_);
|
||||
close(result.original_stderr_);
|
||||
result.original_stdout_ = -1;
|
||||
result.original_stderr_ = -1;
|
||||
while (waitpid(child, nullptr, 0) < 0 && errno == EINTR) {
|
||||
}
|
||||
result.error_ = "cannot activate session log streams: " +
|
||||
std::string(std::strerror(redirect_error));
|
||||
(void)unlink(result.path_.c_str());
|
||||
return result;
|
||||
}
|
||||
(void)setvbuf(stdout, nullptr, _IOLBF, BUFSIZ);
|
||||
|
||||
result.logger_process_ = static_cast<int>(child);
|
||||
result.active_ = true;
|
||||
(void)UpdateLatestLink(paths.logs_root(), result.path_, &result.warning_);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string SessionLog::Header(const Environment& environment,
|
||||
const RuntimePaths& paths,
|
||||
std::string_view graphics_backend) const {
|
||||
const SessionBuildInformation build = CurrentSessionBuildInformation();
|
||||
const std::filesystem::path executable = ExecutablePath();
|
||||
struct utsname system = {};
|
||||
const bool has_system = uname(&system) == 0;
|
||||
const GraphicsHardware graphics = DetectGraphicsHardware();
|
||||
const unsigned int threads = std::thread::hardware_concurrency();
|
||||
|
||||
std::ostringstream output;
|
||||
output << "[mocktail] version=" << build.version
|
||||
<< " commit=" << build.git_commit;
|
||||
if (build.git_dirty == "true") {
|
||||
output << " dirty=true";
|
||||
} else if (build.git_dirty == "unknown") {
|
||||
output << " dirty=unknown";
|
||||
}
|
||||
output << " build=" << build.build_type << " compiler=\"" << build.compiler
|
||||
<< "\"\n"
|
||||
<< "[mocktail] started=" << IsoLocalTimestamp(started_at_)
|
||||
<< " package=" << PackageKind(environment, executable)
|
||||
<< " target=" << build.target;
|
||||
if (has_system) {
|
||||
output << " host=\"" << system.sysname << ' ' << system.release << ' '
|
||||
<< system.machine << '"';
|
||||
}
|
||||
output << " pid=" << getpid() << '\n'
|
||||
<< "[mocktail] cpu=\"" << CpuModel() << "\" threads=";
|
||||
if (threads == 0) {
|
||||
output << "unknown";
|
||||
} else {
|
||||
output << threads;
|
||||
}
|
||||
output << " ram=" << TotalMemory() << " gpu=\"" << graphics.devices
|
||||
<< "\" gpu_driver=\"" << graphics.drivers
|
||||
<< "\" display=" << DisplayServer(environment) << " graphics="
|
||||
<< (graphics_backend.empty() ? "unknown" : graphics_backend) << '\n'
|
||||
<< "[mocktail] executable=" << DisplayPath(executable, paths)
|
||||
<< " config=" << DisplayPath(paths.config_file(), paths) << '\n'
|
||||
<< "[mocktail] log=" << DisplayPath(path_, paths) << '\n';
|
||||
if (!warning_.empty()) output << "[mocktail] warning=" << warning_ << '\n';
|
||||
return output.str();
|
||||
}
|
||||
|
||||
void SessionLog::Stop() {
|
||||
if (!active_) return;
|
||||
std::cout.flush();
|
||||
std::cerr.flush();
|
||||
(void)fflush(nullptr);
|
||||
(void)dup2(original_stdout_, STDOUT_FILENO);
|
||||
(void)dup2(original_stderr_, STDERR_FILENO);
|
||||
close(original_stdout_);
|
||||
close(original_stderr_);
|
||||
original_stdout_ = -1;
|
||||
original_stderr_ = -1;
|
||||
const pid_t child = static_cast<pid_t>(logger_process_);
|
||||
while (waitpid(child, nullptr, 0) < 0 && errno == EINTR) {
|
||||
}
|
||||
logger_process_ = -1;
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
+109
-13
@@ -18,6 +18,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
@@ -906,10 +907,10 @@ bool ValidateSemantics(const std::vector<Instruction>& reference,
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<SignatureMatch> FindSignatureMatch(
|
||||
std::optional<std::vector<SignatureMatch>> FindSignatureMatches(
|
||||
const ElfImage& reference, const ElfImage& candidate,
|
||||
const Disassembler& disassembler, const SignatureSpec& spec,
|
||||
std::string* error) {
|
||||
bool allow_multiple_candidates, std::string* error) {
|
||||
const auto reference_instructions = disassembler.Decode(
|
||||
reference, spec.reference_rva, spec.instruction_count, 4096, error);
|
||||
if (!reference_instructions.has_value()) return std::nullopt;
|
||||
@@ -995,21 +996,68 @@ std::optional<SignatureMatch> FindSignatureMatch(
|
||||
}
|
||||
search = found + 1;
|
||||
}
|
||||
if (matches.size() != 1) {
|
||||
if (matches.empty()) {
|
||||
*error = "signature " + spec.name + " matched " +
|
||||
"0 candidate locations";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!allow_multiple_candidates && matches.size() != 1U) {
|
||||
*error = "signature " + spec.name + " matched " +
|
||||
std::to_string(matches.size()) + " candidate locations";
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::uint64_t match_rva = text->address + matches[0];
|
||||
const auto candidate_instructions = disassembler.Decode(
|
||||
candidate, match_rva, spec.instruction_count, 4096, error);
|
||||
if (!candidate_instructions.has_value() ||
|
||||
!ValidateSemantics(*reference_instructions, *candidate_instructions,
|
||||
spec.registry_size_may_change, error)) {
|
||||
std::vector<SignatureMatch> semantic_matches;
|
||||
std::string first_semantic_error;
|
||||
for (const std::size_t match : matches) {
|
||||
const std::uint64_t match_rva = text->address + match;
|
||||
std::string semantic_error;
|
||||
const auto candidate_instructions = disassembler.Decode(
|
||||
candidate, match_rva, spec.instruction_count, 4096, &semantic_error);
|
||||
if (!candidate_instructions.has_value() ||
|
||||
!ValidateSemantics(*reference_instructions, *candidate_instructions,
|
||||
spec.registry_size_may_change, &semantic_error)) {
|
||||
if (first_semantic_error.empty()) {
|
||||
first_semantic_error = std::move(semantic_error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
semantic_matches.push_back(SignatureMatch{
|
||||
match_rva, *reference_instructions, *candidate_instructions});
|
||||
}
|
||||
if (semantic_matches.empty() && matches.size() == 1U &&
|
||||
!first_semantic_error.empty()) {
|
||||
*error = std::move(first_semantic_error);
|
||||
return std::nullopt;
|
||||
}
|
||||
return SignatureMatch{match_rva, *reference_instructions,
|
||||
*candidate_instructions};
|
||||
return semantic_matches;
|
||||
}
|
||||
|
||||
std::optional<SignatureMatch> FindSignatureMatch(
|
||||
const ElfImage& reference, const ElfImage& candidate,
|
||||
const Disassembler& disassembler, const SignatureSpec& spec,
|
||||
std::string* error) {
|
||||
auto matches =
|
||||
FindSignatureMatches(reference, candidate, disassembler, spec, false,
|
||||
error);
|
||||
if (!matches.has_value()) return std::nullopt;
|
||||
if (matches->size() != 1U) {
|
||||
*error = "signature " + spec.name + " matched " +
|
||||
std::to_string(matches->size()) + " candidate locations";
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::move(matches->front());
|
||||
}
|
||||
|
||||
std::optional<std::uint64_t> DirectCallTarget(
|
||||
const Instruction& instruction, std::string_view description,
|
||||
std::string* error) {
|
||||
if (instruction.mnemonic != "call" || instruction.operands.size() != 1U ||
|
||||
instruction.operands[0].type != Operand::Type::kImmediate ||
|
||||
instruction.operands[0].value < 0) {
|
||||
*error = std::string(description) + " is not one direct call";
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<std::uint64_t>(instruction.operands[0].value);
|
||||
}
|
||||
|
||||
std::vector<std::uint64_t> RipTargets(const Instruction& instruction) {
|
||||
@@ -1182,7 +1230,9 @@ std::optional<std::vector<SignatureSpec>> SignatureSpecs(const Json& sidecar,
|
||||
for (const auto& [label, count] :
|
||||
std::array<std::pair<std::string_view, std::size_t>, 6>{
|
||||
{{"small-allocate", 24},
|
||||
{"usable-size", 24},
|
||||
// usable-size is an 18-instruction leaf. Do not include bytes from
|
||||
// the adjacent function or alignment padding in its signature.
|
||||
{"usable-size", 18},
|
||||
{"reallocate", 24},
|
||||
{"allocate", 24},
|
||||
{"aligned-allocate-direct", 24},
|
||||
@@ -1193,7 +1243,7 @@ std::optional<std::vector<SignatureSpec>> SignatureSpecs(const Json& sidecar,
|
||||
}
|
||||
if (!add("arena-initializer", seeds["arena_initializer"], 24) ||
|
||||
!add("allocator-thread-initializer",
|
||||
seeds["allocator_thread_initializer"], 24) ||
|
||||
seeds["allocator_thread_initializer"], 19) ||
|
||||
!add("registry-initializer", bootstrap["registry_initializer"], 20,
|
||||
true) ||
|
||||
!add("allocator-object-initializer",
|
||||
@@ -1290,12 +1340,58 @@ std::optional<std::pair<Json, Json>> DeriveDocuments(const ElfImage& reference,
|
||||
const auto specs = SignatureSpecs(sidecar, error);
|
||||
if (!specs.has_value()) return std::nullopt;
|
||||
std::map<std::string, SignatureMatch> matches;
|
||||
std::vector<SignatureMatch> allocate_matches;
|
||||
for (const SignatureSpec& spec : *specs) {
|
||||
if (spec.name == "allocate") {
|
||||
auto candidates = FindSignatureMatches(reference, candidate,
|
||||
disassembler, spec, true, error);
|
||||
if (!candidates.has_value()) return std::nullopt;
|
||||
allocate_matches = std::move(*candidates);
|
||||
continue;
|
||||
}
|
||||
auto match =
|
||||
FindSignatureMatch(reference, candidate, disassembler, spec, error);
|
||||
if (!match.has_value()) return std::nullopt;
|
||||
matches.emplace(spec.name, std::move(*match));
|
||||
}
|
||||
// 2904 contains two semantically equivalent allocate wrappers. Select the
|
||||
// one independently referenced by the registry initializer instead of
|
||||
// weakening signature uniqueness globally.
|
||||
const SignatureMatch& registry_match = matches.at("registry-initializer");
|
||||
if (allocate_matches.empty() || registry_match.reference.size() <= 9U ||
|
||||
registry_match.candidate.size() <= 9U) {
|
||||
*error = "allocator signatures are incomplete";
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto reference_allocate = DirectCallTarget(
|
||||
registry_match.reference[9], "reference registry allocator call", error);
|
||||
if (!reference_allocate.has_value() ||
|
||||
*reference_allocate !=
|
||||
allocate_matches.front().reference.front().address) {
|
||||
if (error->empty()) {
|
||||
*error =
|
||||
"reference registry initializer does not call its allocator bridge";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto candidate_allocate = DirectCallTarget(
|
||||
registry_match.candidate[9], "candidate registry allocator call", error);
|
||||
if (!candidate_allocate.has_value()) return std::nullopt;
|
||||
auto selected_allocate = std::find_if(
|
||||
allocate_matches.begin(), allocate_matches.end(),
|
||||
[&](const SignatureMatch& match) {
|
||||
return match.rva == *candidate_allocate;
|
||||
});
|
||||
if (selected_allocate == allocate_matches.end() ||
|
||||
std::find_if(std::next(selected_allocate), allocate_matches.end(),
|
||||
[&](const SignatureMatch& match) {
|
||||
return match.rva == *candidate_allocate;
|
||||
}) != allocate_matches.end()) {
|
||||
*error = "signature allocate candidates do not identify exactly one "
|
||||
"registry allocator call";
|
||||
return std::nullopt;
|
||||
}
|
||||
matches.emplace("allocate", std::move(*selected_allocate));
|
||||
const auto reference_init = reference.InitArray(error);
|
||||
const auto candidate_init = candidate.InitArray(error);
|
||||
if (!reference_init.has_value() || !candidate_init.has_value()) {
|
||||
|
||||
@@ -155,6 +155,11 @@ UpdateConfigResult LoadUpdateConfig(const std::filesystem::path& path) {
|
||||
result.error = "updates.automatic must be true or false";
|
||||
break;
|
||||
}
|
||||
} else if (key == "testing_latest_only") {
|
||||
if (!ParseBoolean(value, &result.config.testing_latest_only)) {
|
||||
result.error = "updates.testing_latest_only must be true or false";
|
||||
break;
|
||||
}
|
||||
} else if (key == "launch_after_update") {
|
||||
if (!ParseBoolean(value, &result.config.launch_after_update)) {
|
||||
result.error = "updates.launch_after_update must be true or false";
|
||||
|
||||
@@ -235,7 +235,8 @@ ReferenceProfile ResolveReference(const UpdatePaths& paths,
|
||||
ApkPureProvider* provider,
|
||||
PayloadStore* store,
|
||||
const std::filesystem::path& workspace,
|
||||
int progress_fd) {
|
||||
int progress_fd,
|
||||
bool allow_reference_download) {
|
||||
ReferenceProfile result;
|
||||
if (installed && !installed.host_abi_profile.empty()) {
|
||||
std::error_code filesystem_error;
|
||||
@@ -262,6 +263,12 @@ ReferenceProfile ResolveReference(const UpdatePaths& paths,
|
||||
result.profile = paths.host_abi_reference_profile;
|
||||
return result;
|
||||
}
|
||||
if (!allow_reference_download) {
|
||||
result.error =
|
||||
"updates.testing_latest_only requires an installed HostAbi reference; "
|
||||
"downloading an older reference payload is disabled";
|
||||
return result;
|
||||
}
|
||||
Candidate downloaded =
|
||||
DownloadCandidate(provider, store, ExactPayloadIdentity(preferred), true,
|
||||
paths, workspace / "reference", progress_fd);
|
||||
@@ -397,7 +404,9 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
}
|
||||
ApkPureProvider provider;
|
||||
std::optional<ProviderVersion> latest_version;
|
||||
if (request.check_latest) {
|
||||
const bool check_latest =
|
||||
request.check_latest || configured.config.testing_latest_only;
|
||||
if (check_latest) {
|
||||
Progress(request.progress_fd, "Checking Roblox...");
|
||||
const ProviderVersion latest = provider.CheckLatest();
|
||||
if (latest) {
|
||||
@@ -414,11 +423,20 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
result.message = "installed Roblox is newer than provider metadata";
|
||||
return result;
|
||||
}
|
||||
} else if (current) {
|
||||
result.payload_id = current.payload_id;
|
||||
result.message =
|
||||
"update metadata is temporarily unavailable: " + latest.error;
|
||||
return result;
|
||||
} else {
|
||||
if (current) {
|
||||
result.payload_id = current.payload_id;
|
||||
result.message =
|
||||
"update metadata is temporarily unavailable: " + latest.error;
|
||||
return result;
|
||||
}
|
||||
if (configured.config.testing_latest_only) {
|
||||
result.error =
|
||||
"updates.testing_latest_only requires available provider latest "
|
||||
"metadata: " +
|
||||
latest.error;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::filesystem::path workspace = UniqueDirectory(
|
||||
@@ -475,7 +493,8 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
if (candidate && !candidate.exact_supported && candidate.profile.empty()) {
|
||||
const ReferenceProfile reference =
|
||||
ResolveReference(paths, installed, *preferred, &provider, &store,
|
||||
workspace, request.progress_fd);
|
||||
workspace, request.progress_fd,
|
||||
!configured.config.testing_latest_only);
|
||||
if (!reference) {
|
||||
candidate_error = reference.error;
|
||||
} else {
|
||||
@@ -530,6 +549,14 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (configured.config.testing_latest_only) {
|
||||
result.error = "latest Roblox failed probation (" + candidate_error +
|
||||
"); updates.testing_latest_only forbids activating an "
|
||||
"older supported fallback";
|
||||
cleanup();
|
||||
return result;
|
||||
}
|
||||
|
||||
Candidate fallback;
|
||||
const std::string preferred_id =
|
||||
std::to_string(preferred->version_code) + "-" + preferred->elf_build_id;
|
||||
|
||||
@@ -62,6 +62,12 @@ target_link_libraries(support_bundle_runtime_test PRIVATE
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_executable(session_log_test session_log_test.cc)
|
||||
target_link_libraries(session_log_test PRIVATE
|
||||
Mocktail::Runtime
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_executable(failure_dialog_test failure_dialog_test.cc)
|
||||
target_link_libraries(failure_dialog_test PRIVATE
|
||||
Mocktail::FailureDialog
|
||||
@@ -803,6 +809,7 @@ gtest_discover_tests(runtime_config_file_test)
|
||||
gtest_discover_tests(payload_update_preflight_test)
|
||||
gtest_discover_tests(native_update_test)
|
||||
gtest_discover_tests(support_bundle_runtime_test)
|
||||
gtest_discover_tests(session_log_test)
|
||||
gtest_discover_tests(failure_dialog_test)
|
||||
gtest_discover_tests(frame_rate_policy_test)
|
||||
gtest_discover_tests(performance_policy_test)
|
||||
|
||||
@@ -128,7 +128,15 @@ grep -Fq 'CleanupStaleBuilderMounts' "${BUILD_HELPER}" ||
|
||||
Fail 'obsolete GitLab CI configuration is still present'
|
||||
grep -Fq 'name: Flatpak' "${GITHUB_CI}" ||
|
||||
Fail 'GitHub Actions workflow has no stable display name'
|
||||
grep -Fq 'branches: [main]' "${GITHUB_CI}" ||
|
||||
validates_main=false
|
||||
if grep -Fq 'branches: [main]' "${GITHUB_CI}"; then
|
||||
validates_main=true
|
||||
elif grep -Fq 'workflow_run:' "${GITHUB_CI}" &&
|
||||
grep -Fq 'workflows: [Native packages]' "${GITHUB_CI}" &&
|
||||
grep -Fq "head_branch == 'main'" "${GITHUB_CI}"; then
|
||||
validates_main=true
|
||||
fi
|
||||
[[ "${validates_main}" == true ]] ||
|
||||
Fail 'GitHub Actions does not validate the main branch'
|
||||
grep -Fq 'pull_request:' "${GITHUB_CI}" ||
|
||||
Fail 'GitHub Actions does not validate pull requests'
|
||||
@@ -201,13 +209,20 @@ grep -Fq 'flatpak-builder is unavailable' "${TEMP_DIR}/stderr" ||
|
||||
Fail 'helper did not explain how to install flatpak-builder'
|
||||
|
||||
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 '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-flatpak.gpg" \
|
||||
"${TEMP_DIR}/public"
|
||||
"${TEMP_DIR}/public" \
|
||||
"${TEMP_DIR}/native-repositories"
|
||||
grep -Fq 'Url=https://mocktail.bigrat.space/repo/' \
|
||||
"${TEMP_DIR}/public/mocktail.flatpakrepo" ||
|
||||
Fail 'published Flatpak repository URL is incorrect'
|
||||
@@ -218,5 +233,9 @@ grep -Fq 'flatpak install --user' "${TEMP_DIR}/public/index.html" ||
|
||||
Fail 'Pages landing page has no direct installation command'
|
||||
grep -Fq 'roblox://experiences/start' "${TEMP_DIR}/public/join.html" ||
|
||||
Fail 'Pages output has no Discord join bridge'
|
||||
[[ -d "${TEMP_DIR}/public/apt" &&
|
||||
-d "${TEMP_DIR}/public/rpm" &&
|
||||
-d "${TEMP_DIR}/public/downloads" ]] ||
|
||||
Fail 'Pages output has no native package repositories'
|
||||
|
||||
printf 'Flatpak packaging contract test passed\n'
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly installer="${1:?user updater installer is required}"
|
||||
readonly project_root="${2:?project root is required}"
|
||||
test_root="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${test_root}"' EXIT
|
||||
|
||||
mkdir -p "${test_root}/bin" "${test_root}/home"
|
||||
canary="${test_root}/mocktail"
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' >"${canary}"
|
||||
chmod 0755 "${canary}"
|
||||
cat >"${test_root}/bin/systemctl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >>"${SYSTEMCTL_CALLS:?}"
|
||||
EOF
|
||||
chmod 0755 "${test_root}/bin/systemctl"
|
||||
|
||||
export HOME="${test_root}/home"
|
||||
export XDG_CONFIG_HOME="${test_root}/config"
|
||||
export MOCKTAIL_UPDATE_CANARY_BIN="${canary}"
|
||||
export SYSTEMCTL_CALLS="${test_root}/systemctl-calls"
|
||||
export PATH="${test_root}/bin:${PATH}"
|
||||
"${installer}" >/dev/null
|
||||
|
||||
readonly config_root="${XDG_CONFIG_HOME}/mocktail"
|
||||
readonly unit_root="${XDG_CONFIG_HOME}/systemd/user"
|
||||
[[ -f "${config_root}/config.yaml" ]]
|
||||
[[ -f "${unit_root}/mocktail-update.service" ]]
|
||||
[[ -f "${unit_root}/mocktail-update.timer" ]]
|
||||
grep -Fxq "MOCKTAIL_PROJECT_ROOT=${project_root}" \
|
||||
"${config_root}/updater.env"
|
||||
grep -Fxq "MOCKTAIL_UPDATE_CANARY_BIN=${canary}" \
|
||||
"${config_root}/updater.env"
|
||||
grep -Fq \
|
||||
'auto_update_roblox.sh --skip-build --no-launch --scheduled' \
|
||||
"${unit_root}/mocktail-update.service"
|
||||
grep -Fxq -- '--user daemon-reload' "${SYSTEMCTL_CALLS}"
|
||||
grep -Fxq -- '--user enable --now mocktail-update.timer' "${SYSTEMCTL_CALLS}"
|
||||
grep -Fxq -- '--user list-timers mocktail-update.timer --no-pager' \
|
||||
"${SYSTEMCTL_CALLS}"
|
||||
|
||||
# A source-tree timer must follow the runtime built from that same tree. An
|
||||
# older system installation may coexist, but it cannot safely approve or
|
||||
# reject payloads on behalf of the development binary.
|
||||
fixture_project="${test_root}/fixture-project"
|
||||
mkdir -p "${fixture_project}/scripts" "${fixture_project}/build" \
|
||||
"${fixture_project}/config/systemd"
|
||||
cp -- "${installer}" "${fixture_project}/scripts/install_user_updater.sh"
|
||||
cp -- "${project_root}/config/systemd/mocktail-update.service" \
|
||||
"${fixture_project}/config/systemd/mocktail-update.service"
|
||||
cp -- "${project_root}/config/systemd/mocktail-update.timer" \
|
||||
"${fixture_project}/config/systemd/mocktail-update.timer"
|
||||
cp -- "${project_root}/config/mocktail.example.yaml" \
|
||||
"${fixture_project}/config/mocktail.example.yaml"
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' > \
|
||||
"${fixture_project}/build/mocktail"
|
||||
chmod 0755 "${fixture_project}/build/mocktail"
|
||||
unset MOCKTAIL_UPDATE_CANARY_BIN
|
||||
"${fixture_project}/scripts/install_user_updater.sh" >/dev/null
|
||||
grep -Fxq \
|
||||
"MOCKTAIL_UPDATE_CANARY_BIN=${fixture_project}/build/mocktail" \
|
||||
"${config_root}/updater.env"
|
||||
|
||||
echo "user updater installation checks passed"
|
||||
@@ -107,11 +107,17 @@ grep -Fq 'paths-ignore:' "${WORKFLOW}" ||
|
||||
Fail 'native package workflow has no path filters'
|
||||
grep -Fq "'assets/screenshots/**'" "${WORKFLOW}" ||
|
||||
Fail 'native packages rebuild for screenshot-only changes'
|
||||
grep -Fq "'packaging/flatpak/**'" "${WORKFLOW}" ||
|
||||
Fail 'native packages rebuild for Flatpak-only changes'
|
||||
if grep -Fq "'packaging/flatpak/**'" "${WORKFLOW}"; then
|
||||
Fail 'native packages ignore Flatpak changes required by Pages publication'
|
||||
fi
|
||||
if grep -Fq "'site/**'" "${WORKFLOW}"; then
|
||||
Fail 'native packages ignore website changes required by Pages publication'
|
||||
fi
|
||||
|
||||
grep -Fq 'sudo pacman -U ./mocktail-*-x86_64.pkg.tar.zst' "${README}" ||
|
||||
Fail 'README has no direct pacman installation command'
|
||||
grep -Fq 'sudo dnf install mocktail-nightly' "${README}" ||
|
||||
Fail 'README has no nightly DNF installation command'
|
||||
grep -Fq 'sudo apt install mocktail-nightly' "${README}" ||
|
||||
Fail 'README has no nightly APT installation command'
|
||||
grep -Fq 'Ubuntu 26.04+' "${README}" ||
|
||||
Fail 'README has no Ubuntu source dependency guide'
|
||||
grep -Fq 'Arch Linux' "${README}" ||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
#include "runtime/session_log.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
class MapEnvironment final : public Environment {
|
||||
public:
|
||||
explicit MapEnvironment(std::unordered_map<std::string, std::string> values)
|
||||
: values_(std::move(values)) {}
|
||||
|
||||
std::optional<std::string> Get(std::string_view name) const override {
|
||||
const auto found = values_.find(std::string(name));
|
||||
return found == values_.end() ? std::nullopt
|
||||
: std::optional<std::string>(found->second);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::string> values_;
|
||||
};
|
||||
|
||||
class TemporaryDirectory final {
|
||||
public:
|
||||
TemporaryDirectory() {
|
||||
char pattern[] = "/tmp/mocktail_session_log_XXXXXX";
|
||||
const char* created = mkdtemp(pattern);
|
||||
if (created != nullptr) root_ = created;
|
||||
}
|
||||
|
||||
~TemporaryDirectory() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(root_, error);
|
||||
}
|
||||
|
||||
const std::filesystem::path& root() const { return root_; }
|
||||
|
||||
private:
|
||||
std::filesystem::path root_;
|
||||
};
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream input(path);
|
||||
std::ostringstream contents;
|
||||
contents << input.rdbuf();
|
||||
return contents.str();
|
||||
}
|
||||
|
||||
TEST(SessionLogTest, CapturesBothStreamsAndUpdatesLatest) {
|
||||
TemporaryDirectory temporary;
|
||||
const std::filesystem::path home = temporary.root() / "home";
|
||||
const std::filesystem::path state = temporary.root() / "state";
|
||||
const MapEnvironment environment({
|
||||
{"HOME", home.string()},
|
||||
{"MOCKTAIL_STATE_ROOT", state.string()},
|
||||
{"WAYLAND_DISPLAY", "wayland-0"},
|
||||
});
|
||||
const RuntimePaths paths = RuntimePaths::FromEnvironment(environment);
|
||||
std::filesystem::path session_path;
|
||||
std::filesystem::path latest_path;
|
||||
{
|
||||
SessionLog log = SessionLog::Start(environment, paths);
|
||||
ASSERT_TRUE(log) << log.error();
|
||||
session_path = log.path();
|
||||
latest_path = log.latest_path();
|
||||
std::cout << log.Header(environment, paths, "direct-vulkan")
|
||||
<< "session-stdout-marker\n"
|
||||
<< std::flush;
|
||||
std::cerr << "session-stderr-marker\n" << std::flush;
|
||||
}
|
||||
|
||||
ASSERT_TRUE(std::filesystem::is_regular_file(session_path));
|
||||
ASSERT_TRUE(std::filesystem::exists(latest_path));
|
||||
EXPECT_EQ(std::filesystem::canonical(latest_path),
|
||||
std::filesystem::canonical(session_path));
|
||||
EXPECT_TRUE(std::regex_match(
|
||||
session_path.filename().string(),
|
||||
std::regex(
|
||||
R"(^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}\.log$)")));
|
||||
struct stat metadata = {};
|
||||
ASSERT_EQ(stat(session_path.c_str(), &metadata), 0);
|
||||
EXPECT_EQ(metadata.st_mode & 0777, 0600);
|
||||
const std::string contents = ReadFile(session_path);
|
||||
EXPECT_EQ(contents.rfind("[mocktail] version=", 0), 0U);
|
||||
EXPECT_NE(contents.find("session-stdout-marker"), std::string::npos);
|
||||
EXPECT_NE(contents.find("session-stderr-marker"), std::string::npos);
|
||||
EXPECT_NE(contents.find(" commit="), std::string::npos);
|
||||
EXPECT_NE(contents.find(" target="), std::string::npos);
|
||||
EXPECT_NE(contents.find("[mocktail] cpu=\""), std::string::npos);
|
||||
EXPECT_NE(contents.find(" ram="), std::string::npos);
|
||||
EXPECT_NE(contents.find(" gpu=\""), std::string::npos);
|
||||
EXPECT_NE(contents.find(" gpu_driver=\""), std::string::npos);
|
||||
EXPECT_NE(contents.find("[mocktail] log="), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(SessionLogTest, PreservesSessionsStartedInTheSameSecond) {
|
||||
TemporaryDirectory temporary;
|
||||
const MapEnvironment environment({
|
||||
{"HOME", (temporary.root() / "home").string()},
|
||||
{"MOCKTAIL_STATE_ROOT", (temporary.root() / "state").string()},
|
||||
});
|
||||
const RuntimePaths paths = RuntimePaths::FromEnvironment(environment);
|
||||
const auto timestamp = std::chrono::system_clock::from_time_t(1770000000);
|
||||
std::filesystem::path first;
|
||||
std::filesystem::path second;
|
||||
{
|
||||
SessionLog log = SessionLog::Start(environment, paths, timestamp);
|
||||
ASSERT_TRUE(log) << log.error();
|
||||
first = log.path();
|
||||
}
|
||||
{
|
||||
SessionLog log = SessionLog::Start(environment, paths, timestamp);
|
||||
ASSERT_TRUE(log) << log.error();
|
||||
second = log.path();
|
||||
}
|
||||
EXPECT_NE(first, second);
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(first));
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(second));
|
||||
EXPECT_EQ(second.stem().string().substr(second.stem().string().size() - 2),
|
||||
"-2");
|
||||
EXPECT_EQ(std::filesystem::canonical(paths.logs_root() / "latest.log"),
|
||||
std::filesystem::canonical(second));
|
||||
}
|
||||
|
||||
TEST(SessionLogTest, SkipsIsolatedCanary) {
|
||||
TemporaryDirectory temporary;
|
||||
const MapEnvironment environment({
|
||||
{"HOME", temporary.root().string()},
|
||||
{"MOCKTAIL_ISOLATED_CANARY", "1"},
|
||||
});
|
||||
const RuntimePaths paths = RuntimePaths::FromEnvironment(environment);
|
||||
SessionLog log = SessionLog::Start(environment, paths);
|
||||
EXPECT_FALSE(log);
|
||||
EXPECT_FALSE(log.attempted());
|
||||
EXPECT_FALSE(std::filesystem::exists(paths.logs_root()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
Reference in new issue
Block a user