Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/app/gamenative/ui/PluviaMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ private static void installGuestLibs(Context ctx) {
public static Future<Boolean> installIfNeededFuture(final Context context, AssetManager assetManager, Container container, Callback<Integer> 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);
}
Expand Down Expand Up @@ -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
Expand All @@ -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;
Comment thread
unbelievableflavour marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<android.content.Context>()
verify(exactly = 1) { ImageFsInstaller.ensureSharedHomeRoot(context, legacyImageFsRoot) }
verify(exactly = 1) {
ImageFsInstaller.ensureProtonVersionSymlink(context, legacyImageFsRoot, wineVersion)
}
}

@Test
fun migrateLegacyDirsIfNeeded_returnsTrueWhenLegacyHomeMissing() {
val context = ApplicationProvider.getApplicationContext<android.content.Context>()

val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot, "")
val migrated = ImageFSLegacyMigrator.migrateLegacyDirsIfNeeded(context, legacyImageFsRoot)

assertTrue(migrated)
assertEnsureCalls("")
assertFalse(File(legacyImageFsRoot, "home").exists())
}

@Test
Expand All @@ -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())
Expand All @@ -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<android.content.Context>()
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
Expand All @@ -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())
Expand All @@ -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
Expand All @@ -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())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,10 +24,18 @@ class ImageFsInstallerTest {
private val context = ApplicationProvider.getApplicationContext<android.content.Context>()
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()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
glibcDir.deleteRecursively()
bionicDir.deleteRecursively()
}

@Test
Expand Down Expand Up @@ -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<Container>(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",
Expand Down
Loading