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
122 changes: 122 additions & 0 deletions app/src/main/java/org/randomcoder/udroid/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import org.randomcoder.udroid.runtime.DesktopEnvironment
import org.randomcoder.udroid.runtime.DesktopEnvironmentScanner
import org.randomcoder.udroid.runtime.DesktopSessionPhase
import org.randomcoder.udroid.runtime.InstalledRootfs
import org.randomcoder.udroid.runtime.ProotMountProfile
import org.randomcoder.udroid.runtime.ProotMountProfileValidator
import org.randomcoder.udroid.runtime.RuntimePhase
import org.randomcoder.udroid.runtime.RuntimeSnapshot
import org.randomcoder.udroid.runtime.RuntimeSupervisorService
Expand Down Expand Up @@ -254,6 +256,9 @@ class MainActivity : ComponentActivity() {
onResetRootfs = { rootfsName, fallback ->
resetRootfs(rootfsName, fallback)
},
onCreateRootfsVariation = { rootfsName, fallback, profile ->
createRootfsVariation(rootfsName, fallback, profile)
},
onDeleteRootfs = { deleteRootfs(it) },
onSelectDesktopEnvironment = { selectDesktopEnvironment(it) },
onCompositingChanged = { updateCompositing(it) },
Expand Down Expand Up @@ -675,6 +680,118 @@ class MainActivity : ComponentActivity() {
)
}

private fun createRootfsVariation(
rootfsName: String,
fallbackDistro: DistroVariant?,
profile: ProotMountProfile,
) {
if (installProgress != null) {
rootfsMaintenanceMessage = "Finish the current Linux setup before creating another distro"
return
}
if (installedRootfses.none { it.name == rootfsName }) {
rootfsMaintenanceMessage = "Linux system $rootfsName is no longer installed"
return
}

val previousWork =
runCatching { app.rootfsInstallSources.load(rootfsName) }
.getOrNull()
?: fallbackDistro
?.takeIf { it.internalName == rootfsName }
?.let {
InstallerWorkRequest.Archive(
distro = it,
operationId = UUID.randomUUID().toString(),
)
}
if (previousWork == null) {
rootfsMaintenanceMessage =
"The original image source was not recorded for this legacy install. " +
"Install it again before creating a variation."
return
}

val variationProfile =
runCatching {
ProotMountProfileValidator.requireValid(
profile.copy(sourceSystemId = profile.sourceSystemId ?: rootfsName),
)
}.getOrElse {
rootfsMaintenanceMessage = it.message ?: "The mount configuration is invalid"
return
}
val (installationName, _) = nextVariationIdentity(previousWork)
val variationDisplayName = "${previousWork.displayName} · ${variationProfile.name}"
val variationWork =
when (previousWork) {
is InstallerWorkRequest.Archive ->
previousWork.copy(
operationId = UUID.randomUUID().toString(),
installationName = installationName,
displayName = variationDisplayName,
)
is InstallerWorkRequest.Oci ->
previousWork.copy(
operationId = UUID.randomUUID().toString(),
installationName = installationName,
displayName = variationDisplayName,
)
}

lifecycleScope.launch {
val prepared =
runCatching {
withContext(Dispatchers.IO) {
app.mountProfiles.save(
installationName,
variationProfile.independentCopy(),
)
}
app.installState.save(InstallationSelection.initial(variationWork))
}
prepared.fold(
onSuccess = { progress ->
installProgress = progress
showInstallTerminal = false
rootfsMaintenanceMessage = null
selectDestination(UdroidDestination.INSTALL)
},
onFailure = {
runCatching {
withContext(Dispatchers.IO) {
app.mountProfiles.remove(installationName)
}
}
rootfsMaintenanceMessage =
it.message ?: "The Linux variation could not be prepared"
},
)
}
}

private fun nextVariationIdentity(work: InstallerWorkRequest): Pair<String, Int> {
val baseName =
when (work) {
is InstallerWorkRequest.Archive -> work.distro.internalName
is InstallerWorkRequest.Oci -> work.installationName.replace(VARIATION_SUFFIX, "")
}
val occupiedNames =
installedRootfses.mapTo(mutableSetOf(), InstalledRootfs::name).apply {
installProgress?.installationName?.let(::add)
}
for (number in 2..999) {
val suffix = "-v$number"
val prefix =
baseName
.take(MAX_INSTALLATION_NAME_LENGTH - suffix.length)
.trimEnd('.', '-', '_')
val candidate = "$prefix$suffix"
if (candidate !in occupiedNames) return candidate to number
}
error("No available variation name for $baseName")
}

