Skip to content
Merged
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: 5 additions & 3 deletions build-android-assets.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/bin/bash

# 安装依赖
yarn install --frozen-lockfile --network-timeout=300000

Expand Down Expand Up @@ -46,6 +48,6 @@ rm -rf assets/templates/WebGAL_Android_Template/app/src/main/java/com
cd ../

# 压缩文件
rm -rf packages/terre-android/app/src/main/assets/
mkdir packages/terre-android/app/src/main/assets/
tar -cvf packages/terre-android/app/src/main/assets/terre.tar -C release .
mkdir -p packages/terre-android/app/src/main/assets/
rm -f packages/terre-android/app/src/main/assets/terre.tar.xz
tar -cJf packages/terre-android/app/src/main/assets/terre.tar.xz -C release .
4 changes: 1 addition & 3 deletions packages/terre-android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@
.cxx
.kotlin
local.properties
app/libnode/bin
app/libnode/include
app/libnode/nodejs-mobile-*
app/src/main/assets/*
app/src/main/jniLibs

# key
key.properties
Expand Down
4 changes: 1 addition & 3 deletions packages/terre-android/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# WebGAL Terre Android

Running with [nodejs-mobile](https://github.com/nodejs-mobile/nodejs-mobile)

## Building

Open `packages/terre-android/` folder on Android studio, select `build` -> `Generate Signed Bundle or APK` -> `APK`, create a `keystore.jks` file on `packages/terre-android/` folder.
Expand All @@ -21,4 +19,4 @@ Open a Shell in the project root directory and run:

``` shell
sh release-android.sh
```
```
92 changes: 2 additions & 90 deletions packages/terre-android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import groovy.json.JsonSlurper
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.net.URI
import java.nio.file.Files
import java.security.MessageDigest
import java.util.Properties
import java.util.zip.ZipInputStream

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
Expand Down Expand Up @@ -45,16 +40,6 @@ android {
versionName = getVersionFromPackageJson()

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

externalNativeBuild {
cmake {
cppFlags("")
arguments("-DANDROID_STL=c++_shared")
}
}
ndk {
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86_64"))
}
}
signingConfigs {
if (keystorePropertiesFile.exists())
Expand Down Expand Up @@ -90,17 +75,6 @@ android {
buildFeatures {
compose = true
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
sourceSets {
getByName("main") {
jniLibs.srcDirs("libnode/bin/")
}
}
packaging {
jniLibs {
useLegacyPackaging = true
Expand All @@ -121,6 +95,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.material.icons.extended)
implementation(libs.commons.compress)
implementation(libs.xz)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand All @@ -130,67 +105,4 @@ dependencies {
debugImplementation(libs.androidx.ui.test.manifest)
}

abstract class DownloadNodejsTask : DefaultTask() {
@TaskAction
fun run() {
val url =
"https://github.com/nodejs-mobile/nodejs-mobile/releases/download/v18.20.4/nodejs-mobile-v18.20.4-android.zip"
val expectedMD5 = "4fe60de25381937b03642404513ec26b"
val zipFile = project.file("./libnode/nodejs-mobile-v18.20.4-android.zip")
val extractDir = project.file("./libnode")

if (zipFile.exists()) {
val calculatedMD5 = MessageDigest.getInstance("MD5")
.digest(Files.readAllBytes(zipFile.toPath()))
.joinToString("") { "%02x".format(it) }

if (calculatedMD5 != expectedMD5) {
zipFile.delete()
println("MD5 mismatch. File deleted: $zipFile")
}
}

if (!zipFile.exists()) {
zipFile.parentFile.mkdirs()
println("Downloading Node.js from: $url")
zipFile.outputStream().use { os ->
URI.create(url).toURL().openStream().use { input ->
input.copyTo(os)
}
}

val calculatedMD5 = MessageDigest.getInstance("MD5")
.digest(Files.readAllBytes(zipFile.toPath()))
.joinToString("") { "%02x".format(it) }

if (calculatedMD5 != expectedMD5) {
throw GradleException("MD5 verification failed for $zipFile")
}

println("Extracting Node.js to: $extractDir")
extractDir.mkdirs()
ZipInputStream(zipFile.inputStream()).use { zis ->
var entry = zis.nextEntry
while (entry != null) {
val targetFile = File(extractDir, entry.name)
if (entry.isDirectory) {
targetFile.mkdirs()
} else {
targetFile.parentFile.mkdirs()
targetFile.outputStream().use { fos ->
zis.copyTo(fos)
}
}
zis.closeEntry()
entry = zis.nextEntry
}
}
}
}
}

tasks.register<DownloadNodejsTask>("downloadNodejs")

tasks.named("preBuild") {
dependsOn("downloadNodejs")
}
apply(from = "./download-nodejs.gradle.kts")
171 changes: 171 additions & 0 deletions packages/terre-android/app/download-nodejs.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import java.security.MessageDigest
import java.net.URI
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.archivers.tar.TarArchiveEntry

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.apache.commons:commons-compress:1.28.0")
classpath("org.tukaani:xz:1.12")
}
}

abstract class DownloadNodejsTask : DefaultTask() {

private val targetArchitectures = listOf("aarch64", "arm", "x86_64")
private val architectureMap = mapOf(
"aarch64" to "arm64-v8a",
"arm" to "armeabi-v7a",
"x86_64" to "x86_64",
)

@TaskAction
fun run() {
val baseUrl = "https://github.com/nini22P/termux-nodejs/releases/latest/download"

val libnodeRoot = project.file("src/main/jniLibs")
val assetsDir = project.file("src/main/assets")

if (!assetsDir.exists()) assetsDir.mkdirs()

val sha256Content = try {
fetch("$baseUrl/sha256.txt")
} catch (e: Exception) {
val allExist = targetArchitectures.all { archKey ->
val abi = architectureMap[archKey]!!
File(libnodeRoot, "$abi/libnode.so").exists()
}
if (allExist) {
println("Offline mode: Node.js binaries already exist, skipping download.")
return
}
throw GradleException("Failed to fetch SHA256 and local binaries are missing: ${e.message}", e)
}
val expectedHashes = parseSha256Content(sha256Content, targetArchitectures)

for (archKey in targetArchitectures) {
val fileName = expectedHashes.keys.find { it.contains(archKey) } ?: continue
val expectedHash = expectedHashes[fileName]!!
val tarFile = File(assetsDir, fileName)

val abi = architectureMap[archKey]!!
val archDir = File(libnodeRoot, abi)

val isTarInvalid = !tarFile.exists() || calculateSha256(tarFile) != expectedHash
if (isTarInvalid) {
downloadFile("$baseUrl/$fileName", tarFile)
if (calculateSha256(tarFile) != expectedHash) throw GradleException("SHA256 failed: $fileName")
}

extractBin(tarFile, archDir)
}
}

private fun extractBin(tarFile: File, archDir: File) {
val binPrefix = "./data/data/com.termux/files/usr/bin/"

val inputStream = tarFile.inputStream().buffered()

val decompressed = when {
tarFile.name.endsWith(".xz") ->
org.apache.commons.compress.compressors.xz.XZCompressorInputStream(inputStream)
tarFile.name.endsWith(".gz") ->
java.util.zip.GZIPInputStream(inputStream)
else -> inputStream
}

decompressed.use { comp ->
TarArchiveInputStream(comp).use { tis ->
var entry: TarArchiveEntry? = tis.nextEntry
while (entry != null) {
val name = entry.name
if (name.startsWith(binPrefix)
&& !entry.isDirectory
&& !entry.isSymbolicLink
) {
val fileName = name.substringAfterLast("/")
val targetFile = File(archDir, "lib$fileName.so")

val bytes = tis.readBytes()

val newHash = calculateSha256(bytes)

if (targetFile.exists()) {
val oldHash = calculateSha256(targetFile)

if (oldHash == newHash) {
entry = tis.nextEntry
continue
}
}

archDir.mkdirs()
targetFile.writeBytes(bytes)

println("Extracted: ${tarFile} -> $targetFile")
}

entry = tis.nextEntry
}
}
}
}

private fun fetch(url: String): String {
println("Fetching: $url")
val connection = URI.create(url).toURL().openConnection()
connection.connectTimeout = 15000
connection.readTimeout = 15000
return connection.getInputStream().bufferedReader().use { it.readText() }
}

private fun downloadFile(url: String, target: File) {
println("Downloading: $url")
val connection = URI.create(url).toURL().openConnection()
connection.connectTimeout = 15000
connection.readTimeout = 15000
connection.getInputStream().use { input ->
target.outputStream().use { output -> input.copyTo(output) }
}
}
Comment on lines +117 to +133

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

使用 URI.create(url).toURL().openStream() 进行网络请求和文件下载时,没有设置连接超时(Connect Timeout)和读取超时(Read Timeout)。如果网络连接不稳定或对端无响应,可能会导致 Gradle 构建无限期挂起。建议使用 URLConnection 并显式设置合理的超时时间。

    private fun fetch(url: String): String {
        println("Fetching: $url")
        val connection = URI.create(url).toURL().openConnection()
        connection.connectTimeout = 15000
        connection.readTimeout = 15000
        return connection.getInputStream().bufferedReader().use { it.readText() }
    }

    private fun downloadFile(url: String, target: File) {
        println("Downloading: $url")
        val connection = URI.create(url).toURL().openConnection()
        connection.connectTimeout = 15000
        connection.readTimeout = 15000
        connection.getInputStream().use { input ->
            target.outputStream().use { output -> input.copyTo(output) }
        }
    }


private fun parseSha256Content(content: String, architectures: List<String>): Map<String, String> {
val hashes = mutableMapOf<String, String>()
content.lines().forEach { line ->
val parts = line.trim().split(Regex("\\s+"))
if (parts.size >= 2) {
val hash = parts[0]
val name = parts[1].removePrefix("*")
if (architectures.any { name.contains(it) }) hashes[name] = hash
}
}
return hashes
}

private fun calculateSha256(file: File): String {
val digest = MessageDigest.getInstance("SHA-256")
file.inputStream().use { fis ->
val buffer = ByteArray(8192)
var bytesRead = fis.read(buffer)
while (bytesRead != -1) {
digest.update(buffer, 0, bytesRead)
bytesRead = fis.read(buffer)
}
}
return digest.digest().joinToString("") { "%02x".format(it) }
}

private fun calculateSha256(data: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256")
return digest.digest(data).joinToString("") { "%02x".format(it) }
}
}

tasks.register<DownloadNodejsTask>("downloadNodejs")

tasks.named("preBuild") {
dependsOn("downloadNodejs")
}
50 changes: 0 additions & 50 deletions packages/terre-android/app/src/main/cpp/CMakeLists.txt

This file was deleted.

Loading
Loading