diff --git a/.gitignore b/.gitignore index 0b3a39ed..5f7ee3c3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build/ *.so .externalNativeBuild .cxx +*.zip # Crashlytics configuations com_crashlytics_export_strings.xml diff --git a/app/build.gradle b/app/build.gradle index 2bf47f80..5c4b25ae 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,6 @@ -apply plugin: 'com.android.application' +plugins { + id "com.android.application" +} android { compileSdkVersion 28 @@ -12,10 +14,21 @@ android { defaultConfig { applicationId "com.termux" - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion 28 versionCode 75 versionName "0.75" + + externalNativeBuild { + ndkBuild { + cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections" + } + } + + ndk { + abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' + } + } signingConfigs { @@ -43,6 +56,12 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + externalNativeBuild { + ndkBuild { + path "src/main/cpp/Android.mk" + } + } } dependencies { @@ -54,3 +73,69 @@ task versionName { print android.defaultConfig.versionName } } + +def downloadBootstrap(String arch, String expectedChecksum, int version) { + def digest = java.security.MessageDigest.getInstance("SHA-256") + + def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip" + def file = new File(projectDir, localUrl) + if (file.exists()) { + def buffer = new byte[8192] + def input = new FileInputStream(file) + while (true) { + def readBytes = input.read(buffer) + if (readBytes < 0) break + digest.update(buffer, 0, readBytes) + } + def checksum = new BigInteger(1, digest.digest()).toString(16) + if (checksum == expectedChecksum) { + return + } else { + logger.quiet("Deleting old local file with wrong hash: " + localUrl) + file.delete() + } + } + + def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip" + logger.quiet("Downloading " + remoteUrl + " ...") + + file.parentFile.mkdirs() + def out = new BufferedOutputStream(new FileOutputStream(file)) + + def connection = new URL(remoteUrl).openConnection() + connection.setInstanceFollowRedirects(true) + def digestStream = new java.security.DigestInputStream(connection.inputStream, digest) + out << digestStream + out.close() + + def checksum = new BigInteger(1, digest.digest()).toString(16) + if (checksum != expectedChecksum) { + file.delete() + throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum) + } +} + +clean { + doLast { + def tree = fileTree(new File(projectDir, 'src/main/cpp')) + tree.include 'bootstrap-*.zip' + tree.each { it.delete() } + } +} + +task downloadBootstraps(){ + doLast { + def version = 17 + downloadBootstrap("aarch64", "1846c453e7d4399559683e9a74eb8db048d1e88872668361652752b40ea2f3a2", version) + downloadBootstrap("arm", "2f5a8061a1c13d48b659cb57c3767d59f5151f247ea032e2401135701dbd8058", version) + downloadBootstrap("i686", "ca0e3559c5ac925528a7c8725a83a752ca63d5c1e0ff306e5ecd6ea9dd4c1de5", version) + downloadBootstrap("x86_64", "9de2ac75ed0f3370418d3c55b470c176f69180079ba9461034bc7f6195fc5872", version) + } +} + +afterEvaluate { + android.applicationVariants.all { variant -> + variant.javaCompiler.dependsOn(downloadBootstraps) + } +} + diff --git a/app/src/main/cpp/Android.mk b/app/src/main/cpp/Android.mk new file mode 100644 index 00000000..d013e2ed --- /dev/null +++ b/app/src/main/cpp/Android.mk @@ -0,0 +1,5 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) +LOCAL_MODULE := libtermux-bootstrap +LOCAL_SRC_FILES := termux-bootstrap-zip.S termux-bootstrap.c +include $(BUILD_SHARED_LIBRARY) diff --git a/app/src/main/cpp/termux-bootstrap-zip.S b/app/src/main/cpp/termux-bootstrap-zip.S new file mode 100644 index 00000000..1cfc9580 --- /dev/null +++ b/app/src/main/cpp/termux-bootstrap-zip.S @@ -0,0 +1,18 @@ + .global blob + .global blob_size + .section .rodata + blob: + #if defined __i686__ + .incbin "bootstrap-i686.zip" + #elif defined __x86_64__ + .incbin "bootstrap-x86_64.zip" + #elif defined __aarch64__ + .incbin "bootstrap-aarch64.zip" + #elif defined __arm__ + .incbin "bootstrap-arm.zip" + #else + # error Unsupported arch + #endif + 1: + blob_size: + .int 1b - blob diff --git a/app/src/main/cpp/termux-bootstrap.c b/app/src/main/cpp/termux-bootstrap.c new file mode 100644 index 00000000..8ba745ff --- /dev/null +++ b/app/src/main/cpp/termux-bootstrap.c @@ -0,0 +1,11 @@ +#include + +extern jbyte blob[]; +extern int blob_size; + +JNIEXPORT jbyteArray JNICALL Java_com_termux_app_TermuxInstaller_getZip(JNIEnv *env, __attribute__((__unused__)) jobject This) +{ + jbyteArray ret = (*env)->NewByteArray(env, blob_size); + (*env)->SetByteArrayRegion(env, ret, 0, blob_size, blob); + return ret; +} diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 74823b63..34b5b15d 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -16,6 +16,7 @@ import com.termux.R; import com.termux.terminal.EmulatorDebug; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -38,7 +39,7 @@ import java.util.zip.ZipInputStream; *

* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below. *

- * (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}. + * (4) The zip file is loaded from a shared library. *

* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream * continuously encountering zip file entries: @@ -82,8 +83,8 @@ final class TermuxInstaller { final byte[] buffer = new byte[8096]; final List> symlinks = new ArrayList<>(50); - final URL zipUrl = determineZipUrl(); - try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) { + final byte[] zipBytes = loadZipBytes(); + try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { ZipEntry zipEntry; while ((zipEntry = zipInput.getNextEntry()) != null) { if (zipEntry.getName().equals("SYMLINKS.txt")) { @@ -167,34 +168,13 @@ final class TermuxInstaller { } } - /** Get bootstrap zip url for this systems cpu architecture. */ - private static URL determineZipUrl() throws MalformedURLException { - String archName = determineTermuxArchName(); - String url = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - ? "https://termux.org/bootstrap-" + archName + ".zip" - : "https://termux.net/bootstrap/bootstrap-" + archName + ".zip"; - return new URL(url); + public static byte[] loadZipBytes() { + // Only load the shared library when necessary to save memory usage. + System.loadLibrary("termux-bootstrap"); + return getZip(); } - private static String determineTermuxArchName() { - // Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64" - // while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo). - // Instead we search through the supported abi:s on the device, see: - // http://developer.android.com/ndk/guides/abis.html - // Note that we search for abi:s in preferred order (the ordering of the - // Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm - // emulation is available. - for (String androidArch : Build.SUPPORTED_ABIS) { - switch (androidArch) { - case "arm64-v8a": return "aarch64"; - case "armeabi-v7a": return "arm"; - case "x86_64": return "x86_64"; - case "x86": return "i686"; - } - } - throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " + - Arrays.toString(Build.SUPPORTED_ABIS)); - } + public static native byte[] getZip(); /** Delete a folder and all its content or throw. Don't follow symlinks. */ static void deleteFolder(File fileOrDirectory) throws IOException {