private fun maintainRootfs(
rootfsName: String,
resetWork: InstallProgress?,
Expand Down Expand Up @@ -724,6 +841,9 @@ class MainActivity : ComponentActivity() {
?.let { cleanupWarnings += it.message ?: "launcher shortcuts" }

if (resetWork == null) {
runCatching { app.mountProfiles.remove(rootfsName) }
.exceptionOrNull()
?.let { cleanupWarnings += it.message ?: "mount profile" }
runCatching { app.rootfsInstallSources.remove(rootfsName) }
.exceptionOrNull()
?.let { cleanupWarnings += it.message ?: "install source" }
Expand Down Expand Up @@ -1264,5 +1384,7 @@ class MainActivity : ComponentActivity() {
private companion object {
const val NOTIFICATION_PERMISSION_REQUEST = 101
const val STATE_OCI_REPOSITORY = "oci-repository"
const val MAX_INSTALLATION_NAME_LENGTH = 96
val VARIATION_SUFFIX = Regex("-v[2-9][0-9]*$")
}
}
5 changes: 5 additions & 0 deletions app/src/main/java/org/randomcoder/udroid/UdroidApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.randomcoder.udroid.install.InstallStateStore
import org.randomcoder.udroid.install.InstalledRootfsSourceStore
import org.randomcoder.udroid.runtime.EventJournal
import org.randomcoder.udroid.runtime.InstalledRootfsRegistry
import org.randomcoder.udroid.runtime.ProotMountProfileStore
import org.randomcoder.udroid.runtime.RuntimeStateMachine
import org.randomcoder.udroid.runtime.RuntimeStateStore
import org.randomcoder.udroid.update.AppUpdateScheduler
Expand All @@ -32,6 +33,9 @@ class UdroidApplication : Application() {
lateinit var rootfsRegistry: InstalledRootfsRegistry
private set

lateinit var mountProfiles: ProotMountProfileStore
private set

override fun onCreate() {
super.onCreate()
runtimeState = RuntimeStateStore(this)
Expand All @@ -40,6 +44,7 @@ class UdroidApplication : Application() {
rootfsInstallSources = InstalledRootfsSourceStore(this)
updateState = AppUpdateStateStore(this)
rootfsRegistry = InstalledRootfsRegistry(this)
mountProfiles = ProotMountProfileStore(this)
if (!isMainProcess()) return

updateState.reconcileInstalledVersion(BuildConfig.VERSION_NAME)
Expand Down
36 changes: 25 additions & 11 deletions app/src/main/java/org/randomcoder/udroid/install/InstallProgress.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,35 @@ data class InstallProgress(

object InstallationSelection {
fun initial(distro: DistroVariant): InstallProgress =
initial(
InstallerWorkRequest.Archive(
distro = distro,
operationId = UUID.randomUUID().toString(),
),
)

fun initial(work: InstallerWorkRequest): InstallProgress =
InstallProgress(
work =
InstallerWorkRequest.Archive(
distro = distro,
operationId = UUID.randomUUID().toString(),
),
work = work,
stage = InstallStage.READY,
stageProgress = 0f,
currentDetail = "SHA-256 metadata is available for ${distro.architecture}",
currentDetail = "Configure this distro, then start its image download",
terminalLines =
listOf(
"\$ udroid pull --plan ${distro.id}",
"[ready] ${distro.downloadUrl.substringAfterLast('/')}",
"[ready] sha256 ${distro.sha256.take(16)}…",
),
when (work) {
is InstallerWorkRequest.Archive ->
listOf(
"\$ udroid pull --plan ${work.distro.id}",
"[ready] ${work.distro.downloadUrl.substringAfterLast('/')}",
"[ready] install as ${work.installationName}",
"[ready] sha256 ${work.distro.sha256.take(16)}…",
)
is InstallerWorkRequest.Oci ->
listOf(
"\$ udroid pull --plan ${work.reference}",
"[ready] install as ${work.installationName}",
"[ready] platform ${work.platform.os}/${work.platform.architecture}",
)
},
previewOnly = false,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class InstallerService : Service() {
val distro = work.distro
val operationId = work.operationId
val rootfsDirectory = File(filesDir, "rootfs")
val installedRootfs = File(rootfsDirectory, distro.internalName)
val installedRootfs = File(rootfsDirectory, work.installationName)
if (File(installedRootfs, RootfsInstallationPipeline.READY_MARKER).isFile) {
publishCompleted(work, installedRootfs, reused = true)
finishOperation(operation)
Expand Down Expand Up @@ -444,7 +444,7 @@ class InstallerService : Service() {
val operationId = work.operationId
RootfsInstallationPipeline.clearInterruptedInstallation(
rootfsDirectory = rootfsDirectory,
installationName = distro.internalName,
installationName = work.installationName,
)
RootfsStoragePreflight.requireSpace(archive, rootfsDirectory)
progressPublisher.configure(
Expand Down Expand Up @@ -479,7 +479,7 @@ class InstallerService : Service() {
RootfsInstallRequest(
archive = archive,
rootfsDirectory = rootfsDirectory,
installationName = distro.internalName,
installationName = work.installationName,
operationId = operationId,
),
onExtractionProgress = progressPublisher::extract,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ sealed interface InstallerWorkRequest {
data class Archive(
val distro: DistroVariant,
override val operationId: String,
override val installationName: String = distro.internalName,
override val displayName: String = distro.releaseName,
) : InstallerWorkRequest {
override val installationName: String = distro.internalName
override val displayName: String = distro.releaseName
override val architecture: String = distro.architecture
}

Expand Down Expand Up @@ -55,6 +55,7 @@ object InstallerWorkRequestCodec {
when (request) {
is InstallerWorkRequest.Archive -> {
put("source", SOURCE_ARCHIVE)
put("source_internal_name", request.distro.internalName)
put("suite", request.distro.suite)
put("variant", request.distro.variant)
put("friendly_name", request.distro.friendlyName)
Expand Down Expand Up @@ -112,7 +113,11 @@ object InstallerWorkRequestCodec {
DistroVariant(
suite = value.requiredString("suite"),
variant = value.requiredString("variant"),
internalName = installationName,
internalName =
value["source_internal_name"]
?.jsonPrimitive
?.contentOrNull
?: installationName,
friendlyName = value.requiredString("friendly_name"),
architecture = architecture,
downloadUrl = value.requiredString("download_url"),
Expand Down Expand Up @@ -143,6 +148,8 @@ object InstallerWorkRequestCodec {
?.intOrNull
?: 0,
),
installationName = installationName,
displayName = displayName,
)

SOURCE_OCI ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class ProotApplicationLaunch(
val command: List<String>,
val workingDirectory: File,
val environment: Map<String, String>,
val mounts: List<ResolvedProotMount>,
)

object ProotApplicationLaunchBuilder {
Expand All @@ -28,6 +29,15 @@ object ProotApplicationLaunchBuilder {
application.workingDirectory
.takeIf { guestPathExists(rootfs, it, directory = true) }
?: guestHome
val mounts =
ProotMountResolver.resolve(
profile = ProotMountProfileStore(context).load(rootfs.name),
sessionMounts =
ProotMountResolver.sessionMounts(
x11SocketDirectory = x11SocketDirectory.absolutePath,
audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath,
),
)
val prootArguments =
buildArguments(
prootPath = runtime.executable.absolutePath,
Expand All @@ -38,6 +48,7 @@ object ProotApplicationLaunchBuilder {
applicationArguments =
listOf(application.executable) + application.arguments,
audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath,
mounts = mounts,
)
val temporaryDirectory =
File(context.cacheDir, "proot").apply {
Expand All @@ -62,6 +73,7 @@ object ProotApplicationLaunchBuilder {
val separator = it.indexOf('=')
it.substring(0, separator) to it.substring(separator + 1)
},
mounts = mounts,
)
}

Expand All @@ -73,6 +85,8 @@ object ProotApplicationLaunchBuilder {
guestWorkingDirectory: String,
applicationArguments: List<String>,
audioAuthDirectory: String? = null,
mounts: List<ResolvedProotMount> =
ProotMountResolver.defaults(x11SocketDirectory, audioAuthDirectory),
): List<String> {
require(applicationArguments.isNotEmpty())
return buildList {
Expand All @@ -81,13 +95,7 @@ object ProotApplicationLaunchBuilder {
add("--kill-on-exit")
add("--root-id")
add("--rootfs=$rootfsPath")
addAndroidProotBindMounts()
add("-b")
add("$x11SocketDirectory:/tmp/.X11-unix")
if (audioAuthDirectory != null) {
add("-b")
add("$audioAuthDirectory:${AudioEndpoint.GUEST_AUTH_DIRECTORY}")
}
addProotBindMounts(mounts)
add("--cwd=$guestWorkingDirectory")
add("/usr/bin/env")
add("-i")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package org.randomcoder.udroid.runtime

internal val ANDROID_PROOT_BIND_MOUNTS =
listOf(
"/system",
"/apex",
"/dev",
"/proc",
"/sys",
"/linkerconfig/ld.config.txt",
)
PROOT_DEFAULT_MOUNTS.map(ProotDefaultMount::hostSource)

internal fun MutableList<String>.addAndroidProotBindMounts() {
ANDROID_PROOT_BIND_MOUNTS.forEach { path ->
internal fun MutableList<String>.addProotBindMounts(mounts: List<ResolvedProotMount>) {
mounts.forEach { mount ->
add("-b")
add(path)
add(mount.argument)
}
}

internal fun MutableList<String>.addAndroidProotBindMounts() =
addProotBindMounts(ProotMountResolver.defaults())
Loading