diff --git a/AndroidProjectTooling.sh b/AndroidProjectTooling.sh index 0908fc2..baf40d5 100755 --- a/AndroidProjectTooling.sh +++ b/AndroidProjectTooling.sh @@ -15,6 +15,7 @@ CMDLINE_TOOLS_DIR="$SDK_DIR/cmdline-tools" CMDLINE_TOOLS_LATEST_DIR="$CMDLINE_TOOLS_DIR/latest" WRAPPER_JAR_PATH="$PROJECT_DIR/gradle/wrapper/gradle-wrapper.jar" LOCAL_PROPERTIES_PATH="$PROJECT_DIR/local.properties" +HOST_GIT_BINARY="$PROJECT_DIR/build/host-git/libgit.so" 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" @@ -300,6 +301,7 @@ maybe_run_operation() { local artifact_target="" local should_bump_version="false" local should_auto_commit="false" + local should_compile_host_git="false" case "$mode" in --build) @@ -329,6 +331,11 @@ maybe_run_operation() { --test) gradle_task="testDebugUnitTest" artifact_label="debug unit tests" + should_compile_host_git="true" + ;; + --compile-git) + "$PROJECT_DIR/CompileGitForAllTargetPlatforms.sh" --all + return ;; *) return @@ -339,6 +346,10 @@ maybe_run_operation() { if [ "$should_bump_version" = "true" ]; then bump_android_version fi + if [ "$should_compile_host_git" = "true" ]; then + "$PROJECT_DIR/CompileGitForAllTargetPlatforms.sh" --host + export GITHUG_TEST_GIT_BINARY="$HOST_GIT_BINARY" + fi log "Running $artifact_label with --no-daemon" "$PROJECT_DIR/gradlew" --no-daemon "$gradle_task" @@ -360,18 +371,19 @@ maybe_run_operation() { print_usage() { cat <&2 @@ -411,12 +423,12 @@ main() { 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 compile Git for host and Android ABIs: bash ./AndroidProjectTooling.sh --compile-git" log "To build without a background Gradle daemon: ./gradlew --no-daemon assembleDebug" log "To set up and build in one step: bash ./AndroidProjectTooling.sh --build" log "To set up and build a debug AAB in one step: bash ./AndroidProjectTooling.sh --build-aab" log "To set up and build a release AAB in one step: bash ./AndroidProjectTooling.sh --build-release-aab" - log "To set up and run unit tests in one step: bash ./AndroidProjectTooling.sh --test" + log "To set up and run unit tests with compiled host Git: bash ./AndroidProjectTooling.sh --test" } main "$@" diff --git a/CompileGitForAllTargetPlatforms.sh b/CompileGitForAllTargetPlatforms.sh new file mode 100755 index 0000000..d13a1c7 --- /dev/null +++ b/CompileGitForAllTargetPlatforms.sh @@ -0,0 +1,173 @@ +#!/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" +HOST_GIT_DIR="$PROJECT_DIR/build/host-git" +HOST_GIT_BINARY="$HOST_GIT_DIR/libgit.so" +ANDROID_JNI_DIR="$PROJECT_DIR/app/src/main/jniLibs" +ANDROID_API=24 + +log() { + printf '\n[%s] %s\n' "compile-git" "$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 +} + +ensure_git_source() { + 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 +} + +common_git_make_args() { + printf '%s\n' \ + 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_REGEX=NeedsStartEnd \ + HAVE_ALLOCA_H=YesPlease \ + HAVE_PATHS_H=YesPlease \ + HAVE_CLOCK_GETTIME=YesPlease \ + HAVE_CLOCK_MONOTONIC=YesPlease \ + HAVE_GETDELIM=YesPlease \ + FREAD_READS_DIRECTORIES=UnfortunatelyYes \ + CSPRNG_METHOD= +} + +build_host_git() { + log "Building Git for development host" + cd "$GIT_SRC_DIR" + make clean >/dev/null 2>&1 || true + mapfile -t make_args < <(common_git_make_args) + make -j"$(nproc 2>/dev/null || printf 4)" "${make_args[@]}" git + + mkdir -p "$HOST_GIT_DIR" + cp git "$HOST_GIT_BINARY" + chmod 755 "$HOST_GIT_BINARY" + + log "Host test Git binary: $HOST_GIT_BINARY" + "$HOST_GIT_BINARY" --version +} + +build_android_git_for_abi() { + local abi="$1" + local cc="$2" + local output_dir="$ANDROID_JNI_DIR/$abi" + + require_path "$TOOLBIN/${cc}" + + log "Building Git for Android $abi" + cd "$GIT_SRC_DIR" + make clean >/dev/null 2>&1 || true + mapfile -t make_args < <(common_git_make_args) + make -j"$(nproc 2>/dev/null || printf 4)" \ + uname_S=Android \ + uname_O=Android \ + NO_PTHREADS=YesPlease \ + NO_LIBGEN_H=YesPlease \ + HAVE_DEV_TTY=YesPlease \ + CC="$cc" \ + AR=llvm-ar \ + RANLIB=llvm-ranlib \ + STRIP=llvm-strip \ + "${make_args[@]}" \ + git + + log "Validating Android $abi binary" + file git + readelf -h git | sed -n '1,12p' + + mkdir -p "$output_dir" + llvm-strip -s git -o "$output_dir/libgit.so" + chmod 755 "$output_dir/libgit.so" + ls -lh "$output_dir/libgit.so" +} + +build_android_git() { + require_path "$NDK_DIR" + export PATH="$TOOLBIN:$PATH" + + build_android_git_for_abi "arm64-v8a" "aarch64-linux-android${ANDROID_API}-clang" + build_android_git_for_abi "armeabi-v7a" "armv7a-linux-androideabi${ANDROID_API}-clang" + build_android_git_for_abi "x86" "i686-linux-android${ANDROID_API}-clang" + build_android_git_for_abi "x86_64" "x86_64-linux-android${ANDROID_API}-clang" +} + +print_usage() { + cat <&2 + print_usage >&2 + exit 1 + ;; + esac + + require_tool git + require_tool make + require_tool perl + require_tool clang + require_tool pkg-config + require_tool readelf + + ensure_git_source + + case "${1:---all}" in + --host) + build_host_git + ;; + --android) + build_android_git + ;; + --all) + build_host_git + build_android_git + ;; + esac +} + +main "$@" diff --git a/CrossCompileGitForAndroid.sh b/CrossCompileGitForAndroid.sh deleted file mode 100644 index 5c99d98..0000000 --- a/CrossCompileGitForAndroid.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/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" -JNI_LIB_DIR="$PROJECT_DIR/app/src/main/jniLibs/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 "$JNI_LIB_DIR" - llvm-strip -s git -o "$JNI_LIB_DIR/libgit.so" - chmod 755 "$JNI_LIB_DIR/libgit.so" - - log "Bundled stripped binary at: $JNI_LIB_DIR/libgit.so" - ls -lh "$JNI_LIB_DIR/libgit.so" -} - -main "$@" \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fc0700a..66f68e1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "solutions.tretter.githugandroid" minSdk = 26 targetSdk = 35 - versionCode = 122 - versionName = "0.1.121" + versionCode = 123 + versionName = "0.1.122" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/jniLibs/arm64-v8a/libgit.so b/app/src/main/jniLibs/arm64-v8a/libgit.so index 90532c8..1f7f115 100755 Binary files a/app/src/main/jniLibs/arm64-v8a/libgit.so and b/app/src/main/jniLibs/arm64-v8a/libgit.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libgit.so b/app/src/main/jniLibs/armeabi-v7a/libgit.so new file mode 100755 index 0000000..ffc27cc Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libgit.so differ diff --git a/app/src/main/jniLibs/x86/libgit.so b/app/src/main/jniLibs/x86/libgit.so new file mode 100755 index 0000000..32d537b Binary files /dev/null and b/app/src/main/jniLibs/x86/libgit.so differ diff --git a/app/src/main/jniLibs/x86_64/libgit.so b/app/src/main/jniLibs/x86_64/libgit.so new file mode 100755 index 0000000..c240b67 Binary files /dev/null and b/app/src/main/jniLibs/x86_64/libgit.so differ diff --git a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt index fe190d2..c83697e 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt @@ -152,7 +152,7 @@ class GitSandboxEngineTest { @Test fun nativeCompletionUsesCurrentDirectoryIncludingGitMetadata() { - val git = File("/usr/bin/git") + val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-completion").toFile() try { @@ -177,7 +177,7 @@ class GitSandboxEngineTest { @Test fun nativePushLevelHasDivergedStatusAndRebasesCleanly() { - val git = File("/usr/bin/git") + val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-push-level").toFile() try { @@ -221,4 +221,22 @@ class GitSandboxEngineTest { assertTrue(output.isEmpty()) assertFalse(updatedRepo.files.any { it.name == "deleteme.txt" }) } + + private fun testGitBinary(): File { + System.getenv("GITHUG_TEST_GIT_BINARY") + ?.takeIf { it.isNotBlank() } + ?.let { File(it) } + ?.takeIf { it.exists() && it.canExecute() } + ?.let { return it } + + val repoHostGit = File(repoRoot(), "build/host-git/libgit.so") + if (repoHostGit.exists() && repoHostGit.canExecute()) return repoHostGit + + return File("/usr/bin/git") + } + + private fun repoRoot(): File { + val userDir = File(System.getProperty("user.dir") ?: ".") + return if (File(userDir, "app/build.gradle.kts").exists()) userDir else userDir.parentFile ?: userDir + } } diff --git a/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt b/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt index 2830fe3..2c30517 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt @@ -31,7 +31,7 @@ class LevelSolutionsTest { @Test fun knownSolutionsCompleteEveryLevel() { val evidence = StringBuilder() - val gitBinary = systemGitBinary() + val gitBinary = testGitBinary() val runtimeRoot = testSandboxRoot().apply { deleteRecursively() mkdirs() @@ -120,7 +120,16 @@ class LevelSolutionsTest { return File(appDir, "build/test-sandboxes/level-solutions") } - fun systemGitBinary(): File { + fun testGitBinary(): File { + System.getenv("GITHUG_TEST_GIT_BINARY") + ?.takeIf { it.isNotBlank() } + ?.let { File(it) } + ?.takeIf { it.exists() && it.canExecute() } + ?.let { return it } + + val repoHostGit = File(repoRoot(), "build/host-git/libgit.so") + if (repoHostGit.exists() && repoHostGit.canExecute()) return repoHostGit + val candidates = listOf( File("/usr/bin/git"), File("/usr/local/bin/git"), @@ -136,6 +145,11 @@ class LevelSolutionsTest { return File(output) } + fun repoRoot(): File { + val userDir = File(System.getProperty("user.dir") ?: ".") + return if (File(userDir, "app/build.gradle.kts").exists()) userDir else userDir.parentFile ?: userDir + } + fun RepoState.describeForEvidence(): String = buildString { appendLine("initialized=$initialized") appendLine("headBranch=$headBranch")