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
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,16 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
Log.d(TAG, "Cmd: " + sanitizeCommand(cmd))
}

currentProcess = ProcessBuilder(cmd).apply {
directory(context.filesDir)
environment().putAll(env)
}.start()
try {
currentProcess = ProcessBuilder(cmd).apply {
directory(context.filesDir)
environment().putAll(env)
}.start()
} catch (e: Exception) {
outputHandler(OUTPUT_STDERR, "> CRITICAL ERROR: Android OS blocked the Linux environment from starting!")
outputHandler(OUTPUT_STDERR, "> Details: ${e.message}")
return 255
}

val stdoutThread = logAndCaptureStream(BufferedReader(InputStreamReader(currentProcess?.inputStream))) { line ->
Log.i(STDOUT_TAG, line)
Expand All @@ -157,13 +163,23 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,

val exitCode = currentProcess?.waitFor() ?: 255
Log.i(TAG, "ExitCode: $exitCode")

if (exitCode != 0) {
outputHandler(OUTPUT_STDERR, "> Linux Environment crashed instantly with Exit Code: $exitCode")
}

currentProcess = null
return exitCode
}

private fun setupProject(projectPath: String, gradleBuildDir: String, outputHandler: (Int, String) -> Unit): File {

private fun setupProject(
projectPath: String,
gradleBuildDir: String,
isPackagingPhase: Boolean,
outputHandler: (Int, String) -> Unit
): File {
val workDir = Utils.getProjectCacheDir(context, projectPath, gradleBuildDir)
val actionText = if (isPackagingPhase) "Packaging" else "Importing"

if (BuildConfig.FLAVOR == "picoos" || BuildConfig.FLAVOR == "horizonos") {
val sourceDir = File(projectPath, gradleBuildDir)
Expand All @@ -178,7 +194,7 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
workDir.mkdirs()
}

outputHandler(OUTPUT_INFO, "> Importing project files...")
outputHandler(OUTPUT_INFO, "> $actionText project files...")
if (!FileUtils.tryCopyDirectory(sourceDir, workDir)) {
throw IOException("Failed to copy $sourceDir to $workDir")
}
Expand Down Expand Up @@ -233,14 +249,29 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
ProjectInfo.writeToDirectory(context, workDir, projectPath, gradleBuildDir, projectTreeUri)
}

outputHandler(OUTPUT_INFO, "> Importing project files...")
FileUtils.importAndroidProject(context, projectTreeUri, gradleBuildDir, workDir)
outputHandler(OUTPUT_INFO, "> $actionText project files...")

// Dynamically intercept the strings coming from FileUtils and replace them if we are packaging!
FileUtils.importAndroidProject(context, projectTreeUri, gradleBuildDir, workDir) { fileName ->
val finalMsg = if (isPackagingPhase) fileName.replace("Importing", "Packaging") else fileName
outputHandler(OUTPUT_INFO, finalMsg)
}

val gradlewFile = File(workDir, "gradlew")
if (gradlewFile.exists()) {
gradlewFile.setExecutable(true, false)
}

val finishText = if (isPackagingPhase) "Packaged" else "Imported"
outputHandler(OUTPUT_INFO, "> Project $finishText!")
return workDir
}

