diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml new file mode 100644 index 0000000..0162a24 --- /dev/null +++ b/.github/workflows/aur-publish.yml @@ -0,0 +1,118 @@ +name: Publish AUR packages + +on: + workflow_dispatch: + inputs: + tag: + description: Release tag to publish, such as v0.3.4 + required: true + type: string + workflow_call: + inputs: + tag: + required: true + type: string + secrets: + AUR_SSH_PRIVATE_KEY: + required: false + +permissions: + contents: read + +concurrency: + group: aur-publish + cancel-in-progress: false + +jobs: + publish: + name: Publish nightcap and nightcap-bin + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out the release + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag }} + persist-credentials: false + + - name: Render the recipes + shell: bash + env: + TAG: ${{ inputs.tag }} + run: | + set -Eeuo pipefail + [[ "$TAG" =~ ^v[0-9]+(\.[0-9]+)*$ ]] + version="${TAG#v}" + archive="nightcap-${version}-1-x86_64.pkg.tar.zst" + # nightcap-bin installs the pacman package from the release page, so + # its checksum is the one published beside that asset. + checksum="$(curl -fsSL \ + "https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${archive}.sha256" | + awk '{print $1}')" + [[ "$checksum" =~ ^[0-9a-f]{64}$ ]] + + sed -i "s/^pkgver=.*/pkgver=${version}/" \ + packaging/aur/nightcap/PKGBUILD packaging/aur/nightcap-bin/PKGBUILD + sed -i "s/^sha256sums=('SKIP')$/sha256sums=('${checksum}')/" \ + packaging/aur/nightcap-bin/PKGBUILD + grep -Fqx "sha256sums=('${checksum}')" packaging/aur/nightcap-bin/PKGBUILD + + docker run --rm \ + --volume "$GITHUB_WORKSPACE/packaging/aur:/aur" \ + ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c ' + useradd --create-home builder + for package in nightcap nightcap-bin; do + install -d -o builder -g builder "/tmp/${package}" + install -o builder -g builder -m 0644 \ + "/aur/${package}/PKGBUILD" "/tmp/${package}/PKGBUILD" + runuser -u builder -- bash -c \ + "cd /tmp/${package} && makepkg --printsrcinfo" \ + > "/aur/${package}/.SRCINFO" + done + ' + + for package in nightcap nightcap-bin; do + grep -Fqx "pkgver=${version}" "packaging/aur/${package}/PKGBUILD" + grep -Fqx $'\tpkgver = '"${version}" "packaging/aur/${package}/.SRCINFO" + done + echo "VERSION=${version}" >> "$GITHUB_ENV" + + - name: Push to the AUR + shell: bash + env: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + run: | + set -Eeuo pipefail + if [[ -z "${AUR_SSH_PRIVATE_KEY}" ]]; then + echo "::notice::AUR_SSH_PRIVATE_KEY is not set; nothing was pushed to the AUR" + exit 0 + fi + + ssh_dir="$(mktemp -d)" + trap 'rm -rf "$ssh_dir"' EXIT + install -m 0600 /dev/null "$ssh_dir/key" + printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$ssh_dir/key" + # The AUR lists this key's fingerprint on its front page: + # SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4 + printf '%s\n' \ + 'aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN' \ + > "$ssh_dir/known_hosts" + export GIT_SSH_COMMAND="ssh -i $ssh_dir/key -o IdentitiesOnly=yes -o UserKnownHostsFile=$ssh_dir/known_hosts -o StrictHostKeyChecking=yes" + + for package in nightcap nightcap-bin; do + checkout="$ssh_dir/$package" + git clone "ssh://aur@aur.archlinux.org/${package}.git" "$checkout" + cp "packaging/aur/${package}/PKGBUILD" "packaging/aur/${package}/.SRCINFO" \ + "$checkout/" + git -C "$checkout" add PKGBUILD .SRCINFO + if git -C "$checkout" diff --cached --quiet; then + echo "::notice::${package} is already at ${VERSION}" + continue + fi + git -C "$checkout" \ + -c user.name='Nightcap release' \ + -c user.email='CoderDayton@users.noreply.github.com' \ + commit --quiet -m "Update to ${VERSION}" + # The AUR only accepts the master branch. + git -C "$checkout" push origin HEAD:master + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index feacf6a..0af2e07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -336,3 +336,15 @@ jobs: --title "Nightcap $tag" --notes-file release/notes.md gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \ "${uploads[@]}" + + # nightcap-bin downloads the pacman package from the release page, so this + # waits for the upload. + aur: + name: Publish AUR packages + if: startsWith(github.ref, 'refs/tags/v') + needs: publish + uses: ./.github/workflows/aur-publish.yml + with: + tag: ${{ github.ref_name }} + secrets: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b77ed6..35f13e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ Release notes on GitHub are taken from the matching section here. ships at the same path, such as a cursor, a font or a sky texture, and it stays in place when Roblox updates. The layout matches Sober's `asset_overlay` folder. `MOCKTAIL_ASSET_OVERLAY_DIR` moves it. Thanks to @dim-ghub. +- The asset overlay folder now also replaces files Roblox downloads and no longer ships, such as the shift + lock and text cursors. Put the file under `content/`, at the path Roblox uses. Thanks to @dim-ghub for the + idea. +- `MOCKTAIL_ASSET_TRACE=1` now also prints the files Roblox asks for and does not get, as `[asset] miss`. +- Arch Linux: `nightcap-git` is on the AUR and builds the current `main` branch. Thanks to @dim-ghub, who + maintains it. ## [0.3.4] - 2026-10-11 diff --git a/CMakeLists.txt b/CMakeLists.txt index 967edad..f6cd25c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -300,6 +300,7 @@ add_library(mocktail_runtime STATIC src/runtime/payload_update_preflight.cc src/runtime/process_launch_policy.cc src/runtime/process_diagnostics.cc + src/runtime/asset_overlay_names.cc src/runtime/runtime_config.cc src/runtime/runtime_config_bootstrap.cc src/runtime/runtime_config_file.cc diff --git a/README.md b/README.md index 51ac2c0..c4fcbe6 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,17 @@ sudo pacman -U nightcap--1-x86_64.pkg.tar.zst ``` They install `/usr/bin/mocktail`, so they conflict with upstream's `mocktail` -package. Nightcap is not in any distribution repository or the AUR; the -recipes under `packaging/aur` are kept for local builds only. +package. + +On Arch Linux, [`nightcap-git`](https://aur.archlinux.org/packages/nightcap-git) +is on the AUR. It builds the current `main` branch and is maintained by +[@dim-ghub](https://github.com/dim-ghub): + +```bash +yay -S nightcap-git +``` + +Nightcap is in no other distribution repository. Or build and install from source using the steps under [Building](#building). diff --git a/include/runtime/asset_overlay_names.h b/include/runtime/asset_overlay_names.h new file mode 100644 index 0000000..755589b --- /dev/null +++ b/include/runtime/asset_overlay_names.h @@ -0,0 +1,27 @@ +#ifndef MOCKTAIL_RUNTIME_ASSET_OVERLAY_NAMES_H_ +#define MOCKTAIL_RUNTIME_ASSET_OVERLAY_NAMES_H_ + +#include +#include +#include +#include +#include + +namespace mocktail { +namespace runtime { + +// The rbxasset:// name of every regular file under /content, such +// as rbxasset://textures/MouseLockedCursor.png. Sorted. +std::vector OverlayRbxAssetNames( + const std::filesystem::path& overlay_dir); + +// Roblox opens no local file for an rbxasset:// name it maps to a download. +// Writes '!' over the first byte of each whole NUL-terminated copy of a name +// in `data`, which unmaps it. Returns the number of copies changed. +std::size_t DisableAssetNameMappings(std::uint8_t* data, std::size_t size, + const std::vector& names); + +} +} + +#endif diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 6284e50..c28a657 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: vii pkgname=nightcap -pkgver=0.3.1 +pkgver=0.3.4 pkgrel=1 pkgdesc='Android x86-64 Roblox compatibility runtime for Linux' arch=('x86_64') diff --git a/packaging/aur/nightcap-bin/.SRCINFO b/packaging/aur/nightcap-bin/.SRCINFO index 7fd41dd..1c3c097 100644 --- a/packaging/aur/nightcap-bin/.SRCINFO +++ b/packaging/aur/nightcap-bin/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = nightcap-bin pkgdesc = Android x86-64 Roblox compatibility runtime for Linux (prebuilt) - pkgver = 0.3.1 + pkgver = 0.3.4 pkgrel = 1 url = https://github.com/CoderDayton/nightcap arch = x86_64 @@ -34,10 +34,10 @@ pkgbase = nightcap-bin conflicts = mocktail conflicts = nightcap conflicts = nightcap-git - noextract = nightcap-0.3.1-1-x86_64.pkg.tar.zst + noextract = nightcap-0.3.4-1-x86_64.pkg.tar.zst options = !debug options = !strip - source = nightcap-0.3.1-1-x86_64.pkg.tar.zst::https://github.com/CoderDayton/nightcap/releases/download/v0.3.1/nightcap-0.3.1-1-x86_64.pkg.tar.zst + source = nightcap-0.3.4-1-x86_64.pkg.tar.zst::https://github.com/CoderDayton/nightcap/releases/download/v0.3.4/nightcap-0.3.4-1-x86_64.pkg.tar.zst sha256sums = SKIP pkgname = nightcap-bin diff --git a/packaging/aur/nightcap-bin/PKGBUILD b/packaging/aur/nightcap-bin/PKGBUILD index 3695011..73eb507 100644 --- a/packaging/aur/nightcap-bin/PKGBUILD +++ b/packaging/aur/nightcap-bin/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: vii pkgname=nightcap-bin -pkgver=0.3.1 +pkgver=0.3.4 pkgrel=1 pkgdesc='Android x86-64 Roblox compatibility runtime for Linux (prebuilt)' arch=('x86_64') diff --git a/packaging/aur/nightcap/.SRCINFO b/packaging/aur/nightcap/.SRCINFO index d2be0f0..33e5d6c 100644 --- a/packaging/aur/nightcap/.SRCINFO +++ b/packaging/aur/nightcap/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = nightcap pkgdesc = Android x86-64 Roblox compatibility runtime for Linux - pkgver = 0.3.1 + pkgver = 0.3.4 pkgrel = 1 url = https://github.com/CoderDayton/nightcap arch = x86_64 @@ -42,7 +42,7 @@ pkgbase = nightcap conflicts = nightcap-git options = !debug options = !lto - source = nightcap::git+https://github.com/CoderDayton/nightcap.git#tag=v0.3.1 + source = nightcap::git+https://github.com/CoderDayton/nightcap.git#tag=v0.3.4 sha256sums = SKIP pkgname = nightcap diff --git a/packaging/aur/nightcap/PKGBUILD b/packaging/aur/nightcap/PKGBUILD index 69c6233..9ee5bf4 100644 --- a/packaging/aur/nightcap/PKGBUILD +++ b/packaging/aur/nightcap/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: vii pkgname=nightcap -pkgver=0.3.1 +pkgver=0.3.4 pkgrel=1 pkgdesc='Android x86-64 Roblox compatibility runtime for Linux' arch=('x86_64') diff --git a/src/legacy/legacy_runtime.cc b/src/legacy/legacy_runtime.cc index 0de58c4..02fee3f 100644 --- a/src/legacy/legacy_runtime.cc +++ b/src/legacy/legacy_runtime.cc @@ -70,6 +70,7 @@ #include "runtime/jnivm_platform_web_callbacks.h" #include "runtime/owned_pthread.h" #include "runtime/platform_cache_migration.h" +#include "runtime/asset_overlay_names.h" #include "runtime/roblox_app_lifecycle.h" #include "runtime/roblox_capability_resolver.h" #include "runtime/roblox_platform_web_symbols.h" @@ -1964,6 +1965,48 @@ extern "C" void MocktailConstructorWrapper() { MaybePrintLibRobloxConstructorSummary(idx); } +// Unmaps the rbxasset:// names that have a file in the asset overlay folder, so +// Roblox opens those files instead of downloading the stock asset. Must run +// before any libroblox code does: the segment loses PROT_EXEC while it is +// written. +void DisableOverlaidAssetMappings(uintptr_t base) { + const std::vector names = mocktail::runtime::OverlayRbxAssetNames( + CurrentRuntimePaths().asset_overlay_dir()); + const long page_size = sysconf(_SC_PAGESIZE); + if (names.empty() || page_size <= 0) { + return; + } + const auto* header = reinterpret_cast(base); + const auto* segments = + reinterpret_cast(base + header->e_phoff); + std::size_t changed = 0; + for (std::size_t index = 0; index < header->e_phnum; ++index) { + const Elf64_Phdr& segment = segments[index]; + if (segment.p_type != PT_LOAD || (segment.p_flags & PF_W) != 0 || + segment.p_filesz == 0) { + continue; + } + const uintptr_t begin = base + segment.p_vaddr; + const uintptr_t page_mask = static_cast(page_size) - 1; + const uintptr_t page_begin = begin & ~page_mask; + const std::size_t span = + ((begin + segment.p_filesz + page_mask) & ~page_mask) - page_begin; + const int original = + PROT_READ | ((segment.p_flags & PF_X) != 0 ? PROT_EXEC : 0); + if (mprotect(reinterpret_cast(page_begin), span, + PROT_READ | PROT_WRITE) != 0) { + continue; + } + changed += mocktail::runtime::DisableAssetNameMappings( + reinterpret_cast(begin), segment.p_filesz, names); + mprotect(reinterpret_cast(page_begin), span, original); + } + std::cout << " [asset] overlay holds " << names.size() + << " rbxasset file(s); unmapped " << changed + << " download name(s)\n" + << std::flush; +} + extern "C" void mocktail_before_soinfo_constructors(const char* realpath, uintptr_t base) { if (realpath == nullptr || std::strstr(realpath, "libroblox.so") == nullptr || @@ -1972,6 +2015,7 @@ extern "C" void mocktail_before_soinfo_constructors(const char* realpath, } g_libroblox_base = base; + DisableOverlaidAssetMappings(base); g_mocktail_abort_libroblox_base = base; g_libroblox_base_static = base; diff --git a/src/runtime/asset_overlay_names.cc b/src/runtime/asset_overlay_names.cc new file mode 100644 index 0000000..9eb0d92 --- /dev/null +++ b/src/runtime/asset_overlay_names.cc @@ -0,0 +1,64 @@ +#include "runtime/asset_overlay_names.h" + +#include +#include +#include + +namespace mocktail { +namespace runtime { + +std::vector OverlayRbxAssetNames( + const std::filesystem::path& overlay_dir) { + std::vector names; + const std::filesystem::path content = overlay_dir / "content"; + std::error_code error; + std::filesystem::recursive_directory_iterator it( + content, std::filesystem::directory_options::skip_permission_denied, + error); + const std::filesystem::recursive_directory_iterator end; + for (; !error && it != end; it.increment(error)) { + std::error_code entry_error; + if (!it->is_regular_file(entry_error)) { + continue; + } + const std::filesystem::path relative = + it->path().lexically_relative(content); + names.push_back("rbxasset://" + relative.generic_string()); + } + std::sort(names.begin(), names.end()); + return names; +} + +std::size_t DisableAssetNameMappings(std::uint8_t* data, std::size_t size, + const std::vector& names) { + std::size_t changed = 0; + if (data == nullptr) { + return changed; + } + std::uint8_t* const end = data + size; + for (const std::string& name : names) { + // The trailing NUL is part of the needle, so a longer string never matches. + const std::size_t needle_size = name.size() + 1; + if (name.empty() || needle_size > size) { + continue; + } + std::uint8_t* cursor = data; + while (cursor + needle_size <= end) { + auto* found = static_cast( + memmem(cursor, static_cast(end - cursor), name.c_str(), + needle_size)); + if (found == nullptr) { + break; + } + if (found == data || found[-1] == 0) { + found[0] = '!'; + ++changed; + } + cursor = found + needle_size; + } + } + return changed; +} + +} // namespace runtime +} // namespace mocktail diff --git a/stubs/libandroid_stub.cc b/stubs/libandroid_stub.cc index 81f8de9..95a1185 100644 --- a/stubs/libandroid_stub.cc +++ b/stubs/libandroid_stub.cc @@ -388,7 +388,8 @@ AAsset* AAssetManager_open(AAssetManager* mgr, const char* filename, ResolvedAsset resolved = ResolveAssetPath(filename); if (resolved.path.empty()) { if (AssetTraceEnabled()) { - std::fprintf(stderr, "[asset] open failed for %s\n", filename); + std::fprintf(stderr, "[asset] miss %s\n", + filename != nullptr ? filename : "(null)"); } return nullptr; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6f54b9d..0166d2a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -304,6 +304,12 @@ target_link_libraries(runtime_paths_test PRIVATE GTest::gtest_main ) +add_executable(asset_overlay_names_test asset_overlay_names_test.cc) +target_link_libraries(asset_overlay_names_test PRIVATE + Mocktail::Runtime + GTest::gtest_main +) + add_executable(single_instance_lock_test single_instance_lock_test.cc) target_link_libraries(single_instance_lock_test PRIVATE Mocktail::Runtime @@ -1148,6 +1154,7 @@ gtest_discover_tests(game_mode_link_namespace_test PROPERTIES ) gtest_discover_tests(memory_limit_test) gtest_discover_tests(runtime_paths_test) +gtest_discover_tests(asset_overlay_names_test) gtest_discover_tests(single_instance_lock_test) gtest_discover_tests(supported_launch_policy_test) gtest_discover_tests(process_launch_policy_test) diff --git a/tests/asset_overlay_names_test.cc b/tests/asset_overlay_names_test.cc new file mode 100644 index 0000000..06c19b2 --- /dev/null +++ b/tests/asset_overlay_names_test.cc @@ -0,0 +1,122 @@ +#include "runtime/asset_overlay_names.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +using mocktail::runtime::DisableAssetNameMappings; +using mocktail::runtime::OverlayRbxAssetNames; + +class TemporaryDirectory { + public: + TemporaryDirectory() { + char pattern[] = "/tmp/mocktail_asset_overlay_names_XXXXXX"; + char* created = mkdtemp(pattern); + if (created != nullptr) { + path_ = created; + } + } + + ~TemporaryDirectory() { + std::error_code error; + std::filesystem::remove_all(path_, error); + } + + const std::filesystem::path& path() const { return path_; } + + private: + std::filesystem::path path_; +}; + +void WriteFile(const std::filesystem::path& path) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream(path) << "x"; +} + +// Joins strings the way a string table stores them: each followed by a NUL. +std::vector StringTable( + const std::vector& strings) { + std::vector table; + for (std::string_view text : strings) { + table.insert(table.end(), text.begin(), text.end()); + table.push_back(0); + } + return table; +} + +std::string TextAt(const std::vector& table, std::size_t offset) { + return std::string(reinterpret_cast(table.data()) + offset); +} + +TEST(AssetOverlayNamesTest, ListsFilesUnderContentAsRbxAssetNames) { + TemporaryDirectory overlay; + ASSERT_FALSE(overlay.path().empty()); + WriteFile(overlay.path() / "content/textures/MouseLockedCursor.png"); + WriteFile(overlay.path() / + "content/textures/Cursors/KeyboardMouse/IBeamCursor.png"); + WriteFile(overlay.path() / "notes.txt"); + std::filesystem::create_directories(overlay.path() / "content/empty"); + + EXPECT_EQ(OverlayRbxAssetNames(overlay.path()), + (std::vector{ + "rbxasset://textures/Cursors/KeyboardMouse/IBeamCursor.png", + "rbxasset://textures/MouseLockedCursor.png", + })); +} + +TEST(AssetOverlayNamesTest, MissingOverlayFolderHasNoNames) { + TemporaryDirectory overlay; + ASSERT_FALSE(overlay.path().empty()); + + EXPECT_TRUE(OverlayRbxAssetNames(overlay.path() / "absent").empty()); + EXPECT_TRUE(OverlayRbxAssetNames(overlay.path()).empty()); +} + +TEST(AssetOverlayNamesTest, ChangesOnlyTheNamedWholeStrings) { + const std::string name = "rbxasset://textures/MouseLockedCursor.png"; + const std::string longer = name + ".bak"; + const std::string prefixed = "x" + name; + std::vector table = + StringTable({"first", name, longer, prefixed, name}); + const std::size_t first_copy = 6; + const std::size_t longer_copy = first_copy + name.size() + 1; + const std::size_t prefixed_copy = longer_copy + longer.size() + 1; + const std::size_t second_copy = prefixed_copy + prefixed.size() + 1; + + EXPECT_EQ(DisableAssetNameMappings(table.data(), table.size(), {name}), 2u); + + EXPECT_EQ(TextAt(table, 0), "first"); + EXPECT_EQ(TextAt(table, first_copy), "!" + name.substr(1)); + EXPECT_EQ(TextAt(table, longer_copy), longer); + EXPECT_EQ(TextAt(table, prefixed_copy), prefixed); + EXPECT_EQ(TextAt(table, second_copy), "!" + name.substr(1)); +} + +TEST(AssetOverlayNamesTest, MatchesANameAtTheStartOfTheData) { + const std::string name = "rbxasset://textures/IBeamCursor.png"; + std::vector table = StringTable({name, "other"}); + + EXPECT_EQ(DisableAssetNameMappings(table.data(), table.size(), {name}), 1u); + EXPECT_EQ(TextAt(table, 0), "!" + name.substr(1)); +} + +TEST(AssetOverlayNamesTest, LeavesDataAloneWithoutAMatch) { + std::vector table = StringTable({"alpha", "beta"}); + const std::vector before = table; + + EXPECT_EQ(DisableAssetNameMappings(table.data(), table.size(), + {"rbxasset://missing.png", ""}), + 0u); + EXPECT_EQ(table, before); + EXPECT_EQ(DisableAssetNameMappings(nullptr, 0, {"alpha"}), 0u); +} + +} // namespace diff --git a/tests/native_packaging_test.sh b/tests/native_packaging_test.sh index d086e5e..70a21be 100755 --- a/tests/native_packaging_test.sh +++ b/tests/native_packaging_test.sh @@ -15,6 +15,7 @@ readonly AUR_BIN_SRCINFO="${ROOT}/packaging/aur/nightcap-bin/.SRCINFO" readonly AUR_PKGBUILD="${ROOT}/packaging/aur/nightcap-git/PKGBUILD" readonly AUR_SRCINFO="${ROOT}/packaging/aur/nightcap-git/.SRCINFO" readonly AUR_WORKFLOW="${ROOT}/.github/workflows/aur.yml" +readonly AUR_PUBLISH_WORKFLOW="${ROOT}/.github/workflows/aur-publish.yml" readonly WORKFLOW="${ROOT}/.github/workflows/packages.yml" readonly RELEASE_WORKFLOW="${ROOT}/.github/workflows/release.yml" readonly README="${ROOT}/README.md" @@ -93,11 +94,11 @@ grep -Fq 'git describe' "${AUR_PKGBUILD}" || grep -Fq 'git describe' "${AUR_WORKFLOW}" || Fail 'AUR workflow does not version from the release tag' for pkgbuild in "${PKGBUILD}" "${AUR_STABLE_PKGBUILD}" "${AUR_BIN_PKGBUILD}"; do - grep -Fqx 'pkgver=0.3.1' "${pkgbuild}" || + grep -Fqx 'pkgver=0.3.4' "${pkgbuild}" || Fail "package version does not match the release being prepared: ${pkgbuild}" done -# Nightcap keeps its AUR recipes in the tree. Nothing publishes them. +# aur.yml only checks the nightcap-git recipe. Publishing is aur-publish.yml. for unexpected in \ 'aur.archlinux.org' \ 'AUR_SSH_PRIVATE_KEY' \ @@ -106,6 +107,34 @@ for unexpected in \ Fail "AUR workflow should not publish: ${unexpected}" fi done + +for expected in \ + 'workflow_call:' \ + 'workflow_dispatch:' \ + 'for package in nightcap nightcap-bin' \ + '${archive}.sha256' \ + 'makepkg --printsrcinfo' \ + 'secrets.AUR_SSH_PRIVATE_KEY' \ + 'ssh://aur@aur.archlinux.org/' \ + 'aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN' \ + 'StrictHostKeyChecking=yes'; do + grep -Fq -- "${expected}" "${AUR_PUBLISH_WORKFLOW}" || + Fail "AUR publish workflow is missing: ${expected}" +done + +# The publish workflow holds the AUR key. It runs for a release tag or by hand, +# never for a push or a pull request, and it leaves nightcap-git to its own +# maintainer. +for unexpected in \ + 'pull_request' \ + 'push:' \ + 'nightcap-git' \ + 'StrictHostKeyChecking=no' \ + 'set -x'; do + if grep -Fq -- "${unexpected}" "${AUR_PUBLISH_WORKFLOW}"; then + Fail "AUR publish workflow should not contain: ${unexpected}" + fi +done grep -Fq 'LINKER:--no-as-needed' "${STUB_CMAKE}" || Fail 'system shims can lose their host libc dependencies' grep -Fq 'MOCKTAIL_MINIZIP_HAS_STREAM_TELL' "${STUB_CMAKE}" || @@ -155,13 +184,14 @@ for expected in \ '-DMOCKTAIL_PACKAGE_NAME=nightcap' \ '-DMOCKTAIL_PACKAGE_VERSION=' \ 'Nightcap-x86_64.AppImage' \ - 'gh release upload'; do + 'gh release upload' \ + 'uses: ./.github/workflows/aur-publish.yml'; do grep -Fq -- "${expected}" "${RELEASE_WORKFLOW}" || Fail "release workflow is missing: ${expected}" done -# Nightcap publishes its packages on the release page only. Pushing them to -# APT, RPM or AUR repositories is out of scope for this workflow. +# This workflow publishes on the release page only. It hands the AUR push to +# aur-publish.yml, and APT and RPM repositories are out of scope. for unexpected in \ 'createrepo' \ 'reprepro' \ diff --git a/textures/README.md b/textures/README.md index d63aae5..c15a21d 100644 --- a/textures/README.md +++ b/textures/README.md @@ -57,9 +57,24 @@ Restart the game. The layout matches Sober's `asset_overlay` folder, so a file set made for Sober can be copied in as it is. The Flatpak build keeps the folder at `~/.var/app/io.github.CoderDayton.nightcap/data/mocktail/asset_overlay`. +Roblox downloads some of these files, such as the shift lock and text cursors, +and reads no local file for them. A file under `content/` in the overlay folder +still replaces one: at start, Nightcap changes that file's name inside the +loaded Roblox library, in memory only, so Roblox reads the local file. Nothing +is changed when the overlay folder holds no file. Roblox does not allow +modified clients, so use this at your own risk. + +```sh +dir=~/.local/share/mocktail/asset_overlay/content/textures +mkdir -p "$dir/Cursors/KeyboardMouse" +cp my-shift-lock.png "$dir/MouseLockedCursor.png" +cp my-text-cursor.png "$dir/Cursors/KeyboardMouse/IBeamCursor.png" +``` + Set `MOCKTAIL_ASSET_TRACE=1` to print every file Roblox opens as -`[asset] open -> `. A line that ends in `[overlay]` came from the -overlay folder. `MOCKTAIL_ASSET_OVERLAY_DIR` points the lookup somewhere else. +`[asset] open -> `, and every file it asks for and does not get as +`[asset] miss `. A line that ends in `[overlay]` came from the overlay +folder. `MOCKTAIL_ASSET_OVERLAY_DIR` points the lookup somewhere else. An overlay file changes the texture's bytes, so a texture override keyed on the original file's hash stops matching it. Use one of the two for a given texture.