diff --git a/CrossCompileGitForAndroid.sh b/CrossCompileGitForAndroid.sh new file mode 100644 index 0000000..530ea90 --- /dev/null +++ b/CrossCompileGitForAndroid.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$SCRIPT_DIR" +SDK_DIR="$PROJECT_DIR/android-sdk" +NDK_DIR="$SDK_DIR/ndk/27.2.12479018" +TOOLBIN="$NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin" +TMP_DIR="$PROJECT_DIR/.tmp-android-build" +GIT_SRC_DIR="$TMP_DIR/git-src" +ASSET_DIR="$PROJECT_DIR/app/src/main/assets/native-git/arm64-v8a" + +log() { + printf '\n[%s] %s\n' "cross-compile" "$1" +} + +require_path() { + if [ ! -e "$1" ]; then + echo "Required path not found: $1" >&2 + exit 1 + fi +} + +require_tool() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required tool: $1" >&2 + exit 1 + fi +} + +main() { + require_tool git + require_tool make + require_tool perl + require_tool clang + require_tool pkg-config + require_tool readelf + + require_path "$NDK_DIR" + require_path "$TOOLBIN/aarch64-linux-android24-clang" + + mkdir -p "$TMP_DIR" + + if [ ! -d "$GIT_SRC_DIR/.git" ]; then + log "Cloning Git source" + git clone --depth 1 https://github.com/git/git.git "$GIT_SRC_DIR" + else + log "Using existing Git source checkout" + fi + + export PATH="$TOOLBIN:$PATH" + + log "Building native Git for Android arm64-v8a" + cd "$GIT_SRC_DIR" + make clean >/dev/null 2>&1 || true + make -j4 \ + uname_S=Android \ + uname_O=Android \ + NO_PTHREADS=YesPlease \ + NO_OPENSSL=YesPlease \ + NO_CURL=YesPlease \ + NO_EXPAT=YesPlease \ + NO_GETTEXT=YesPlease \ + NO_TCLTK=YesPlease \ + NO_PERL=YesPlease \ + NO_PYTHON=YesPlease \ + NO_INSTALL_HARDLINKS=YesPlease \ + NO_ICONV=YesPlease \ + NO_LIBGEN_H=YesPlease \ + NO_REGEX=NeedsStartEnd \ + HAVE_ALLOCA_H=YesPlease \ + HAVE_PATHS_H=YesPlease \ + HAVE_DEV_TTY=YesPlease \ + HAVE_CLOCK_GETTIME=YesPlease \ + HAVE_CLOCK_MONOTONIC=YesPlease \ + HAVE_GETDELIM=YesPlease \ + FREAD_READS_DIRECTORIES=UnfortunatelyYes \ + CSPRNG_METHOD= \ + CC=aarch64-linux-android24-clang \ + AR=llvm-ar \ + RANLIB=llvm-ranlib \ + STRIP=llvm-strip \ + git + + log "Validating the produced binary" + file git + readelf -h git | sed -n '1,20p' + readelf -d git | sed -n '1,40p' + + mkdir -p "$ASSET_DIR" + llvm-strip -s git -o "$ASSET_DIR/git" + chmod 755 "$ASSET_DIR/git" + + log "Bundled stripped binary at: $ASSET_DIR/git" + ls -lh "$ASSET_DIR/git" +} + +main "$@" \ No newline at end of file diff --git a/SetupBuildEnvironment.sh b/SetupBuildEnvironment.sh index ba98be7..54c3045 100755 --- a/SetupBuildEnvironment.sh +++ b/SetupBuildEnvironment.sh @@ -16,6 +16,10 @@ LOCAL_PROPERTIES_PATH="$PROJECT_DIR/local.properties" ANDROID_CMDLINE_TOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" GRADLE_DIST_URL="https://services.gradle.org/distributions/gradle-8.7-bin.zip" JDK_DIST_URL="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jdk/hotspot/normal/eclipse" +ANDROID_NDK_PACKAGE="ndk;27.2.12479018" +ANDROID_CMAKE_PACKAGE="cmake;3.22.1" +ANDROID_NDK_DIR="$SDK_DIR/ndk/27.2.12479018" +ANDROID_CMAKE_DIR="$SDK_DIR/cmake/3.22.1" STATE_TTL_SECONDS=86400 log() { @@ -242,7 +246,9 @@ ensure_sdk_packages() { if have_recent_success_marker \ && [ -d "$SDK_DIR/platform-tools" ] \ && [ -d "$SDK_DIR/platforms/android-34" ] \ - && [ -d "$SDK_DIR/build-tools/34.0.0" ]; then + && [ -d "$SDK_DIR/build-tools/34.0.0" ] \ + && [ -d "$ANDROID_NDK_DIR" ] \ + && [ -d "$ANDROID_CMAKE_DIR" ]; then log "Skipping SDK verification/install because all components were confirmed within the last 24 hours" return fi @@ -263,7 +269,9 @@ ensure_sdk_packages() { "$CMDLINE_TOOLS_LATEST_DIR/bin/sdkmanager" --sdk_root="$SDK_DIR" \ "platform-tools" \ "platforms;android-34" \ - "build-tools;34.0.0" + "build-tools;34.0.0" \ + "$ANDROID_NDK_PACKAGE" \ + "$ANDROID_CMAKE_PACKAGE" mark_successful_check } @@ -286,6 +294,12 @@ main() { require_tool curl require_tool unzip require_tool tar + require_tool git + require_tool perl + require_tool make + require_tool clang + require_tool pkg-config + require_tool readelf ensure_dir "$TMP_DIR" ensure_dir "$SDK_DIR" @@ -301,6 +315,9 @@ main() { log "Project dir: $PROJECT_DIR" log "JDK dir: $JDK_DIR" log "SDK dir: $SDK_DIR" + log "NDK dir: $ANDROID_NDK_DIR" + log "CMake dir: $ANDROID_CMAKE_DIR" + log "To cross-compile Git for Android arm64-v8a: bash ./CrossCompileGitForAndroid.sh" log "To build without a background Gradle daemon: ./gradlew --no-daemon assembleDebug" log "To setup and build in one step: bash ./SetupBuildEnvironment.sh --build" } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cf5cdff..de23f5b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.kawomi.githugandroid" minSdk = 26 targetSdk = 34 - versionCode = 9 - versionName = "0.1.8" + versionCode = 10 + versionName = "0.1.9" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/assets/native-git/arm64-v8a/git b/app/src/main/assets/native-git/arm64-v8a/git new file mode 100755 index 0000000..90532c8 Binary files /dev/null and b/app/src/main/assets/native-git/arm64-v8a/git differ diff --git a/app/src/main/java/com/kawomi/githugandroid/GitRuntime.kt b/app/src/main/java/com/kawomi/githugandroid/GitRuntime.kt index aac28a0..fb50c5e 100644 --- a/app/src/main/java/com/kawomi/githugandroid/GitRuntime.kt +++ b/app/src/main/java/com/kawomi/githugandroid/GitRuntime.kt @@ -1,21 +1,23 @@ package com.kawomi.githugandroid +import android.os.Build import android.content.Context import java.io.File class GitRepositoryRuntime(private val context: Context) { private val sandboxesRoot = File(context.filesDir, "githug-sandboxes") + private val extractedGitDir = File(context.filesDir, "native-git/bin") fun startupBanner(): String { - return if (nativeGitBinary() != null) { + return if (ensureNativeGitBinary() != null) { "Welcome to GitHug Android. Native Git prototype ready." } else { - "Welcome to GitHug Android. Native Git prototype scaffolded; using in-memory fallback until a bundled Git binary is available." + "Welcome to GitHug Android. Native Git binary not bundled for this ABI yet; using in-memory fallback." } } fun prepareLevel(level: Level): RepoState { - val nativeGit = nativeGitBinary() + val nativeGit = ensureNativeGitBinary() if (nativeGit == null) { return level.setup() } @@ -50,7 +52,7 @@ class GitRepositoryRuntime(private val context: Context) { } fun execute(level: Level, currentRepo: RepoState, command: String): Pair> { - val nativeGit = nativeGitBinary() + val nativeGit = ensureNativeGitBinary() if (nativeGit == null) { return GitSandboxEngine.execute(currentRepo, command) } @@ -76,7 +78,8 @@ class GitRepositoryRuntime(private val context: Context) { return buildList { addAll(GitSandboxEngine.commandReferenceLines()) add("Native Git prototype:") - add(" bundled binary path: files/native-git/bin/git") + add(" extracted binary path: files/native-git/bin/git") + add(" selected ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}") add(" helper commands: ls, pwd, cat, touch, mkdir, rm, echo") } } @@ -129,7 +132,7 @@ class GitRepositoryRuntime(private val context: Context) { private fun inspectSandbox(level: Level): RepoState { val sandbox = sandboxDir(level) - val nativeGit = nativeGitBinary() + val nativeGit = ensureNativeGitBinary() val filesOnDisk = sandbox.listFiles() ?.filter { it.isFile && it.name != ".git" } .orEmpty() @@ -177,12 +180,27 @@ class GitRepositoryRuntime(private val context: Context) { ) } - private fun nativeGitBinary(): File? { - val candidates = listOf( - File(context.filesDir, "native-git/bin/git"), - File(context.filesDir, "git/bin/git"), - ) - return candidates.firstOrNull { it.exists() && it.canExecute() } + private fun ensureNativeGitBinary(): File? { + val extracted = File(extractedGitDir, "git") + if (extracted.exists() && extracted.canExecute()) { + return extracted + } + + val abi = Build.SUPPORTED_ABIS.firstOrNull() ?: return null + val assetPath = "native-git/$abi/git" + + return try { + extractedGitDir.mkdirs() + context.assets.open(assetPath).use { input -> + extracted.outputStream().use { output -> + input.copyTo(output) + } + } + extracted.setExecutable(true) + extracted.takeIf { it.canExecute() } + } catch (_: Exception) { + null + } } private fun sandboxDir(level: Level): File = File(sandboxesRoot, level.id)