private fun fixGradleArgs(projectPath: String, rawGradleArgs: List<String>): List<String> {
val normalizedProjectPath = projectPath.trimEnd('/')
return rawGradleArgs.map { arg ->
val verboseArgs = rawGradleArgs.toMutableList()

return verboseArgs.map { arg ->
when {
arg.startsWith("-Pdebug_keystore_file=") -> "-Pdebug_keystore_file=/project/.android/debug.keystore"
arg.startsWith("-Prelease_keystore_file=") -> "-Prelease_keystore_file=/project/.android/release.keystore"
Expand All @@ -249,11 +280,17 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
arg.startsWith("-Pplugins_local_binaries=") -> {
val prefix = "-Pplugins_local_binaries="
val value = arg.removePrefix(prefix)
val updated = value.replace(
"$normalizedProjectPath/${FileUtils.ADDONS_DIR_NAME}",
"/project/${FileUtils.ADDONS_DIR_NAME}"
)
prefix + updated

// Godot sends a comma-separated list of absolute Android OS paths.
// Since FileUtils.kt now dumps all V1 plugins directly into the PRoot workspace,
// we just isolate the file name and point Gradle to /project/FileName.aar
val updatedPaths = value.split(",").map { rawPath ->
val cleanPath = rawPath.trim().removeSurrounding("\"").removeSurrounding("'")
val fileName = File(cleanPath).name
"/project/$fileName"
}.joinToString(",")

prefix + updatedPaths
}

else -> arg
Expand Down Expand Up @@ -286,10 +323,19 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,

rootfs.mkdirs()

val progressCallback: (Int, Long) -> Unit = { count, elapsedMs ->
val totalSeconds = (elapsedMs / 1000).toInt()
val minutes = totalSeconds / 60
val seconds = totalSeconds % 60
val timeString = String.format("%02d:%02d", minutes, seconds)

outputHandler(99, "Extracted: $count files ($timeString)")
}

val version: String
if (localUri != null) {
outputHandler(OUTPUT_INFO, "> Extracting rootfs from local file...")
TarXzExtractor.extractLocalTarXz(context, localUri, rootfs)
TarXzExtractor.extractLocalTarXz(context, localUri, rootfs, progressCallback)
version = ROOTFS_VERSION_CUSTOM
} else {
val hasAsset = try {
Expand All @@ -299,7 +345,7 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
}
if (hasAsset) {
outputHandler(OUTPUT_INFO, "> Extracting rootfs from assets...")
TarXzExtractor.extractAssetTarXz(context, ROOTFS_ASSET_PATH, rootfs)
TarXzExtractor.extractAssetTarXz(context, ROOTFS_ASSET_PATH, rootfs, progressCallback)
version = ROOTFS_VERSION_CUSTOM
} else {
val tempFile = File(context.cacheDir, ROOTFS_FILENAME)
Expand All @@ -314,7 +360,7 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
version = releaseTag

outputHandler(OUTPUT_INFO, "> Extracting rootfs...")
TarXzExtractor.extractFileTarXz(tempFile, rootfs)
TarXzExtractor.extractFileTarXz(tempFile, rootfs, progressCallback)
} finally {
if (tempFile.exists()) {
tempFile.delete()
Expand Down Expand Up @@ -443,8 +489,11 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
return 255
}

// Check if Godot is running the final build/packaging tasks
val isPackagingPhase = !rawGradleArgs.any { it.contains("assemble") || it.contains("bundle") }

val workDir = try {
setupProject(projectPath, gradleBuildDir, outputHandler)
setupProject(projectPath, gradleBuildDir, isPackagingPhase, outputHandler)
} catch (e: Exception) {
outputHandler(OUTPUT_STDERR, "Unable to setup project: ${e.message}")
return 255
Expand All @@ -459,15 +508,28 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
}
outputHandler(type, line)
}

val baseArgs = if (BuildConfig.FLAVOR == "picoos" || BuildConfig.FLAVOR == "horizonos") {
rawGradleArgs
} else {
fixGradleArgs(projectPath, rawGradleArgs)
}

val gradleArgs = baseArgs.toMutableList()
// Let the Linux shell dynamically find the exact path to AAPT2 (e.g., /opt/sdk/build-tools/36.1.0/aapt2)
// Since the gradleCmd string uses double quotes, bash will evaluate $(which aapt2) perfectly!
gradleArgs.add("-Pandroid.aapt2FromMavenOverride=\$(which aapt2)")

var result = executeGradleInternal(gradleArgs, workDir, captureOutputHandler)

val gradleArgs = if (BuildConfig.FLAVOR == "picoos" || BuildConfig.FLAVOR == "horizonos") {

/**val gradleArgs = if (BuildConfig.FLAVOR == "picoos" || BuildConfig.FLAVOR == "horizonos") {
// GABE has full storage access on XR devices, so we are not pulling addons dir, or keystore files.
rawGradleArgs
} else {
fixGradleArgs(projectPath, rawGradleArgs)
}

var result = executeGradleInternal(gradleArgs, workDir, captureOutputHandler)
var result = executeGradleInternal(gradleArgs, workDir, captureOutputHandler)**/

val stderr = stderrBuilder.toString()
if (result == 0 && stderr.contains("BUILD FAILED")) {
Expand All @@ -493,8 +555,8 @@ class BuildEnvironment(private val context: Context, private val rootfs: String,
// Now, try running Gradle again!
outputHandler(OUTPUT_INFO, "> Retrying Gradle build...")
result = executeGradleInternal(gradleArgs, workDir, captureOutputHandler)
val stderr = stderrBuilder.toString()
if (result == 0 && stderr.contains("BUILD FAILED")) {
val stderr2 = stderrBuilder.toString()
if (result == 0 && stderr2.contains("BUILD FAILED")) {
result = 1
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class BuildEnvironmentService : Service() {
const val MSG_INSTALL_ROOTFS = 7
const val MSG_DELETE_ROOTFS = 8
const val MSG_BUILD_DIR_ACCESS_GRANTED = 9

// NEW: Constant for live timer updates sent to the UI
const val MSG_EXTRACTION_PROGRESS = 99

const val EXTRA_LOCAL_ROOTFS_URI = "local_rootfs_uri"
}
Expand Down Expand Up @@ -201,15 +204,31 @@ class BuildEnvironmentService : Service() {

try {
mBuildEnvironment.installRootfs(localUri) { type, line ->
val outputMsg = Message.obtain(null, MSG_COMMAND_OUTPUT, id, type)
val outputData = Bundle()
outputData.putString("line", line)
outputMsg.data = outputData

try {
msg.replyTo.send(outputMsg)
} catch (e: RemoteException) {
Log.e(TAG, "Error sending output to client: ${e.message}")

// NEW: If type is 99, route it to the UI's progress timer state
if (type == MSG_EXTRACTION_PROGRESS) {
val progressMsg = Message.obtain(null, MSG_EXTRACTION_PROGRESS)
val bundle = Bundle()
bundle.putString("timerText", line)
progressMsg.data = bundle

try {
msg.replyTo.send(progressMsg)
} catch (e: RemoteException) {
Log.e(TAG, "Error sending progress to client: ${e.message}")
}
} else {
// Standard command output routing
val outputMsg = Message.obtain(null, MSG_COMMAND_OUTPUT, id, type)
val outputData = Bundle()
outputData.putString("line", line)
outputMsg.data = outputData

try {
msg.replyTo.send(outputMsg)
} catch (e: RemoteException) {
Log.e(TAG, "Error sending output to client: ${e.message}")
}
}
}
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ object FileUtils {
return String.format("%.1f %s", value, units[unitIndex])
}

fun importAndroidProject(context: Context, projectTreeUri: Uri, gradleBuildDir: String, destDir: File) {
fun importAndroidProject(
context: Context,
projectTreeUri: Uri,
gradleBuildDir: String,
destDir: File,
onProgress: (String) -> Unit
) {
val root = DocumentFile.fromTreeUri(context, projectTreeUri)
?: throw IOException("Invalid tree uri")

Expand All @@ -103,6 +109,8 @@ object FileUtils {
it.isDirectory && it.name == ADDONS_DIR_NAME
}

val pluginsDir = findDirByPath(root, "android/plugins")

if (destDir.exists()) {
val apkAssetsDir = File(destDir, "src/main/assets")
if (apkAssetsDir.exists()) apkAssetsDir.deleteRecursively()
Expand All @@ -113,18 +121,36 @@ object FileUtils {
destDir.mkdirs()
}

copyDirectoryMerge(context, gradleDir, destDir)
onProgress("> Importing Gradle build directory...")
copyDirectoryMerge(context, gradleDir, destDir, onProgress)

if (addonsDir != null) {
val localAddons = File(destDir, ADDONS_DIR_NAME)
if (localAddons.exists()) {
localAddons.deleteRecursively()
}
localAddons.mkdirs()
copyDirectoryMerge(context, addonsDir, localAddons)
onProgress("> Importing Addons directory...")
copyDirectoryMerge(context, addonsDir, localAddons, onProgress)
}

if (pluginsDir != null) {
// Force V1 plugins to copy directly into the project root directory!
onProgress("> Importing v1 Plugins directory...")
copyDirectoryMerge(context, pluginsDir, destDir, onProgress)
}
}

/*if (pluginsDir != null) {
val localPlugins = File(destDir.parentFile, "plugins")
if (localPlugins.exists()) {
localPlugins.deleteRecursively()
}
localPlugins.mkdirs()
onProgress("> Importing v1 Plugins directory...")
copyDirectoryMerge(context, pluginsDir, localPlugins, onProgress)
}*/

private fun findDirByPath(parent: DocumentFile, relativePath: String): DocumentFile? {
var current: DocumentFile? = parent

Expand All @@ -139,16 +165,34 @@ object FileUtils {
return current
}

private fun copyDirectoryMerge(context: Context, src: DocumentFile, dest: File) {
private fun copyDirectoryMerge(
context: Context,
src: DocumentFile,
dest: File,
onProgress: (String) -> Unit
) {
src.listFiles().forEach { file ->
val name = file.name ?: return@forEach

if (file.isDirectory) {
val newDir = File(dest, name)
if (!newDir.exists()) newDir.mkdirs()
copyDirectoryMerge(context, file, newDir)

// Log directory milestones instead of every internal file
onProgress("Importing Directory: $name")
copyDirectoryMerge(context, file, newDir, onProgress)
} else {
val outFile = File(dest, name)

// Only log high-value plugin binaries, configuration, or build files to the UI
if (name.endsWith(".aar", true) ||
name.endsWith(".jar", true) ||
name.endsWith(".gdap", true) ||
name.endsWith(".gradle", true) ||
name == "AndroidManifest.xml") {
onProgress("Importing Plugin/Config: $name")
}

context.contentResolver.openInputStream(file.uri).use { input ->
FileOutputStream(outFile, false).use { output ->
input?.copyTo(output)
Expand Down
Loading
Loading