[PATCH v5 0/2] MR10438: configure: fail early on invalid Gradle/SDK
This change is motivated by the long-standing issue that most Linux distributions ship an outdated Gradle, which is often too old for modern Android builds. In typical Android projects this is solved by using the Gradle wrapper, which ensures a compatible Gradle version is downloaded and used automatically. However, Wine cannot rely on the Gradle wrapper for this purpose, and integrating automatic downloads into the build system is not acceptable, since the build process must not fetch external resources. As a result, Gradle and the Android SDK effectively become external prerequisites that must be provided by the user or by higher-level build scripts. Previously, missing or outdated dependencies would only surface later during the build, often resulting in confusing or hard-to-diagnose errors. This change makes these requirements explicit and enforces them at configure time: - `gradle` is detected, its version is checked, and the resolved path is cached. - `ANDROID_HOME` is required and validated against the expected SDK layout. - A minimal sanity check ensures that a usable `build-tools` installation is present. This allows the build to fail early with clear error messages if the environment is not properly set up, instead of failing deep inside the Gradle build. Additionally, `GRADLE` and `ANDROID_HOME` are cached by configure, so they do not need to be re-specified when invoking make or even present in environment. -- v5: wineandroid: print hint for full APK build when using dummy target configure: validate Android build environment using minimal Gradle project https://gitlab.winehq.org/wine/wine/-/merge_requests/10438
From: Twaik Yont <9674930+twaik@users.noreply.github.com> Detect and cache GRADLE and ANDROID_HOME and verify the Android build environment by compiling a minimal Gradle project during configure. Ensures the toolchain is functional and able to compile Java sources. Fail early on invalid setups and log details to config.log. Signed-off-by: Twaik Yont <9674930+twaik@users.noreply.github.com> --- configure | 137 ++++++++++++++++++++++++++- configure.ac | 82 +++++++++++++++- dlls/wineandroid.drv/build.gradle.in | 6 +- 3 files changed, 220 insertions(+), 5 deletions(-) diff --git a/configure b/configure index 981fb77c455..0c98a451379 100755 --- a/configure +++ b/configure @@ -761,6 +761,8 @@ CXX_PE_CFLAGS CAPSTONE_PE_LIBS CAPSTONE_PE_CFLAGS MINGW_PKG_CONFIG +GRADLE +ANDROID_HOME WINELOADER_DEPENDS ac_ct_OBJC OBJCFLAGS @@ -1853,6 +1855,7 @@ CXXFLAGS CCC OBJC OBJCFLAGS +ANDROID_HOME CAPSTONE_PE_CFLAGS CAPSTONE_PE_LIBS CXX_PE_CFLAGS @@ -2664,6 +2667,8 @@ Some influential environment variables: CXXFLAGS C++ compiler flags OBJC Objective C compiler command OBJCFLAGS Objective C compiler flags + ANDROID_HOME + Path to the Android SDK CAPSTONE_PE_CFLAGS C compiler flags for the PE capstone, overriding the bundled version @@ -13072,6 +13077,134 @@ fi aarch64) exec_prefix='${prefix}/arm64-v8a' ;; esac fi + + GRADLE_MIN_VERSION=9.4.0 + ANDROID_COMPILE_SDK=36 + + + # Extract the first word of "gradle", so it can be a program name with args. +set dummy gradle; ac_word=$2 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_path_GRADLE+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) case $GRADLE in + [\\/]* | ?:[\\/]*) + ac_cv_path_GRADLE="$GRADLE" # Let the user override the test with a path. + ;; + *) + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_path_GRADLE="$as_dir$ac_word$ac_exec_ext" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + + ;; +esac ;; +esac +fi +GRADLE=$ac_cv_path_GRADLE +if test -n "$GRADLE"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $GRADLE" >&5 +printf "%s\n" "$GRADLE" >&6; } +else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +fi + + + + test -n "$GRADLE" || as_fn_error $? "gradle not found" "$LINENO" 5 + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether gradle can build a minimal Android project" >&5 +printf %s "checking whether gradle can build a minimal Android project... " >&6; } + + rm -rf conftest + mkdir -p conftest/src || as_fn_error $? "cannot create conftest/src" "$LINENO" 5 + + cat > conftest/build.gradle <<EOF + if (GradleVersion.current() < GradleVersion.version("$GRADLE_MIN_VERSION")) { + throw new GradleException("Gradle $GRADLE_MIN_VERSION+ required") + } + + apply plugin: 'com.android.application' + + buildscript + { + repositories + { + google() + mavenCentral() + } + dependencies + { + classpath "com.android.tools.build:gradle:9.1.0" + } + } + + repositories + { + google() + mavenCentral() + } + + android + { + namespace "com.test" + compileSdk $ANDROID_COMPILE_SDK + + defaultConfig + { + applicationId "com.test" + minSdk 21 + targetSdk $ANDROID_COMPILE_SDK + versionCode 1 + versionName "1.0" + } + + sourceSets + { + main.java.srcDirs = [ "./src" ] + main.manifest.srcFile "./AndroidManifest.xml" + } + } + + configurations.configureEach + { + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib" + } +EOF + + echo "package com.test; public class A extends android.app.Activity { }" > conftest/src/A.java + echo '<manifest xmlns:android="http://schemas.android.com/apk/res/android"><application><activity android:name="com.test.A" /></application></manifest>' > conftest/AndroidManifest.xml + + echo "configure: testing minimal Android Gradle build" >>config.log + if (cd conftest && env ANDROID_HOME="$ANDROID_HOME" "$GRADLE" -q assembleDebug >>../config.log 2>&1); then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } + else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } + as_fn_error $? "failed to build a minimal Android project with gradle and ANDROID_HOME='$ANDROID_HOME'. See 'config.log' for more details." "$LINENO" 5 + fi + + rm -rf conftest + SED_CMD="$SED_CMD -e 's,@GRADLE_MIN_VERSION@,$GRADLE_MIN_VERSION,g' -e 's,@ANDROID_COMPILE_SDK@,$ANDROID_COMPILE_SDK,g'" ;; *) @@ -24696,7 +24829,7 @@ dlls/ntdll/unix/version.c: dummy programs/winetest/build.rc: dummy @build=\"STRINGTABLE { 1 \\\"\`GIT_DIR=${wine_srcdir}.git git rev-parse HEAD 2>/dev/null\`\\\" }\" && (echo \$\$build | cmp -s - \$@) || echo \$\$build >\$@ || (rm -f \$@ && exit 1) dlls/wineandroid.drv/wine-debug.apk: dlls/wineandroid.drv/build.gradle ${wine_srcdir}dlls/wineandroid.drv/AndroidManifest.xml ${wine_srcdir}dlls/wineandroid.drv/WineActivity.java ${wine_srcdir}dlls/wineandroid.drv/wine.svg - cd dlls/wineandroid.drv && gradle -q -Psrcdir=$srcdir assembleDebug + cd dlls/wineandroid.drv && env ANDROID_HOME=\"$ANDROID_HOME\" $GRADLE -q -Psrcdir=$srcdir assembleDebug mv dlls/wineandroid.drv/build/outputs/apk/debug/wine-debug.apk \$@" @@ -25589,6 +25722,8 @@ OBJC = $OBJC OBJCFLAGS = $OBJCFLAGS ac_ct_OBJC = $ac_ct_OBJC WINELOADER_DEPENDS = $WINELOADER_DEPENDS +ANDROID_HOME = $ANDROID_HOME +GRADLE = $GRADLE MINGW_PKG_CONFIG = $MINGW_PKG_CONFIG CAPSTONE_PE_CFLAGS = $CAPSTONE_PE_CFLAGS CAPSTONE_PE_LIBS = $CAPSTONE_PE_LIBS diff --git a/configure.ac b/configure.ac index 9a437851996..1a0bca19403 100644 --- a/configure.ac +++ b/configure.ac @@ -1064,6 +1064,86 @@ case $host_os in aarch64) exec_prefix='${prefix}/arm64-v8a' ;; esac fi + + GRADLE_MIN_VERSION=9.4.0 + ANDROID_COMPILE_SDK=36 + + AC_ARG_VAR([ANDROID_HOME], [Path to the Android SDK]) + AC_PATH_PROG([GRADLE], [gradle]) + + test -n "$GRADLE" || AC_MSG_ERROR([gradle not found]) + + AC_MSG_CHECKING([whether gradle can build a minimal Android project]) + + rm -rf conftest + mkdir -p conftest/src || AC_MSG_ERROR([cannot create conftest/src]) + + cat > conftest/build.gradle <<EOF + if (GradleVersion.current() < GradleVersion.version("$GRADLE_MIN_VERSION")) { + throw new GradleException("Gradle $GRADLE_MIN_VERSION+ required") + } + + apply plugin: 'com.android.application' + + buildscript + { + repositories + { + google() + mavenCentral() + } + dependencies + { + classpath "com.android.tools.build:gradle:9.1.0" + } + } + + repositories + { + google() + mavenCentral() + } + + android + { + namespace "com.test" + compileSdk $ANDROID_COMPILE_SDK + + defaultConfig + { + applicationId "com.test" + minSdk 21 + targetSdk $ANDROID_COMPILE_SDK + versionCode 1 + versionName "1.0" + } + + sourceSets + { + main.java.srcDirs = [[ "./src" ]] + main.manifest.srcFile "./AndroidManifest.xml" + } + } + + configurations.configureEach + { + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib" + } +EOF + + echo "package com.test; public class A extends android.app.Activity { }" > conftest/src/A.java + echo '<manifest xmlns:android="http://schemas.android.com/apk/res/android"><application><activity android:name="com.test.A" /></application></manifest>' > conftest/AndroidManifest.xml + + echo "configure: testing minimal Android Gradle build" >>config.log + if (cd conftest && env ANDROID_HOME="$ANDROID_HOME" "$GRADLE" -q assembleDebug >>../config.log 2>&1); then + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + AC_MSG_ERROR([failed to build a minimal Android project with gradle and ANDROID_HOME='$ANDROID_HOME'. See 'config.log' for more details.]) + fi + + rm -rf conftest + SED_CMD="$SED_CMD -e 's,@GRADLE_MIN_VERSION@,$GRADLE_MIN_VERSION,g' -e 's,@ANDROID_COMPILE_SDK@,$ANDROID_COMPILE_SDK,g'" ;; *) @@ -3844,7 +3924,7 @@ WINE_APPEND_RULE( programs/winetest/build.rc: dummy @build=\"STRINGTABLE { 1 \\\"\`GIT_DIR=${wine_srcdir}.git git rev-parse HEAD 2>/dev/null\`\\\" }\" && (echo \$\$build | cmp -s - \$[@]) || echo \$\$build >\$[@] || (rm -f \$[@] && exit 1) dlls/wineandroid.drv/wine-debug.apk: dlls/wineandroid.drv/build.gradle ${wine_srcdir}dlls/wineandroid.drv/AndroidManifest.xml ${wine_srcdir}dlls/wineandroid.drv/WineActivity.java ${wine_srcdir}dlls/wineandroid.drv/wine.svg - cd dlls/wineandroid.drv && gradle -q -Psrcdir=$srcdir assembleDebug + cd dlls/wineandroid.drv && env ANDROID_HOME=\"$ANDROID_HOME\" $GRADLE -q -Psrcdir=$srcdir assembleDebug mv dlls/wineandroid.drv/build/outputs/apk/debug/wine-debug.apk \$[@]]) dnl Misc rules diff --git a/dlls/wineandroid.drv/build.gradle.in b/dlls/wineandroid.drv/build.gradle.in index 8d0e138c9a2..03cab2741b3 100644 --- a/dlls/wineandroid.drv/build.gradle.in +++ b/dlls/wineandroid.drv/build.gradle.in @@ -18,8 +18,8 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ -if (GradleVersion.current() < GradleVersion.version("9.4.0")) { - throw new GradleException("Gradle 9.4.0+ required") +if (GradleVersion.current() < GradleVersion.version("@GRADLE_MIN_VERSION@")) { + throw new GradleException("Gradle @GRADLE_MIN_VERSION@+ required") } apply plugin: 'com.android.application' @@ -107,7 +107,7 @@ tasks.withType(JavaCompile) android { namespace "org.winehq.wine" - compileSdkVersion 36 + compileSdk @ANDROID_COMPILE_SDK@ defaultConfig { -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10438
From: Twaik Yont <9674930+twaik@users.noreply.github.com> Print a message when the dummy wine-debug.apk target is built without packaged assets, pointing to a reference full build script. This clarifies that the target is only a sanity check and helps avoid confusion when the resulting APK is not a complete build. Also document this behavior in Makefile.in. Signed-off-by: Twaik Yont <9674930+twaik@users.noreply.github.com> --- configure | 3 ++- configure.ac | 3 ++- dlls/wineandroid.drv/Makefile.in | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/configure b/configure index 0c98a451379..016f03d6b28 100755 --- a/configure +++ b/configure @@ -24830,7 +24830,8 @@ programs/winetest/build.rc: dummy @build=\"STRINGTABLE { 1 \\\"\`GIT_DIR=${wine_srcdir}.git git rev-parse HEAD 2>/dev/null\`\\\" }\" && (echo \$\$build | cmp -s - \$@) || echo \$\$build >\$@ || (rm -f \$@ && exit 1) dlls/wineandroid.drv/wine-debug.apk: dlls/wineandroid.drv/build.gradle ${wine_srcdir}dlls/wineandroid.drv/AndroidManifest.xml ${wine_srcdir}dlls/wineandroid.drv/WineActivity.java ${wine_srcdir}dlls/wineandroid.drv/wine.svg cd dlls/wineandroid.drv && env ANDROID_HOME=\"$ANDROID_HOME\" $GRADLE -q -Psrcdir=$srcdir assembleDebug - mv dlls/wineandroid.drv/build/outputs/apk/debug/wine-debug.apk \$@" + mv dlls/wineandroid.drv/build/outputs/apk/debug/wine-debug.apk \$@ + @(cd dlls/wineandroid.drv && test -d assets && test -d lib) || echo \"Dummy APK build succeeded. For a full build example, see: https://gitlab.winehq.org/winehq/tools/-/tree/master/packaging/android\" >&2" EXTERNAL_SUBDIRS="libs/capstone libs/c++ libs/c++abi libs/faudio libs/fluidsynth libs/gsm libs/icucommon libs/icui18n libs/jpeg libs/jxr libs/lcms2 libs/ldap libs/mpg123 libs/musl libs/png libs/tiff libs/tomcrypt libs/unwind libs/vkd3d libs/xml2 libs/xslt libs/zlib libs/compiler-rt" diff --git a/configure.ac b/configure.ac index 1a0bca19403..22326582bd0 100644 --- a/configure.ac +++ b/configure.ac @@ -3925,7 +3925,8 @@ programs/winetest/build.rc: dummy @build=\"STRINGTABLE { 1 \\\"\`GIT_DIR=${wine_srcdir}.git git rev-parse HEAD 2>/dev/null\`\\\" }\" && (echo \$\$build | cmp -s - \$[@]) || echo \$\$build >\$[@] || (rm -f \$[@] && exit 1) dlls/wineandroid.drv/wine-debug.apk: dlls/wineandroid.drv/build.gradle ${wine_srcdir}dlls/wineandroid.drv/AndroidManifest.xml ${wine_srcdir}dlls/wineandroid.drv/WineActivity.java ${wine_srcdir}dlls/wineandroid.drv/wine.svg cd dlls/wineandroid.drv && env ANDROID_HOME=\"$ANDROID_HOME\" $GRADLE -q -Psrcdir=$srcdir assembleDebug - mv dlls/wineandroid.drv/build/outputs/apk/debug/wine-debug.apk \$[@]]) + mv dlls/wineandroid.drv/build/outputs/apk/debug/wine-debug.apk \$[@] + @(cd dlls/wineandroid.drv && test -d assets && test -d lib) || echo \"Dummy APK build succeeded. For a full build example, see: https://gitlab.winehq.org/winehq/tools/-/tree/master/packaging/android\" >&2]) dnl Misc rules diff --git a/dlls/wineandroid.drv/Makefile.in b/dlls/wineandroid.drv/Makefile.in index 9a8be0ef90a..2f20575eac7 100644 --- a/dlls/wineandroid.drv/Makefile.in +++ b/dlls/wineandroid.drv/Makefile.in @@ -13,4 +13,6 @@ SOURCES = \ window.c \ wine.svg +# wine-debug.apk is a dummy target used to verify the build. +# For a full build script example, see: https://gitlab.winehq.org/winehq/tools/-/tree/master/packaging/android EXTRA_TARGETS = wine-debug.apk -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10438
Good. Now it only checks that Gradle exists and is able to build a minimal Android Java project, without additional SDK heuristics. All errors are written to config.log, similar to how C toolchain checks report build failures. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10438#note_134273
Thanks for accommodating me. I'm afraid I still don't see that as an improvement though. Having to duplicate all that information in configure just to get an earlier failure doesn't seem worth the trouble. It may be different if configure actually had to detect some things, but apart from maybe the path to gradle, everything is hardcoded anyway. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10438#note_134343
That makes sense. In that case we could drop the configure check and instead move the APK target earlier in the build, for example near the beginning of the all prerequisites. The main goal here is to fail early. Waiting 15+ minutes and then hitting a missing Gradle/ANDROID_HOME/Java error is quite frustrating. Since the rest of the code is already well covered by regular non-Android builds, it should be fine to build wineandroid earlier to catch such issues sooner. But I'd like to keep gradle and ANDROID_HOME caching if you do not mind. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10438#note_134345
With parallel makes, trying to control the build order is not useful. I don't think it's much of an issue precisely because we don't do anything in configure at the moment. So once the build fails, you can fix the issue and run make again and it doesn't have to rebuild anything. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10438#note_134347
`configure.ac` adds our custom rules near the beginning of the generated makefile, right after the automake variables and the initial ``` all: @echo 'Wine build complete.' ``` block. So we could make `wine-debug.apk` the first prerequisite of all and mark it `.NOTPARALLEL`. That would keep it as part of the normal build rather than a configure-time environment check, but still make it run very early, or at least as early as possible, and without interleaving its output with unrelated parallel jobs. In practice that should still give the same fail-early effect, without adding extra dummy validation code. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10438#note_134348
I don't think we should add complexity to the makefile to catch an error case that will essentially never happen, except on the very first build. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10438#note_134349
I see your point. For me this is not only about catching missing Gradle or ANDROID_HOME early, but also about detecting Java build issues earlier when working on Android-specific changes. It also helps during development, since caching GRADLE and ANDROID_HOME allows running make directly in the build directory without having to re-specify the environment or re-run build scripts when changing make parameters, similar to the usual Linux workflow. But I understand the concern about adding complexity to the build system. If this is not something you'd like to include, I'm fine with dropping it. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10438#note_134352
participants (3)
-
Alexandre Julliard (@julliard) -
Twaik Yont -
Twaik Yont (@twaik)