diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index e1a377973..7da5b88a1 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -2,12 +2,12 @@ name: PR CI on: push: - branches: [ main ] + branches: [ Windows-subsystem-test ] paths-ignore: - '**.md' - '.gitignore' pull_request: - branches: [ main ] + branches: [ Windows-subsystem-test ] paths-ignore: - '**.md' - '.gitignore' @@ -172,6 +172,68 @@ jobs: # APK contents are already compressed; deflating ~560MB again wastes CPU. compression-level: 0 + # Experimental: proves the tree still configures and builds for x86_64 (WSA, Android-x86, ChromeOS, + # the emulator). It cannot fail a pull request. See docs/X86_64-WSA.md. + build-x86_64: + name: Build (x86_64, experimental) + runs-on: ubuntu-latest + continue-on-error: true + permissions: + contents: read + steps: + - name: Clone repository + uses: actions/checkout@v6 + with: + submodules: recursive + fetch-depth: 1 + persist-credentials: false + - name: Cache LFS + uses: actions/cache@v5 + with: + path: .git/lfs + key: lfs-${{ hashFiles('**/*.txz', '**/*.tzst') }} + restore-keys: lfs- + - name: Pull LFS files + run: git lfs pull + - name: Cache NDK build outputs + uses: actions/cache@v5 + with: + path: | + app/.cxx + app/build/intermediates/cxx + key: ndk-x86_64-${{ hashFiles('app/src/main/cpp/**', 'app/build.gradle', 'app/src/main/cpp/CMakeLists.txt') }} + restore-keys: | + ndk-x86_64- + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + with: + validate-wrappers: true + cache-cleanup: 'on-success' + cache-read-only: ${{ github.event_name == 'pull_request' }} + - name: Build x86_64 debug APK + timeout-minutes: 30 + env: + GRADLE_OPTS: -Xmx4g -XX:+UseG1GC + run: | + export KEYSTORE_FILE="${{ github.workspace }}/release.jks" + export KEYSTORE_PASSWORD="dummypass" KEY_ALIAS="winnative" KEY_PASSWORD="dummypass" + keytool -genkey -v -keystore "$KEYSTORE_FILE" -alias winnative -keyalg RSA -keysize 2048 \ + -validity 1 -storepass dummypass -keypass dummypass \ + -dname "CN=CI, OU=CI, O=CI, L=CI, ST=CI, C=US" + ./gradlew :app:assembleStandardDebug -Pwinnative.abis=x86_64 --build-cache --parallel -x lint + - name: Upload APK + uses: actions/upload-artifact@v7 + with: + name: apk-standard-x86_64 + path: app/build/outputs/apk/standard/debug/standard.apk + retention-days: 7 + if-no-files-found: error + compression-level: 0 cleanup-caches: needs: build if: ${{ always() && github.event_name == 'push' }} diff --git a/.github/workflows/x86_64-runtime.yml b/.github/workflows/x86_64-runtime.yml new file mode 100644 index 000000000..c47c78006 --- /dev/null +++ b/.github/workflows/x86_64-runtime.yml @@ -0,0 +1,232 @@ +name: x86_64 runtime (experimental) + +# Builds what a native x86_64 Android host (Windows Subsystem for Android, Android-x86/BlissOS, +# ChromeOS, the Android Emulator) needs and this repository cannot ship yet: +# * a native x86_64-linux-android Wine (.wcp), reusing WinNative-Emu/proton-wine's build scripts +# (the same ones that build the published x86_64 and ARM64EC packages), +# * the SysV shared-memory shim (libandroid-sysvshm.so), +# * a base imagefs-x86_64.tzst assembled from the x86_64 Termux sysroot that Wine builds against. +# Run it by hand: Actions > x86_64 runtime (experimental) > Run workflow. Nothing is published +# automatically. See docs/X86_64-WSA.md. + +on: + workflow_dispatch: + inputs: + wine_ref: + description: 'WinNative-Emu/proton-wine branch, tag or commit to build' + required: true + default: proton_11.0 + version_label: + description: 'Version used in the package names and profile.json' + required: true + default: 11.0-1 + prefix_pack: + description: 'Prefix pack asset in the proton-wine "Assets" release' + required: true + default: prefixPack-x86_64-11.txz + page_size: + description: '4k = Android API 28 baseline. 16k = API 35 with 16 KB memory pages.' + type: choice + options: [ '4k', '16k' ] + default: '4k' + build_imagefs: + description: 'Also assemble a base imagefs-x86_64.tzst' + type: boolean + default: true + +permissions: + contents: read + +env: + ASSETS_URL: https://github.com/WinNative-Emu/proton-wine/releases/download/Assets + +jobs: + wine: + name: Wine and SysV shim (x86_64-linux-android) + runs-on: ubuntu-24.04 + timeout-minutes: 300 + env: + VERSION_LABEL: ${{ inputs.version_label }} + PAGE_FLAG: ${{ inputs.page_size == '16k' && '--enable-16kb-pages' || '' }} + steps: + - name: Free up disk space + run: | + sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/share/boost "$AGENT_TOOLSDIRECTORY" + df -h / + + - name: Check out WinNative-Emu/proton-wine + uses: actions/checkout@v4 + with: + repository: WinNative-Emu/proton-wine + ref: ${{ inputs.wine_ref }} + submodules: recursive + persist-credentials: false + + - name: Install build dependencies + run: | + sudo dpkg --add-architecture i386 + sudo apt-get update + sudo apt-get install -y \ + build-essential git wget curl unzip flex bison gettext autoconf automake libtool \ + pkg-config mingw-w64 gcc-multilib g++-multilib \ + libfreetype6-dev libfreetype6-dev:i386 libpng-dev libpng-dev:i386 \ + zlib1g-dev zlib1g-dev:i386 + + - name: Download and extract the x86_64 sysroot + run: | + mkdir -p "$HOME/termuxfs/x86_64" + cd "$HOME/termuxfs/x86_64" + wget -q "$ASSETS_URL/termuxfs-x86_64.tar" + tar -xf termuxfs-x86_64.tar + rm termuxfs-x86_64.tar + + - name: Cache Android NDK r27d + id: cache-ndk + uses: actions/cache@v4 + with: + path: ~/Android/Sdk/ndk/27.3.13750724 + key: android-ndk-r27d + - name: Install Android NDK r27d + if: steps.cache-ndk.outputs.cache-hit != 'true' + run: | + mkdir -p "$HOME/Android/Sdk/ndk" + cd "$HOME/Android/Sdk/ndk" + wget -q https://dl.google.com/android/repository/android-ndk-r27d-linux.zip + unzip -q android-ndk-r27d-linux.zip + mv android-ndk-r27d 27.3.13750724 + + - name: Cache LLVM MinGW + id: cache-llvm-mingw + uses: actions/cache@v4 + with: + path: ~/toolchains/llvm-mingw-20250920-ucrt-ubuntu-22.04-x86_64 + key: bylaws-llvm-mingw-20250920 + - name: Install LLVM MinGW + if: steps.cache-llvm-mingw.outputs.cache-hit != 'true' + run: | + mkdir -p "$HOME/toolchains" + cd "$HOME/toolchains" + wget -q https://github.com/bylaws/llvm-mingw/releases/download/20250920/llvm-mingw-20250920-ucrt-ubuntu-22.04-x86_64.tar.xz + tar -xf llvm-mingw-20250920-ucrt-ubuntu-22.04-x86_64.tar.xz + + - name: Autogen + run: bash build-scripts/build-step0-autogen.sh + + - name: Cache wine-tools + id: cache-wine-tools + uses: actions/cache@v4 + with: + path: wine-tools + key: wine-tools-${{ hashFiles('configure.ac', 'configure') }} + - name: Build wine-tools (step 0) + if: steps.cache-wine-tools.outputs.cache-hit != 'true' + run: bash build-scripts/build-step0.sh + + # Not a link-time dependency of Wine (the x86_64 script never links it); the app preloads it at + # run time. The published workflow skips it for x86_64, so failure here must not fail the job. + - name: Build the SysV shared-memory shim + continue-on-error: true + run: | + # PAGE_FLAG is empty or a single flag, so it must stay unquoted. + # shellcheck disable=SC2086 + bash build-scripts/build-step-x86_64.sh $PAGE_FLAG --build-sysvshm + + - name: Configure + run: | + # PAGE_FLAG is empty or a single flag, so it must stay unquoted. + # shellcheck disable=SC2086 + bash build-scripts/build-step-x86_64.sh $PAGE_FLAG --configure + - name: Build + run: | + # PAGE_FLAG is empty or a single flag, so it must stay unquoted. + # shellcheck disable=SC2086 + bash build-scripts/build-step-x86_64.sh $PAGE_FLAG --build + - name: Install + run: | + # PAGE_FLAG is empty or a single flag, so it must stay unquoted. + # shellcheck disable=SC2086 + bash build-scripts/build-step-x86_64.sh $PAGE_FLAG --install + + - name: Collect the SysV shim + if: always() + run: | + mkdir -p "$GITHUB_WORKSPACE/out/sysvshm" + find "$HOME/termuxfs/x86_64" "$GITHUB_WORKSPACE/android" -name 'libandroid-sysvshm*.so' -type f \ + -print -exec cp {} "$GITHUB_WORKSPACE/out/sysvshm/" \; 2>/dev/null || true + ls -la "$GITHUB_WORKSPACE/out/sysvshm" + + - name: Download the prefix pack + run: wget -q "$ASSETS_URL/${{ inputs.prefix_pack }}" -O "$GITHUB_WORKSPACE/prefixPack.txz" + + - name: Generate profile.json files + run: | + cat > "$GITHUB_WORKSPACE/profile.json" < "$GITHUB_WORKSPACE/profile-wine.json" + + - name: Package + run: | + set -euo pipefail + cd "$HOME/compiled-files-x86_64" + cp "$GITHUB_WORKSPACE/prefixPack.txz" "$GITHUB_WORKSPACE/profile.json" . + tar cJf "$GITHUB_WORKSPACE/proton-${VERSION_LABEL}-x86_64.wcp" bin lib share prefixPack.txz profile.json + cp "$GITHUB_WORKSPACE/profile-wine.json" profile.json + tar cJf "$GITHUB_WORKSPACE/proton-wine-${VERSION_LABEL}-x86_64.wcp.xz" bin lib share prefixPack.txz profile.json + ls -lh "$GITHUB_WORKSPACE"/proton-* + + - name: Upload Wine packages + uses: actions/upload-artifact@v4 + with: + name: proton-x86_64 + path: | + proton-*-x86_64.wcp + proton-wine-*-x86_64.wcp.xz + retention-days: 30 + if-no-files-found: error + - name: Upload the SysV shim + uses: actions/upload-artifact@v4 + with: + name: sysvshm-x86_64 + path: out/sysvshm/*.so + retention-days: 30 + if-no-files-found: warn + + imagefs: + name: Base imagefs-x86_64.tzst + needs: wine + if: ${{ !cancelled() && inputs.build_imagefs }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Check out this repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install zstd + run: sudo apt-get update && sudo apt-get install -y zstd + - name: Download the x86_64 sysroot + run: wget -q "$ASSETS_URL/termuxfs-x86_64.tar" -O "$RUNNER_TEMP/termuxfs-x86_64.tar" + - name: Assemble + run: | + mkdir -p out + bash tools/x86_64/assemble-imagefs.sh "$RUNNER_TEMP/termuxfs-x86_64.tar" out + - name: Report + if: always() + run: cat out/imagefs-report.txt >> "$GITHUB_STEP_SUMMARY" || true + - name: Upload imagefs + uses: actions/upload-artifact@v4 + with: + name: imagefs-x86_64 + path: out/imagefs-x86_64.tzst + retention-days: 30 + if-no-files-found: error + compression-level: 0 diff --git a/app/build.gradle b/app/build.gradle index bf9fbe83d..44c0f36fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,6 +177,23 @@ if (brandedVersionCode < playUnreachableVersionCodeBase || brandedVersionCode > throw new GradleException("VERSION_CODE ${appVersionCode} pushes the branded versionCode past the 32-bit limit") } +// Target ABIs. arm64-v8a stays the default so existing builds are unchanged. +// x86_64 is opt-in and targets Android-x86/BlissOS, Windows Subsystem for Android (WSA), +// ChromeOS and the Android Emulator. See docs/X86_64-WSA.md. +// ./gradlew assembleStandardDebug -Pwinnative.abis=x86_64 +// ./gradlew assembleStandardDebug -Pwinnative.abis=arm64-v8a,x86_64 +def winnativeSupportedAbis = ['arm64-v8a', 'x86_64'] +def winnativeAbis = (findProperty('winnative.abis') ?: 'arm64-v8a') + .toString().split(',').collect { it.trim() }.findAll { it }.unique() +if (winnativeAbis.isEmpty()) { + throw new GradleException("winnative.abis is empty (supported: ${winnativeSupportedAbis.join(', ')})") +} +winnativeAbis.each { abi -> + if (!winnativeSupportedAbis.contains(abi)) { + throw new GradleException("winnative.abis: unsupported ABI '${abi}' (supported: ${winnativeSupportedAbis.join(', ')})") + } +} + def coldClientVersionFile = rootProject.file("tools/gbe_fork.version") def coldClientVersion = coldClientVersionFile.exists() ? coldClientVersionFile.text.trim() : "unknown" @@ -202,12 +219,13 @@ android { } ndk { - abiFilters 'arm64-v8a' + abiFilters.addAll(winnativeAbis) } resConfigs "en", "da", "de", "es", "b+es+419", "fi", "fr", "hi", "it", "ja", "ko", "no", "pl", "pt", "pt-rBR", "ro", "ru", "sv", "th", "tr", "uk", "zh-rCN", "zh-rTW" buildConfigField("String", "COLD_CLIENT_VERSION", "\"${coldClientVersion}\"") + buildConfigField "String", "WN_ABIS", "\"${winnativeAbis.join(',')}\"" buildConfigField "int", "TRUE_VERSION_CODE", "${appVersionCode}" buildConfigField "String", "BUILD_ID", "\"${appBuildId}\"" buildConfigField "String", "PR_NUMBER", "\"${appPullRequest}\"" diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index bc3d2085f..db02d9d1b 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -38,12 +38,36 @@ if(NOT xz_POPULATED) set(BUILD_SHARED_LIBS "${_winlator_saved_bsl}") endif() +# ---------------------------------------------------------------------------- +# ABI switch. +# +# arm64-v8a is the shipping ABI. x86_64 (Android-x86/BlissOS, Windows Subsystem for Android, +# ChromeOS, the Android Emulator) runs Wine natively, so it needs no CPU translator, but several +# components below exist only for arm64 today. They are gated on WN_ARM64 so the tree still +# configures and builds for x86_64; the matching Java entry points already tolerate the libraries +# being absent. See docs/X86_64-WSA.md for the list and how to lift each restriction. +# +# adrenotools - libadrenotools is arm64-only (custom Adreno/Turnip drivers) +# wn-steam-* - the Rust Steam client is only built for aarch64-linux-android +# steamctrl - the vendored SDL3 prefab AAR only ships android.arm64-v8a +# waylandcomp - links a prebuilt aarch64 libwayland-server.so +# proot - the loader here only carries ARM/ARM64 assembly +# ---------------------------------------------------------------------------- +if(ANDROID_ABI STREQUAL "arm64-v8a" OR CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") + set(WN_ARM64 ON) +else() + set(WN_ARM64 OFF) + message(STATUS "WinNative: building for ${ANDROID_ABI}; arm64-only components are disabled") +endif() + add_subdirectory(thirdparty/dxbc EXCLUDE_FROM_ALL) add_subdirectory(patchelf) -add_subdirectory(adrenotools) -add_subdirectory(wn-steam-client) -add_subdirectory(wn-steam-bootstrap) -add_subdirectory(wn-libsteamclient) +if(WN_ARM64) + add_subdirectory(adrenotools) + add_subdirectory(wn-steam-client) + add_subdirectory(wn-steam-bootstrap) + add_subdirectory(wn-libsteamclient) +endif() find_package(curl REQUIRED CONFIG) @@ -200,13 +224,22 @@ target_include_directories(winlator PRIVATE target_compile_features(winlator PRIVATE cxx_std_17) +# Custom Adreno driver loading (libadrenotools) only exists for arm64; elsewhere vulkan.c falls +# back to the system Vulkan loader. Kept in one plain target_link_libraries() call below, because +# CMake forbids mixing the plain and keyword signatures on a single target. +set(WN_WINLATOR_ARM64_LIBS "") +if(WN_ARM64) + list(APPEND WN_WINLATOR_ARM64_LIBS adrenotools) + target_compile_definitions(winlator PRIVATE WN_HAS_ADRENOTOOLS=1) +endif() + target_link_libraries(winlator log android mediandk jnigraphics vulkan - adrenotools + ${WN_WINLATOR_ARM64_LIBS} dxbc libzstd_static liblzma @@ -239,14 +272,16 @@ add_library(wnaudiohost SHARED target_compile_options(wnaudiohost PRIVATE -Wall -Wextra -fvisibility=hidden) target_link_libraries(wnaudiohost aaudio android log) -find_package(SDL3 REQUIRED CONFIG) +if(WN_ARM64) + find_package(SDL3 REQUIRED CONFIG) -add_library(steamctrl SHARED - steamctrl/steam_controller_bridge.cpp -) -target_compile_options(steamctrl PRIVATE -Wall -Wextra -fvisibility=hidden) -target_link_libraries(steamctrl SDL3::SDL3 log) + add_library(steamctrl SHARED + steamctrl/steam_controller_bridge.cpp + ) + target_compile_options(steamctrl PRIVATE -Wall -Wextra -fvisibility=hidden) + target_link_libraries(steamctrl SDL3::SDL3 log) -# Embedded Wayland compositor (winewayland.drv sessions); see waylandcomp/CMakeLists.txt. -add_subdirectory(waylandcomp) -add_subdirectory(proot) + # Embedded Wayland compositor (winewayland.drv sessions); see waylandcomp/CMakeLists.txt. + add_subdirectory(waylandcomp) + add_subdirectory(proot) +endif() diff --git a/app/src/main/cpp/winlator/vulkan.c b/app/src/main/cpp/winlator/vulkan.c index 3e5b83251..7e01ba9ba 100644 --- a/app/src/main/cpp/winlator/vulkan.c +++ b/app/src/main/cpp/winlator/vulkan.c @@ -1,6 +1,8 @@ #include +#ifdef WN_HAS_ADRENOTOOLS #include "../adrenotools/include/adrenotools/driver.h" +#endif #include #include #include @@ -226,7 +228,11 @@ static void preload_vendor_icd_deps() { void *winlator_open_system_vulkan(void) { preload_vendor_icd_deps(); - return dlopen("/system/lib64/libvulkan.so", RTLD_LOCAL | RTLD_NOW); + void *handle = dlopen("/system/lib64/libvulkan.so", RTLD_LOCAL | RTLD_NOW); + // Not every 64-bit Android keeps the loader at that path (x86_64 images such as + // Windows Subsystem for Android or Android-x86 may not), so let the linker resolve it. + if (!handle) handle = dlopen("libvulkan.so", RTLD_LOCAL | RTLD_NOW); + return handle; } void *winlator_open_vulkan(JNIEnv *env, jobject context, const char *driver_name) { @@ -236,6 +242,13 @@ void *winlator_open_vulkan(JNIEnv *env, jobject context, const char *driver_name preload_vendor_icd_deps(); +#ifndef WN_HAS_ADRENOTOOLS + // libadrenotools only exists for arm64. Everywhere else a custom driver name degrades + // to the system Vulkan loader instead of failing the whole session. + (void)env; + (void)context; + return winlator_open_system_vulkan(); +#else char *driver_path = get_driver_path(env, context, driver_name); if (!driver_path || access(driver_path, F_OK) != 0) { free(driver_path); @@ -261,6 +274,7 @@ void *winlator_open_vulkan(JNIEnv *env, jobject context, const char *driver_name free(driver_path); if (!handle) return winlator_open_system_vulkan(); return handle; +#endif // WN_HAS_ADRENOTOOLS } static void init_original_vulkan() { diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 00461f0a6..aa68e4bf1 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -10372,7 +10372,7 @@ private void extractGraphicsDriverFiles() { + "' gameSource='" + gameSource + "') - system Vulkan driver will be used"); } - envVars.put("VK_ICD_FILENAMES", imageFs.getShareDir() + "/vulkan/icd.d/wrapper_icd.aarch64.json"); + envVars.put("VK_ICD_FILENAMES", imageFs.getShareDir() + "/vulkan/icd.d/wrapper_icd." + com.winlator.cmod.shared.android.HostPlatform.vulkanIcdCpuName() + ".json"); String vulkanVersion = graphicsDriverConfig.get("vulkanVersion"); if (vulkanVersion == null) vulkanVersion = "1.4"; diff --git a/app/src/main/runtime/display/environment/ImageFsInstaller.java b/app/src/main/runtime/display/environment/ImageFsInstaller.java index b3d1e9ea4..218e04db2 100644 --- a/app/src/main/runtime/display/environment/ImageFsInstaller.java +++ b/app/src/main/runtime/display/environment/ImageFsInstaller.java @@ -12,6 +12,7 @@ import com.winlator.cmod.runtime.content.AdrenotoolsManager; import com.winlator.cmod.runtime.wine.WineInfo; import com.winlator.cmod.shared.android.AppUtils; +import com.winlator.cmod.shared.android.HostPlatform; import com.winlator.cmod.shared.ui.toast.WinToast; import com.winlator.cmod.shared.io.FileUtils; import com.winlator.cmod.shared.io.TarCompressorUtils; @@ -30,6 +31,7 @@ public abstract class ImageFsInstaller { public static final byte LATEST_VERSION = 22; private static final String IMAGEFS_ARCHIVE = "imagefs.tzst"; + private static final String IMAGEFS_ARCHIVE_X86_64 = "imagefs-x86_64.tzst"; private static final TarCompressorUtils.Type IMAGEFS_ARCHIVE_TYPE = TarCompressorUtils.Type.ZSTD; private static final long IMAGEFS_EXTRACTED_BYTES = 869024992L; private static final int XZ_PROGRESS_COMPRESSION_RATIO = 22; @@ -412,13 +414,43 @@ private static boolean waitForExtraction(Future future) { } } + /** + * arm64 keeps its historical asset names. x86_64 ships its own image, because the rootfs holds + * native binaries and an arm64 one is useless there. + */ + private static String imageFsArchive() { + return HostPlatform.isX86_64() ? IMAGEFS_ARCHIVE_X86_64 : IMAGEFS_ARCHIVE; + } + + private static String imageFsShardPrefix() { + return HostPlatform.isX86_64() ? "imagefs-x86_64.part" : "imagefs.part"; + } + + private static boolean assetExists(Context context, String name) { + try (java.io.InputStream ignored = context.getAssets().open(name)) { + return true; + } catch (java.io.IOException e) { + return false; + } + } + private static boolean extractImageFs( android.app.Activity activity, File rootDir, OnExtractFileListener listener) { String[] shards = listImageFsShards(activity); if (shards.length == 0) { + if (HostPlatform.isX86_64() && !assetExists(activity, imageFsArchive())) { + Log.e( + "ImageFsInstaller", + "No runtime image for " + + HostPlatform.processAbi() + + ": asset '" + + imageFsArchive() + + "' is not packaged in this build. See docs/X86_64-WSA.md."); + return false; + } return waitForExtraction( TarCompressorUtils.extractAsync( - IMAGEFS_ARCHIVE_TYPE, activity, IMAGEFS_ARCHIVE, rootDir, listener)); + IMAGEFS_ARCHIVE_TYPE, activity, imageFsArchive(), rootDir, listener)); } int threads = Math.max(2, Math.min(shards.length, Runtime.getRuntime().availableProcessors())); ExecutorService pool = Executors.newFixedThreadPool(threads); @@ -443,7 +475,7 @@ private static String[] listImageFsShards(Context context) { String[] names = context.getAssets().list(""); if (names != null) { for (String name : names) { - if (name.startsWith("imagefs.part") && name.endsWith(".tzst")) shards.add(name); + if (name.startsWith(imageFsShardPrefix()) && name.endsWith(".tzst")) shards.add(name); } } } catch (Exception e) { diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java index 4425bd239..4be214d05 100644 --- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java @@ -406,10 +406,11 @@ public String execShellCommand(String command, boolean includeStderr) { // For arm64ec Wine builds the wine binary is native ARM64 — call it directly // with a fully-qualified path. Wrapping with box64 causes it to fail ELF - // header detection and adds overhead. - // For non-arm64ec, box64 translates the x86_64 Wine binary. + // header detection and adds overhead. The same holds for an x86_64 Wine on an + // x86_64 host (see WineInfo.runsNatively()). + // Otherwise box64 translates the x86_64 Wine binary. String finalCommand; - if (wineInfo != null && wineInfo.isArm64EC()) { + if (wineInfo != null && wineInfo.isDirectExec()) { // Resolve bare "wine" or "wineserver" to full path under the Wine bin directory if (command.startsWith("wine ") || command.equals("wine")) { finalCommand = winePath + "/wine" + command.substring(4); @@ -686,7 +687,10 @@ public void start() { long startTime = System.currentTimeMillis(); if (wineInfo.isArm64EC()) { extractEmulatorsDlls(); - } else extractBox64Files(); + } else if (!wineInfo.runsNatively()) { + // A native x86_64 Wine has no translator to install. + extractBox64Files(); + } copyDefaultBox64RCFile(); scheduleDependencyCheck(); @@ -1353,6 +1357,9 @@ private int execGuestProgram() { } envVars.put("HODLL", "libwow64fex.dll"); } + } else if (wineInfo.runsNatively()) { + // x86_64 Wine on an x86_64 host: nothing to translate, exec it directly. + command = winePath + "/" + guestExecutable; } else { command = imageFs.getBinDir() + "/box64 " + guestExecutable; } @@ -1390,7 +1397,7 @@ private int execGuestProgram() { + "MESA_VK_WSI_DEBUG=" + envVars.get("MESA_VK_WSI_DEBUG")); - String wineLauncher = wineInfo != null && wineInfo.isArm64EC() + String wineLauncher = wineInfo != null && wineInfo.isDirectExec() ? imageFs.getWinePath() + "/bin/wine" : imageFs.getBinDir() + "/box64 wine"; updatePrefixBeforeSession(envVars, wineLauncher, rootDir, imageFs); diff --git a/app/src/main/runtime/wine/WineInfo.java b/app/src/main/runtime/wine/WineInfo.java index 63ae79be3..84ae94b76 100644 --- a/app/src/main/runtime/wine/WineInfo.java +++ b/app/src/main/runtime/wine/WineInfo.java @@ -9,6 +9,7 @@ import com.winlator.cmod.runtime.content.ContentProfile; import com.winlator.cmod.runtime.content.ContentsManager; import com.winlator.cmod.runtime.display.environment.ImageFs; +import com.winlator.cmod.shared.android.HostPlatform; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -70,6 +71,24 @@ public boolean isArm64EC() { return arch.equals("arm64ec"); } + /** + * True when this Wine's binaries are built for the CPU we are running on, so it is exec'd + * directly with no CPU translator: an x86_64 Wine on an x86_64 Android (Android-x86/BlissOS, + * Windows Subsystem for Android, ChromeOS, the Android Emulator). On arm64 an x86_64 Wine still + * needs Box64, and an ARM64EC Wine has always been exec'd directly, so both keep their paths. + */ + public boolean runsNatively() { + return arch.equals("x86_64") && HostPlatform.isX86_64(); + } + + /** + * True when the guest is exec'd directly rather than being wrapped in {@code box64}: either an + * ARM64EC Wine (native ARM64) or an x86_64 Wine on an x86_64 host. + */ + public boolean isDirectExec() { + return isArm64EC() || runsNatively(); + } + public String identifier() { if (type.equals("proton")) return "proton-" + fullVersion() + "-" + arch; else return "wine-" + fullVersion() + "-" + arch; diff --git a/app/src/main/shared/android/HostPlatform.java b/app/src/main/shared/android/HostPlatform.java new file mode 100644 index 000000000..d4b423f11 --- /dev/null +++ b/app/src/main/shared/android/HostPlatform.java @@ -0,0 +1,128 @@ +package com.winlator.cmod.shared.android; + +import android.os.Build; +import com.winlator.cmod.BuildConfig; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** + * Which CPU architecture this process runs as, and a best-effort hint about the platform. + * + *

