From dfdf8dc4f0ea0d6d261e8b2dbfd810592a0805d9 Mon Sep 17 00:00:00 2001 From: bart Date: Tue, 12 May 2026 11:49:18 +0200 Subject: [PATCH] Moved migration to ensureImageFsSymlinks which is now called in main --- .../main/java/app/gamenative/ui/PluviaMain.kt | 8 +-- .../xenvironment/ImageFSLegacyMigrator.java | 6 +- .../xenvironment/ImageFsInstaller.java | 22 ++++++- .../xenvironment/ImageFSLegacyMigratorTest.kt | 57 ++++++++----------- .../xenvironment/ImageFsInstallerTest.kt | 26 +++++++++ 5 files changed, 76 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/app/gamenative/ui/PluviaMain.kt b/app/src/main/java/app/gamenative/ui/PluviaMain.kt index de597b3a3a..36327d4d47 100644 --- a/app/src/main/java/app/gamenative/ui/PluviaMain.kt +++ b/app/src/main/java/app/gamenative/ui/PluviaMain.kt @@ -1559,14 +1559,14 @@ fun preLaunchApp( // Migrate legacy on-disk imagefs layout (e.g. legacy Proton → shared paths) before manifest // installs or launch deps — resolveMissingManifestInstallRequests can install Proton too. val legacyImageFsRoot = File(context.filesDir, "imagefs") - val migrationOk = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded( + val symlinksOk = ImageFsInstaller.ensureImageFsSymlinks( context, legacyImageFsRoot, - container.wineVersion, + container, ) - if (!migrationOk) { + if (!symlinksOk) { Timber.tag("preLaunchApp").e( - "Legacy ImageFS migration failed: ${legacyImageFsRoot.absolutePath}", + "ImageFS symlinks failed: ${legacyImageFsRoot.absolutePath}", ) setLoadingDialogVisible(false) setMessageDialogState( diff --git a/app/src/main/java/com/winlator/xenvironment/ImageFSLegacyMigrator.java b/app/src/main/java/com/winlator/xenvironment/ImageFSLegacyMigrator.java index 6155c33d2b..efac9250ad 100644 --- a/app/src/main/java/com/winlator/xenvironment/ImageFSLegacyMigrator.java +++ b/app/src/main/java/com/winlator/xenvironment/ImageFSLegacyMigrator.java @@ -11,17 +11,15 @@ public final class ImageFSLegacyMigrator { private ImageFSLegacyMigrator() {} /** - * Migrate legacy directories if needed. After that, ensure the shared home and proton are symlinked. + * Migrate legacy directories if needed. */ - public static boolean migrateLegacyDirsIfNeeded(Context context, File legacyImageFsRoot, String wineVersion) { + public static boolean migrateLegacyDirsIfNeeded(Context context, File legacyImageFsRoot) { if (!migrateLegacyHomeToShared(context, legacyImageFsRoot)) { return false; } if (!migrateLegacyProtonToShared(context, legacyImageFsRoot)) { return false; } - ImageFsInstaller.ensureSharedHomeRoot(context, legacyImageFsRoot); - ImageFsInstaller.ensureProtonVersionSymlink(context, legacyImageFsRoot, wineVersion); return true; } diff --git a/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java b/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java index 84b05aa633..eba75e696a 100644 --- a/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java +++ b/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java @@ -211,7 +211,7 @@ private static void installGuestLibs(Context ctx) { public static Future installIfNeededFuture(final Context context, AssetManager assetManager, Container container, Callback onProgress) { ImageFs imageFs = ImageFs.find(context); String wineVersion = container.getWineVersion(); - if (!ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, imageFs.getRootDir(), wineVersion)) { + if (!ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, imageFs.getRootDir())) { Log.w("ImageFsInstaller", "Failed to migrate legacy directories before installation."); return Executors.newSingleThreadExecutor().submit(() -> false); } @@ -488,7 +488,7 @@ private static File resolveInstalledProtonDir(Context context, String protonVers return ContentsManager.getInstallDir(context, profile); } return new File(ImageFs.getSharedProtonDir(context), protonVersion); - } + } /** * Removes the current Proton symlink(s) from opt/ so it can be replaced with the current proton @@ -512,4 +512,22 @@ private static void removeCurrentProtonSymlink(File optDir, String activeProtonV } } } + + /** + * Migrate legacy directories if needed. After that, ensure the shared home and proton are symlinked. + */ + public static boolean ensureImageFsSymlinks(Context context, File legacyImageFsRoot, Container container) { + if (!ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot)) { + return false; + } + + String wineVersion = container.getWineVersion(); + String variant = container.getContainerVariant(); + ensureSharedHomeRoot(context, legacyImageFsRoot); + if (Container.BIONIC.equals(variant)) { + ensureProtonVersionSymlink(context, legacyImageFsRoot, wineVersion); + } + + return true; + } } diff --git a/app/src/test/java/com/winlator/xenvironment/ImageFSLegacyMigratorTest.kt b/app/src/test/java/com/winlator/xenvironment/ImageFSLegacyMigratorTest.kt index c2db9ed77b..cad2e525a0 100644 --- a/app/src/test/java/com/winlator/xenvironment/ImageFSLegacyMigratorTest.kt +++ b/app/src/test/java/com/winlator/xenvironment/ImageFSLegacyMigratorTest.kt @@ -2,12 +2,7 @@ package com.winlator.xenvironment import androidx.test.core.app.ApplicationProvider import java.io.File -import io.mockk.every -import io.mockk.just -import io.mockk.mockkStatic -import io.mockk.runs -import io.mockk.unmockkStatic -import io.mockk.verify +import java.nio.file.Files import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -29,34 +24,22 @@ class ImageFSLegacyMigratorTest { filesDir = context.filesDir legacyImageFsRoot = File(filesDir, "legacy-imagefs-test-${System.nanoTime()}").apply { mkdirs() } sharedDir = File(filesDir, "imagefs_shared").apply { deleteRecursively() } - mockkStatic(ImageFsInstaller::class) - every { ImageFsInstaller.ensureSharedHomeRoot(any(), any()) } just runs - every { ImageFsInstaller.ensureProtonVersionSymlink(any(), any(), any()) } just runs } @After fun tearDown() { - unmockkStatic(ImageFsInstaller::class) legacyImageFsRoot.deleteRecursively() sharedDir.deleteRecursively() } - private fun assertEnsureCalls(wineVersion: String) { - val context = ApplicationProvider.getApplicationContext() - verify(exactly = 1) { ImageFsInstaller.ensureSharedHomeRoot(context, legacyImageFsRoot) } - verify(exactly = 1) { - ImageFsInstaller.ensureProtonVersionSymlink(context, legacyImageFsRoot, wineVersion) - } - } - @Test fun migrateLegacyDirsIfNeeded_returnsTrueWhenLegacyHomeMissing() { val context = ApplicationProvider.getApplicationContext() - val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot, "") + val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot) assertTrue(migrated) - assertEnsureCalls("") + assertFalse(File(legacyImageFsRoot, "home").exists()) } @Test @@ -65,10 +48,10 @@ class ImageFSLegacyMigratorTest { val legacyHome = File(legacyImageFsRoot, "home").apply { mkdirs() } val legacyFile = File(legacyHome, "marker.txt").apply { writeText("legacy-content") } - val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot, "") + val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot) assertTrue(migrated) - assertEnsureCalls("") + assertFalse("Legacy home should have been moved away", legacyHome.exists()) val sharedHome = File(sharedDir, "home") assertTrue(sharedHome.exists()) assertEquals("legacy-content", File(sharedHome, legacyFile.name).readText()) @@ -83,21 +66,24 @@ class ImageFSLegacyMigratorTest { val legacyHome = File(legacyImageFsRoot, "home").apply { mkdirs() } File(legacyHome, "new.txt").writeText("new-legacy") - val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot, "") + val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot) assertTrue(migrated) - assertEnsureCalls("") assertFalse(File(sharedHome, "old.txt").exists()) assertEquals("new-legacy", File(sharedHome, "new.txt").readText()) } @Test - fun migrateLegacyDirsIfNeeded_callsEnsureMethodsWhenNoLegacyHomeExists() { + fun migrateLegacyDirsIfNeeded_succeedsWhenLegacyHomeIsSymlinkAndKeepsLegacyRoot() { val context = ApplicationProvider.getApplicationContext() - val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot, "") + val realHome = File(legacyImageFsRoot, "real-home").apply { mkdirs() } + val symlinkPath = File(legacyImageFsRoot, "home").toPath() + Files.createSymbolicLink(symlinkPath, realHome.toPath()) + + val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot) assertTrue(migrated) - assertEnsureCalls("") + assertTrue("Migrator only moves legacy directories and does not delete root", legacyImageFsRoot.exists()) } @Test @@ -106,11 +92,10 @@ class ImageFSLegacyMigratorTest { val protonDir = File(legacyImageFsRoot, "opt/proton-ge-9-2").apply { mkdirs() } val protonFile = File(protonDir, "version.txt").apply { writeText("ge-9-2") } - val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot, "proton-ge-9-2") + val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot) val sharedProtonDir = File(sharedDir, "proton/proton-ge-9-2") assertTrue(migrated) - assertEnsureCalls("proton-ge-9-2") assertFalse("Legacy proton dir should be moved away", protonDir.exists()) assertTrue("Shared proton dir should exist", sharedProtonDir.exists()) assertEquals("ge-9-2", File(sharedProtonDir, protonFile.name).readText()) @@ -122,11 +107,18 @@ class ImageFSLegacyMigratorTest { val optDir = File(legacyImageFsRoot, "opt").apply { mkdirs() } File(optDir, "wine-ge-custom").apply { mkdirs() } - val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot, "") + val protonReal = File(legacyImageFsRoot, "proton-real").apply { mkdirs() } + val protonSymlinkPath = File(optDir, "proton-symlink").toPath() + Files.createSymbolicLink(protonSymlinkPath, protonReal.toPath()) + + val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot) assertTrue(migrated) - assertEnsureCalls("") + assertTrue("Skipped opt entries remain under legacy root until installer cleanup", legacyImageFsRoot.exists()) assertTrue(File(optDir, "wine-ge-custom").exists()) + assertTrue(Files.isSymbolicLink(protonSymlinkPath)) + assertFalse(File(sharedDir, "proton/wine-ge-custom").exists()) + assertFalse(File(sharedDir, "proton/proton-symlink").exists()) } @Test @@ -138,10 +130,9 @@ class ImageFSLegacyMigratorTest { val existingShared = File(sharedDir, "proton/proton-ge-9-5").apply { mkdirs() } File(existingShared, "existing.txt").writeText("shared") - val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot, "") + val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot) assertTrue(migrated) - assertEnsureCalls("") assertFalse("Legacy proton should be removed when shared exists", legacyProton.exists()) assertEquals("shared", File(existingShared, "existing.txt").readText()) } diff --git a/app/src/test/java/com/winlator/xenvironment/ImageFsInstallerTest.kt b/app/src/test/java/com/winlator/xenvironment/ImageFsInstallerTest.kt index af3ba80637..619598a3c3 100644 --- a/app/src/test/java/com/winlator/xenvironment/ImageFsInstallerTest.kt +++ b/app/src/test/java/com/winlator/xenvironment/ImageFsInstallerTest.kt @@ -1,11 +1,13 @@ package com.winlator.xenvironment import androidx.test.core.app.ApplicationProvider +import com.winlator.container.Container import com.winlator.core.FileUtils import java.io.File import java.lang.reflect.Method import java.nio.file.Files import io.mockk.every +import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkStatic import io.mockk.verify @@ -22,10 +24,18 @@ class ImageFsInstallerTest { private val context = ApplicationProvider.getApplicationContext() private val filesDir = context.filesDir private val sharedDir = File(filesDir, "imagefs_shared") + private val imageFsLink = File(filesDir, "imagefs") + private val glibcDir = File(filesDir, "glibc") + private val bionicDir = File(filesDir, "bionic") @After fun tearDown() { sharedDir.deleteRecursively() + if (Files.isSymbolicLink(imageFsLink.toPath()) || imageFsLink.exists()) { + imageFsLink.delete() + } + glibcDir.deleteRecursively() + bionicDir.deleteRecursively() } @Test @@ -202,6 +212,22 @@ class ImageFsInstallerTest { optDir.deleteRecursively() } + @Test + fun ensureImageFsSymlinks_returnsFalseWhenLegacyMigrationFails() { + val legacyRoot = File(filesDir, "legacy-migration-fail-${System.nanoTime()}").apply { mkdirs() } + val container = mockk(relaxed = true) + + mockkStatic(ImageFSLegacyMigrator::class) + try { + every { ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(any(), any()) } returns false + + assertFalse(ImageFsInstaller.ensureImageFsSymlinks(context, legacyRoot, container)) + } finally { + unmockkStatic(ImageFSLegacyMigrator::class) + legacyRoot.deleteRecursively() + } + } + private fun invokeEnsureSharedHomeRoot(context: android.content.Context, rootDir: File) { val method: Method = ImageFsInstaller::class.java.getDeclaredMethod( "ensureSharedHomeRoot",