WinNative ships for arm64, where Windows (x86_64) software is translated by Box64 or FEXCore. + * On an x86_64 Android (Android-x86/BlissOS, Windows Subsystem for Android, ChromeOS, the Android + * Emulator) the guest already matches the CPU, so Wine runs natively and no translator is + * involved. Code that has to choose between the two asks this class rather than reading {@link + * Build#SUPPORTED_ABIS} directly, because the device's preferred ABI is not necessarily the ABI of + * this process: an arm64-only APK installed on an x86_64 device still runs as arm64 (translated by + * the platform), and only the ABI of the process matters for which of our native libraries loaded. + */ +public final class HostPlatform { + public static final String ABI_ARM64 = "arm64-v8a"; + public static final String ABI_X86_64 = "x86_64"; + + private static volatile String processAbi; + + private HostPlatform() {} + + /** + * The ABI our native libraries were loaded for. Mirrors the package manager's choice: the first + * ABI the device prefers that this APK actually contains ({@code BuildConfig.WN_ABIS}). + */ + public static String processAbi() { + String abi = processAbi; + if (abi == null) { + abi = pickProcessAbi(Build.SUPPORTED_ABIS, BuildConfig.WN_ABIS); + processAbi = abi; + } + return abi; + } + + /** True when this process is x86_64, so an x86_64 Wine runs with no CPU translation. */ + public static boolean isX86_64() { + return ABI_X86_64.equals(processAbi()); + } + + public static boolean isArm64() { + return ABI_ARM64.equals(processAbi()); + } + + /** + * Best-effort hint that this is Windows Subsystem for Android. Use it to tailor guidance and + * defaults only; never to gate functionality. Confirmed on a live WSA: model "Subsystem for + * Android(TM)", manufacturer "Microsoft Corporation", device "windows_x86_64", ABI list + * "x86_64,arm64-v8a,x86,armeabi-v7a,armeabi". Brand and product were not captured; {@link + * #describe()} logs every value. + */ + public static boolean isWindowsSubsystemForAndroid() { + return looksLikeWsa(Build.MODEL, Build.MANUFACTURER, Build.BRAND, Build.DEVICE, Build.PRODUCT); + } + + /** + * CPU name in Mesa's per-architecture Vulkan ICD manifests (Meson's {@code host_machine.cpu()}): + * the wrapper driver installs {@code wrapper_icd.aarch64.json} or {@code wrapper_icd.x86_64.json}. + */ + public static String vulkanIcdCpuName() { + return icdCpuNameFor(processAbi()); + } + + /** One line for logs and bug reports. */ + public static String describe() { + return "abi=" + + processAbi() + + " deviceAbis=" + + String.join(",", Build.SUPPORTED_ABIS) + + " apkAbis=" + + BuildConfig.WN_ABIS + + " model=" + + Build.MODEL + + " manufacturer=" + + Build.MANUFACTURER + + " brand=" + + Build.BRAND + + " device=" + + Build.DEVICE + + " product=" + + Build.PRODUCT + + " sdk=" + + Build.VERSION.SDK_INT + + " wsaHint=" + + isWindowsSubsystemForAndroid(); + } + + // -- Pure decision logic, kept free of android.* reads so it can be unit tested on the JVM. -- + + /** + * The first of {@code deviceAbis} (in the device's order of preference) that {@code apkAbisCsv} + * lists. Falls back to the device's first ABI when the APK's list is unknown or shares nothing + * with the device, and to an empty string when the device reports no ABIs at all. + */ + static String pickProcessAbi(String[] deviceAbis, String apkAbisCsv) { + if (deviceAbis == null || deviceAbis.length == 0) return ""; + if (apkAbisCsv == null || apkAbisCsv.trim().isEmpty()) return deviceAbis[0]; + Set apk = new HashSet<>(); + for (String abi : apkAbisCsv.split(",")) { + String trimmed = abi.trim(); + if (!trimmed.isEmpty()) apk.add(trimmed); + } + for (String abi : deviceAbis) { + if (apk.contains(abi)) return abi; + } + return deviceAbis[0]; + } + + static String icdCpuNameFor(String abi) { + return ABI_X86_64.equals(abi) ? "x86_64" : "aarch64"; + } + + static boolean looksLikeWsa( + String model, String manufacturer, String brand, String device, String product) { + if (lower(model).contains("subsystem for android")) return true; + if (lower(device).startsWith("windows") || lower(product).startsWith("windows")) return true; + return lower(manufacturer).contains("microsoft") && lower(brand).equals("windows"); + } + + private static String lower(String s) { + return s == null ? "" : s.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/app/src/test/kotlin/com/winlator/cmod/shared/android/HostPlatformTest.kt b/app/src/test/kotlin/com/winlator/cmod/shared/android/HostPlatformTest.kt new file mode 100644 index 000000000..2d9e44b44 --- /dev/null +++ b/app/src/test/kotlin/com/winlator/cmod/shared/android/HostPlatformTest.kt @@ -0,0 +1,98 @@ +package com.winlator.cmod.shared.android + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class HostPlatformTest { + // ro.product.cpu.abilist read from a live WSA. + private val wsaAbis = arrayOf("x86_64", "arm64-v8a", "x86", "armeabi-v7a", "armeabi") + private val phoneAbis = arrayOf("arm64-v8a", "armeabi-v7a", "armeabi") + + @Test + fun x86ApkOnWsaRunsAsX86_64() { + assertEquals("x86_64", HostPlatform.pickProcessAbi(wsaAbis, "arm64-v8a,x86_64")) + assertEquals("x86_64", HostPlatform.pickProcessAbi(wsaAbis, "x86_64")) + } + + @Test + fun arm64OnlyApkOnWsaStaysArm64() { + // The platform would run it through its ARM translation; our native libs are arm64. + assertEquals("arm64-v8a", HostPlatform.pickProcessAbi(wsaAbis, "arm64-v8a")) + } + + @Test + fun universalApkOnPhoneRunsAsArm64() { + assertEquals("arm64-v8a", HostPlatform.pickProcessAbi(phoneAbis, "arm64-v8a,x86_64")) + } + + @Test + fun apkWithNoCommonAbiFallsBackToDevicePreference() { + assertEquals("arm64-v8a", HostPlatform.pickProcessAbi(phoneAbis, "x86_64")) + } + + @Test + fun apkAbiListIsTrimmedAndTolerant() { + assertEquals("x86_64", HostPlatform.pickProcessAbi(wsaAbis, " arm64-v8a , x86_64 ,, ")) + assertEquals("x86_64", HostPlatform.pickProcessAbi(wsaAbis, null)) + assertEquals("x86_64", HostPlatform.pickProcessAbi(wsaAbis, " ")) + } + + @Test + fun deviceWithoutAbisYieldsEmpty() { + assertEquals("", HostPlatform.pickProcessAbi(null, "arm64-v8a")) + assertEquals("", HostPlatform.pickProcessAbi(emptyArray(), "arm64-v8a")) + } + + @Test + fun recognisesWsaByModel() { + assertTrue( + HostPlatform.looksLikeWsa( + "Subsystem for Android(TM)", "Microsoft Corporation", "Windows", "x", "y")) + } + + @Test + fun recognisesWsaByDeviceOrProduct() { + assertTrue(HostPlatform.looksLikeWsa("m", "m", "b", "windows_x86_64", "p")) + assertTrue(HostPlatform.looksLikeWsa("m", "m", "b", "d", "Windows_arm64")) + } + + @Test + fun recognisesWsaByManufacturerAndBrand() { + assertTrue(HostPlatform.looksLikeWsa("m", "Microsoft Corporation", "Windows", "d", "p")) + } + + @Test + fun doesNotFlagOrdinaryDevices() { + assertFalse(HostPlatform.looksLikeWsa("Pixel 8", "Google", "google", "shiba", "shiba")) + assertFalse( + HostPlatform.looksLikeWsa("SM-S928B", "samsung", "samsung", "e3q", "e3qxeea")) + } + + @Test + fun doesNotFlagRealMicrosoftHardware() { + // Microsoft-made phones are not WSA; the brand has to say Windows as well. + assertFalse(HostPlatform.looksLikeWsa("Surface Duo", "Microsoft Corporation", "Microsoft", "duo", "duo")) + } + + @Test + fun toleratesNulls() { + assertFalse(HostPlatform.looksLikeWsa(null, null, null, null, null)) + } + + @Test + fun recognisesTheLiveWsaValues() { + // getprop values captured from a real WSA install. + assertTrue( + HostPlatform.looksLikeWsa( + "Subsystem for Android(TM)", "Microsoft Corporation", "", "windows_x86_64", "")) + } + + @Test + fun icdManifestNameFollowsTheProcessAbi() { + assertEquals("x86_64", HostPlatform.icdCpuNameFor("x86_64")) + assertEquals("aarch64", HostPlatform.icdCpuNameFor("arm64-v8a")) + assertEquals("aarch64", HostPlatform.icdCpuNameFor("")) + } +} diff --git a/docs/X86_64-WSA.md b/docs/X86_64-WSA.md new file mode 100644 index 000000000..54a558918 --- /dev/null +++ b/docs/X86_64-WSA.md @@ -0,0 +1,125 @@ +# WinNative on x86_64 Android (WSA, Android-x86/BlissOS, ChromeOS, the emulator) + +**Status: experimental.** The build and runtime changes have been checked in isolation +([what is verified](#what-is-verified)). Nothing has run end to end on a device yet. + +On arm64, Windows x86_64 programs are translated (Box64, or FEX for ARM64EC Wine). On an x86_64 +Android the guest already matches the CPU, so an `x86_64-linux-android` Wine is exec'd directly with +no translator. `HostPlatform` decides which ABI the process runs as, and `WineInfo.runsNatively()` is +the switch the launcher uses. + +## Quick start (Windows Subsystem for Android) + +1. **APK.** `./gradlew :app:assembleStandardDebug -Pwinnative.abis=x86_64`, or take + `apk-standard-x86_64` from a *PR CI* run. The default ABI is still `arm64-v8a`, so existing builds + are unchanged. +2. **Runtime.** Actions → *x86_64 runtime (experimental)* → Run workflow. Artifacts: `proton-x86_64` + (Wine `.wcp`), `sysvshm-x86_64` (if the shim built) and `imagefs-x86_64` (a base image plus a + report of what is still missing). +3. **Place the pieces.** + + | Artifact | Goes to | Why | + |---|---|---| + | `libandroid-sysvshm.so` | `app/src/main/jniLibs/x86_64/` | the launcher copies it from `nativeLibraryDir` into the imagefs | + | `imagefs-x86_64.tzst` | `app/src/main/assets/` (track with Git LFS, it is large) | `ImageFsInstaller` picks it on x86_64 | + | `proton-…-x86_64.wcp` | import in the app's component manager | the Wine build | + + Rebuild the APK after adding the first two. +4. **Install.** `adb connect 127.0.0.1:58526`, then `adb install -r standard.apk`. +5. **Check graphics first** (next section). Nothing renders without Vulkan. + +## Graphics on WSA: what is known + +The Windows-side files of a WSA package show how Vulkan reaches the guest: + +* `vulkan_dzn.dll` is Mesa's **Dozen** (Vulkan on Direct3D 12): **Mesa 23.3.0-devel, commit + `d6de98fdfc`**, built July 2023. Its ICD manifest declares API version **1.1.0**. +* It ships beside `vulkan-1.dll`, ANGLE-style `libEGL.dll`/`libGLESv2.dll` and `dxil.dll`, all x64. + +So Vulkan in WSA is Vulkan-over-D3D12 on the Windows host, which fits it being an opt-in preview (WSA +2307 and later; enable it in the WSA Settings app). What the *Android guest* sees depends on the guest +driver, which those host files do not reveal. + +The app's own display path is Vulkan-only. The native renderer requests **Vulkan 1.1**, and its code +references the hardware-buffer import chain: `VK_ANDROID_external_memory_android_hardware_buffer`, +`VK_KHR_external_memory`, `VK_KHR_sampler_ycbcr_conversion`, `VK_EXT_queue_family_foreign`, +`VK_KHR_dedicated_allocation`, `VK_KHR_get_memory_requirements2`, plus `VK_KHR_swapchain` and +`VK_KHR_android_surface`. If the guest lacks them the X server window cannot be shown; the fix is a +software present path (see [Known gaps](#known-gaps)). + +Check on the device (Windows `cmd`): + +``` +adb shell pm list features | findstr /i vulkan +adb shell getprop ro.hardware.vulkan +adb shell cmd gpu vkjson > vkjson.json +``` + +Look for the extensions above and an API version of at least 1.1. + +## Milestones (also the test plan) + +| | Milestone | Passes when | Needs | +|---|---|---|---| +| M1 | APK runs on WSA | the app opens its UI | the x86_64 APK | +| M2 | X server window renders | starting a container shows the Wine desktop background | guest Vulkan 1.1 + hardware-buffer import, or a software present path | +| M3 | Wine reaches the desktop | `explorer` is visible and Notepad opens | `imagefs-x86_64`, the Wine `.wcp`, the SysV shim (audio also needs the PulseAudio libs) | +| M4 | Accelerated Direct3D | DXVK/VKD3D run | an x86_64 Vulkan wrapper | + +## What is gated on x86_64, and why + +`-Pwinnative.abis=x86_64` builds everything portable and skips what only exists for arm64: + +| Skipped | Reason | To lift | +|---|---|---| +| `adrenotools` | libadrenotools is arm64-only (custom Adreno drivers); `vulkan.c` falls back to the system loader | not applicable | +| Steam native (`wn-steam-*`) | the Rust client is only built for `aarch64-linux-android` | add the `x86_64-linux-android` Rust target | +| `steamctrl` | the vendored SDL3 prefab only ships `arm64-v8a` | ship an x86_64 SDL3 | +| `waylandcomp` | links a prebuilt aarch64 `libwayland-server.so` | provide an x86_64 build | +| `proot` (Linux/Proton sessions) | the loader here only has ARM/ARM64 assembly | restore the x86_64 loader from upstream proot | + +## Assets an x86_64 build still needs + +* **Wine**: built by CI ([below](#ci)), reusing WinNative-Emu/proton-wine's `build-step-x86_64.sh` + (NDK r27d, `x86_64-linux-android28`). +* **imagefs**: CI builds a *base* from the x86_64 Termux sysroot (about 1.2 GB including 143 MB of + headers: libxcb, freetype, fontconfig, PulseAudio client, SDL2, the Vulkan loader, Mesa GL/EGL and a + software rasterizer; no glibc loader). The arm64 image is a Git LFS object that has not been + inspected, so `tools/x86_64/imagefs-required.txt` lists only what the code reads. Known missing: + `libredirect.so` (source of an x86_64 build unknown). +* **PulseAudio and its support libraries** under `jniLibs/x86_64/`: they currently exist only under + `jniLibs/arm64-v8a/`. +* **Vulkan wrapper** (`wrapper_icd.x86_64.json`): no public CI recipe found, and Termux has no + package. Facts: the source is leegao/bionic-vulkan-wrapper (a Mesa fork, branch `wrapper`); it is + the Meson driver `wrapper`; its manifest name comes from `host_machine.cpu()`, which is why + `HostPlatform.vulkanIcdCpuName()` returns `x86_64`; `wrapper_private.h` declares + `adrenotools_open_libvulkan` as a strong external symbol that is only called when `ADRENOTOOLS_*` + is set, so an x86_64 build needs it weak or stubbed. + +## CI + +* **PR CI → `build-x86_64`**: experimental, cannot fail a PR, uploads `apk-standard-x86_64`. +* **`x86_64-runtime.yml`**: manual. Checks out WinNative-Emu/proton-wine (`wine_ref`, default + `proton_11.0`), builds the shim and Wine, packages the `.wcp`, then assembles the base imagefs. + `page_size=16k` targets API 35 with 16 KB pages. Nothing is published. + +## What is verified + +Verified: `vulkan.c` compiles with and without `WN_HAS_ADRENOTOOLS` (host gcc, Android headers +stubbed); the CMake gating evaluates correctly for `arm64-v8a`, `x86_64` and an undefined ABI; 14 JVM +unit tests for `HostPlatform`; the modified Java files parse no worse than their baseline; +`x86_64-runtime.yml` passes actionlint; `assemble-imagefs.sh` passes shellcheck and a functional test +against a fake sysroot. + +Not verified: a full Gradle/NDK build for either ABI; the workflows running on GitHub; anything on a +device. + +## Known gaps + +* No software present path: without guest Vulkan 1.1 and hardware-buffer import the display cannot + start. +* `WaylandCompositor` loads its native library in an unguarded static initializer, so enabling Wayland + on x86_64 throws. +* Steam, Retro and adrenotools UI are not hidden on x86_64. +* The x86_64 APK still packages the arm64 imagefs (about 200 MB). +* No in-app Vulkan diagnostic yet: `HostPlatform.describe()` exists but nothing logs it. diff --git a/tools/x86_64/assemble-imagefs.sh b/tools/x86_64/assemble-imagefs.sh new file mode 100755 index 000000000..ee00cd002 --- /dev/null +++ b/tools/x86_64/assemble-imagefs.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Assemble a BASE x86_64 imagefs from the x86_64 Termux sysroot that WinNative-Emu/proton-wine builds +# Wine against (termuxfs-x86_64.tar). +# +# STATUS: experimental, never run against a live app. The arm64 imagefs.tzst is a Git LFS object that +# has not been inspected, so what else it carries is unknown. This keeps everything the sysroot +# provides, drops development-only material, optionally adds extra libraries, checks the result +# against tools/x86_64/imagefs-required.txt and writes a report. Missing paths only fail the script +# when STRICT=1. +# +# Usage: assemble-imagefs.sh [extra-libs-dir] + +set -euo pipefail + +sysroot_tar=${1:?usage: $0 [extra-libs-dir]} +out_dir=${2:?usage: $0 [extra-libs-dir]} +extra_dir=${3:-} +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +required_list=${REQUIRED_LIST:-$here/imagefs-required.txt} +prefix='data/data/com.termux/files/usr' + +mkdir -p "$out_dir" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +root="$work/imagefs" + +echo "==> extracting usr/ from $(basename "$sysroot_tar")" +tar -xf "$sysroot_tar" -C "$work" "$prefix" +mkdir -p "$root" +mv "$work/$prefix" "$root/usr" + +echo "==> dropping development-only material" +rm -rf "$root/usr/include" "$root/usr/share/man" "$root/usr/share/doc" "$root/usr/share/info" \ + "$root/usr/share/aclocal" "$root/usr/lib/pkgconfig" "$root/usr/share/pkgconfig" "$root/usr/lib/cmake" +find "$root/usr" \( -name '*.a' -o -name '*.la' \) -type f -delete +mkdir -p "$root/tmp" "$root/etc" + +if [ -n "$extra_dir" ] && [ -d "$extra_dir" ]; then + echo "==> adding extra libraries from $extra_dir" + find "$extra_dir" -type f -name '*.so*' -exec cp -v {} "$root/usr/lib/" \; +fi + +echo "==> checking required paths" +lines=() +missing=0 +while IFS= read -r raw; do + path=${raw%%#*} + path=$(printf '%s' "$path" | xargs) + [ -n "$path" ] || continue + if [ -e "$root/$path" ] || [ -L "$root/$path" ]; then + lines+=("- [x] \`$path\`") + else + lines+=("- [ ] **missing** \`$path\`") + missing=$((missing + 1)) + fi +done < "$required_list" + +{ + echo "### x86_64 imagefs report" + echo + echo "Unpacked size: $(du -sh "$root" | cut -f1)" + echo + printf '%s\n' "${lines[@]}" + echo + echo "$missing required path(s) missing. See tools/x86_64/imagefs-required.txt for where each comes from." +} | tee "$out_dir/imagefs-report.txt" + +echo "==> packing imagefs-x86_64.tzst" +tar --owner=0 --group=0 --numeric-owner -C "$root" -cf - . | zstd -T0 -19 -q -f -o "$out_dir/imagefs-x86_64.tzst" +ls -lh "$out_dir/imagefs-x86_64.tzst" + +if [ "${STRICT:-0}" = 1 ] && [ "$missing" -gt 0 ]; then + echo "STRICT=1 and $missing required path(s) missing" >&2 + exit 1 +fi diff --git a/tools/x86_64/imagefs-required.txt b/tools/x86_64/imagefs-required.txt new file mode 100644 index 000000000..a0442ded9 --- /dev/null +++ b/tools/x86_64/imagefs-required.txt @@ -0,0 +1,14 @@ +# Paths the app reads inside the x86_64 imagefs (relative to the imagefs root), one per line. +# "#" starts a comment. Derived from ImageFs.java and GuestProgramLauncherComponent.java; anything else +# the arm64 image carries is unknown (imagefs.tzst is a Git LFS object that has not been inspected). +# +# NOT listed on purpose: libandroid-sysvshm.so. The launcher copies it from the APK's nativeLibraryDir +# (ensureImageFsNativeLibrary), so it belongs in app/src/main/jniLibs/x86_64/, not in the image. + +usr/lib # ImageFs.getLibDir(); the guest's LD_LIBRARY_PATH +usr/bin # ImageFs.getBinDir() +usr/share # ImageFs.getShareDir() +usr/lib/libc++_shared.so # C++ runtime for the bionic-linked Wine components +usr/lib/libvulkan.so.1 # Vulkan loader used by Wine's Vulkan layer +usr/lib/libredirect.so # LD_PRELOAD path redirection; source of the x86_64 build unknown +usr/share/vulkan/icd.d/wrapper_icd.x86_64.json # VK_ICD_FILENAMES target; needs an x86_64 wrapper build