From 2520eed0a529be3815f70c43e1a5006deeee5596 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Wed, 1 May 2024 07:36:45 +0100 Subject: [PATCH] gh-116622: Add Android testbed (GH-117878) Add code and config for a minimal Android app, and instructions to build and run it. Improve Android build instructions in general. Add a tool subcommand to download the Gradle wrapper (with its binary blob). Android studio must be downloaded manually (due to the license). --- .github/CODEOWNERS | 12 ++ Android/README.md | 43 ++++- Android/android.py | 45 +++++- Android/testbed/.gitignore | 21 +++ Android/testbed/app/.gitignore | 1 + Android/testbed/app/build.gradle.kts | 129 +++++++++++++++ .../testbed/app/src/main/AndroidManifest.xml | 20 +++ Android/testbed/app/src/main/c/CMakeLists.txt | 9 ++ .../testbed/app/src/main/c/main_activity.c | 147 ++++++++++++++++++ .../java/org/python/testbed/MainActivity.kt | 61 ++++++++ Android/testbed/app/src/main/python/main.py | 17 ++ .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 3110 bytes .../app/src/main/res/layout/activity_main.xml | 19 +++ .../app/src/main/res/values/strings.xml | 3 + Android/testbed/build.gradle.kts | 5 + Android/testbed/gradle.properties | 23 +++ .../gradle/wrapper/gradle-wrapper.properties | 6 + Android/testbed/settings.gradle.kts | 18 +++ ...-04-14-19-35-35.gh-issue-116622.8lpX-7.rst | 1 + 19 files changed, 570 insertions(+), 10 deletions(-) create mode 100644 Android/testbed/.gitignore create mode 100644 Android/testbed/app/.gitignore create mode 100644 Android/testbed/app/build.gradle.kts create mode 100644 Android/testbed/app/src/main/AndroidManifest.xml create mode 100644 Android/testbed/app/src/main/c/CMakeLists.txt create mode 100644 Android/testbed/app/src/main/c/main_activity.c create mode 100644 Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt create mode 100644 Android/testbed/app/src/main/python/main.py create mode 100644 Android/testbed/app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 Android/testbed/app/src/main/res/layout/activity_main.xml create mode 100644 Android/testbed/app/src/main/res/values/strings.xml create mode 100644 Android/testbed/build.gradle.kts create mode 100644 Android/testbed/gradle.properties create mode 100644 Android/testbed/gradle/wrapper/gradle-wrapper.properties create mode 100644 Android/testbed/settings.gradle.kts create mode 100644 Misc/NEWS.d/next/Build/2024-04-14-19-35-35.gh-issue-116622.8lpX-7.rst diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 235bc7859940..1f5f7e57dc48 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -243,6 +243,18 @@ Lib/test/support/interpreters/ @ericsnowcurrently Modules/_xx*interp*module.c @ericsnowcurrently Lib/test/test_interpreters/ @ericsnowcurrently +# Android +**/*Android* @mhsmith +**/*android* @mhsmith + +# iOS (but not termios) +**/iOS* @freakboy3742 +**/ios* @freakboy3742 +**/*_iOS* @freakboy3742 +**/*_ios* @freakboy3742 +**/*-iOS* @freakboy3742 +**/*-ios* @freakboy3742 + # WebAssembly /Tools/wasm/ @brettcannon diff --git a/Android/README.md b/Android/README.md index 5ed186e06e39..f5f463ca1165 100644 --- a/Android/README.md +++ b/Android/README.md @@ -22,12 +22,25 @@ you don't already have the SDK, here's how to install it: `android-sdk/cmdline-tools/latest`. * `export ANDROID_HOME=/path/to/android-sdk` +The `android.py` script also requires the following commands to be on the `PATH`: + +* `curl` +* `java` +* `tar` +* `unzip` + ## Building -Building for Android requires doing a cross-build where you have a "build" -Python to help produce an Android build of CPython. This procedure has been -tested on Linux and macOS. +Python can be built for Android on any POSIX platform supported by the Android +development tools, which currently means Linux or macOS. This involves doing a +cross-build where you use a "build" Python (for your development machine) to +help produce a "host" Python for Android. + +First, make sure you have all the usual tools and libraries needed to build +Python for your development machine. The only Android tool you need to install +is the command line tools package above: the build script will download the +rest. The easiest way to do a build is to use the `android.py` script. You can either have it perform the entire build process from start to finish in one step, or @@ -43,9 +56,10 @@ The discrete steps for building via `android.py` are: ./android.py make-host HOST ``` -To see the possible values of HOST, run `./android.py configure-host --help`. +`HOST` identifies which architecture to build. To see the possible values, run +`./android.py configure-host --help`. -Or to do it all in a single command, run: +To do all steps in a single command, run: ```sh ./android.py build HOST @@ -62,3 +76,22 @@ call. For example, if you want a pydebug build that also caches the results from ```sh ./android.py build HOST -- -C --with-pydebug ``` + + +## Testing + +To run the Python test suite on Android: + +* Install Android Studio, if you don't already have it. +* Follow the instructions in the previous section to build all supported + architectures. +* Run `./android.py setup-testbed` to download the Gradle wrapper. +* Open the `testbed` directory in Android Studio. +* In the *Device Manager* dock, connect a device or start an emulator. + Then select it from the drop-down list in the toolbar. +* Click the "Run" button in the toolbar. +* The testbed app displays nothing on screen while running. To see its output, + open the [Logcat window](https://developer.android.com/studio/debug/logcat). + +To run specific tests, or pass any other arguments to the test suite, edit the +command line in testbed/app/src/main/python/main.py. diff --git a/Android/android.py b/Android/android.py index 5c57e53c415d..0a1393e61ddb 100755 --- a/Android/android.py +++ b/Android/android.py @@ -7,8 +7,9 @@ import shutil import subprocess import sys import sysconfig -from os.path import relpath +from os.path import basename, relpath from pathlib import Path +from tempfile import TemporaryDirectory SCRIPT_NAME = Path(__file__).name CHECKOUT = Path(__file__).resolve().parent.parent @@ -102,11 +103,17 @@ def unpack_deps(host): for name_ver in ["bzip2-1.0.8-1", "libffi-3.4.4-2", "openssl-3.0.13-1", "sqlite-3.45.1-0", "xz-5.4.6-0"]: filename = f"{name_ver}-{host}.tar.gz" - run(["wget", f"{deps_url}/{name_ver}/{filename}"]) + download(f"{deps_url}/{name_ver}/{filename}") run(["tar", "-xf", filename]) os.remove(filename) +def download(url, target_dir="."): + out_path = f"{target_dir}/{basename(url)}" + run(["curl", "-Lf", "-o", out_path, url]) + return out_path + + def configure_host_python(context): host_dir = subdir(context.host, clean=context.clean) @@ -160,6 +167,30 @@ def clean_all(context): delete_if_exists(CROSS_BUILD_DIR) +# To avoid distributing compiled artifacts without corresponding source code, +# the Gradle wrapper is not included in the CPython repository. Instead, we +# extract it from the Gradle release. +def setup_testbed(context): + ver_long = "8.7.0" + ver_short = ver_long.removesuffix(".0") + testbed_dir = CHECKOUT / "Android/testbed" + + for filename in ["gradlew", "gradlew.bat"]: + out_path = download( + f"https://raw.githubusercontent.com/gradle/gradle/v{ver_long}/{filename}", + testbed_dir) + os.chmod(out_path, 0o755) + + with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir: + os.chdir(temp_dir) + bin_zip = download( + f"https://services.gradle.org/distributions/gradle-{ver_short}-bin.zip") + outer_jar = f"gradle-{ver_short}/lib/plugins/gradle-wrapper-{ver_short}.jar" + run(["unzip", bin_zip, outer_jar]) + run(["unzip", "-o", "-d", f"{testbed_dir}/gradle/wrapper", outer_jar, + "gradle-wrapper.jar"]) + + def main(): parser = argparse.ArgumentParser() subcommands = parser.add_subparsers(dest="subcommand") @@ -173,8 +204,11 @@ def main(): help="Run `configure` for Android") make_host = subcommands.add_parser("make-host", help="Run `make` for Android") - clean = subcommands.add_parser("clean", help="Delete files and directories " - "created by this script") + subcommands.add_parser( + "clean", help="Delete the cross-build directory") + subcommands.add_parser( + "setup-testbed", help="Download the testbed Gradle wrapper") + for subcommand in build, configure_build, configure_host: subcommand.add_argument( "--clean", action="store_true", default=False, dest="clean", @@ -194,7 +228,8 @@ def main(): "configure-host": configure_host_python, "make-host": make_host_python, "build": build_all, - "clean": clean_all} + "clean": clean_all, + "setup-testbed": setup_testbed} dispatch[context.subcommand](context) diff --git a/Android/testbed/.gitignore b/Android/testbed/.gitignore new file mode 100644 index 000000000000..b9a7d611c943 --- /dev/null +++ b/Android/testbed/.gitignore @@ -0,0 +1,21 @@ +# The Gradle wrapper should be downloaded by running `../android.py setup-testbed`. +/gradlew +/gradlew.bat +/gradle/wrapper/gradle-wrapper.jar + +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/deploymentTargetDropdown.xml +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/Android/testbed/app/.gitignore b/Android/testbed/app/.gitignore new file mode 100644 index 000000000000..42afabfd2abe --- /dev/null +++ b/Android/testbed/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Android/testbed/app/build.gradle.kts b/Android/testbed/app/build.gradle.kts new file mode 100644 index 000000000000..7690d3fd86b2 --- /dev/null +++ b/Android/testbed/app/build.gradle.kts @@ -0,0 +1,129 @@ +import com.android.build.api.variant.* + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +val PYTHON_DIR = File(projectDir, "../../..").canonicalPath +val PYTHON_CROSS_DIR = "$PYTHON_DIR/cross-build" +val ABIS = mapOf( + "arm64-v8a" to "aarch64-linux-android", + "x86_64" to "x86_64-linux-android", +) + +val PYTHON_VERSION = File("$PYTHON_DIR/Include/patchlevel.h").useLines { + for (line in it) { + val match = """#define PY_VERSION\s+"(\d+\.\d+)""".toRegex().find(line) + if (match != null) { + return@useLines match.groupValues[1] + } + } + throw GradleException("Failed to find Python version") +} + + +android { + namespace = "org.python.testbed" + compileSdk = 34 + + defaultConfig { + applicationId = "org.python.testbed" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + ndk.abiFilters.addAll(ABIS.keys) + externalNativeBuild.cmake.arguments( + "-DPYTHON_CROSS_DIR=$PYTHON_CROSS_DIR", + "-DPYTHON_VERSION=$PYTHON_VERSION") + } + + externalNativeBuild.cmake { + path("src/main/c/CMakeLists.txt") + } + + // Set this property to something non-empty, otherwise it'll use the default + // list, which ignores asset directories beginning with an underscore. + aaptOptions.ignoreAssetsPattern = ".git" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") +} + + +// Create some custom tasks to copy Python and its standard library from +// elsewhere in the repository. +androidComponents.onVariants { variant -> + generateTask(variant, variant.sources.assets!!) { + into("python") { + for (triplet in ABIS.values) { + for (subDir in listOf("include", "lib")) { + into(subDir) { + from("$PYTHON_CROSS_DIR/$triplet/prefix/$subDir") + include("python$PYTHON_VERSION/**") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + } + } + into("lib/python$PYTHON_VERSION") { + // Uncomment this to pick up edits from the source directory + // without having to rerun `make install`. + // from("$PYTHON_DIR/Lib") + // duplicatesStrategy = DuplicatesStrategy.INCLUDE + + into("site-packages") { + from("$projectDir/src/main/python") + } + } + } + exclude("**/__pycache__") + } + + generateTask(variant, variant.sources.jniLibs!!) { + for ((abi, triplet) in ABIS.entries) { + into(abi) { + from("$PYTHON_CROSS_DIR/$triplet/prefix/lib") + include("libpython*.*.so") + include("lib*_python.so") + } + } + } +} + + +fun generateTask( + variant: ApplicationVariant, directories: SourceDirectories, + configure: GenerateTask.() -> Unit +) { + val taskName = "generate" + + listOf(variant.name, "Python", directories.name) + .map { it.replaceFirstChar(Char::uppercase) } + .joinToString("") + + directories.addGeneratedSourceDirectory( + tasks.register(taskName) { + into(outputDir) + configure() + }, + GenerateTask::outputDir) +} + + +// addGeneratedSourceDirectory requires the task to have a DirectoryProperty. +abstract class GenerateTask: Sync() { + @get:OutputDirectory + abstract val outputDir: DirectoryProperty +} diff --git a/Android/testbed/app/src/main/AndroidManifest.xml b/Android/testbed/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..2be8a82d4260 --- /dev/null +++ b/Android/testbed/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/testbed/app/src/main/c/CMakeLists.txt b/Android/testbed/app/src/main/c/CMakeLists.txt new file mode 100644 index 000000000000..1d5df9a73465 --- /dev/null +++ b/Android/testbed/app/src/main/c/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.4.1) +project(testbed) + +set(PREFIX_DIR ${PYTHON_CROSS_DIR}/${CMAKE_LIBRARY_ARCHITECTURE}/prefix) +include_directories(${PREFIX_DIR}/include/python${PYTHON_VERSION}) +link_directories(${PREFIX_DIR}/lib) +link_libraries(log python${PYTHON_VERSION}) + +add_library(main_activity SHARED main_activity.c) diff --git a/Android/testbed/app/src/main/c/main_activity.c b/Android/testbed/app/src/main/c/main_activity.c new file mode 100644 index 000000000000..73aba4164d00 --- /dev/null +++ b/Android/testbed/app/src/main/c/main_activity.c @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include +#include +#include + + +static void throw_runtime_exception(JNIEnv *env, const char *message) { + (*env)->ThrowNew( + env, + (*env)->FindClass(env, "java/lang/RuntimeException"), + message); +} + + +// --- Stdio redirection ------------------------------------------------------ + +// Most apps won't need this, because the Python-level sys.stdout and sys.stderr +// are redirected to the Android logcat by Python itself. However, in the +// testbed it's useful to redirect the native streams as well, to debug problems +// in the Python startup or redirection process. +// +// Based on +// https://github.com/beeware/briefcase-android-gradle-template/blob/v0.3.11/%7B%7B%20cookiecutter.safe_formal_name%20%7D%7D/app/src/main/cpp/native-lib.cpp + +typedef struct { + FILE *file; + int fd; + android_LogPriority priority; + char *tag; + int pipe[2]; +} StreamInfo; + +static StreamInfo STREAMS[] = { + {stdout, STDOUT_FILENO, ANDROID_LOG_INFO, "native.stdout", {-1, -1}}, + {stderr, STDERR_FILENO, ANDROID_LOG_WARN, "native.stderr", {-1, -1}}, + {NULL, -1, ANDROID_LOG_UNKNOWN, NULL, {-1, -1}}, +}; + +// The maximum length of a log message in bytes, including the level marker and +// tag, is defined as LOGGER_ENTRY_MAX_PAYLOAD in +// platform/system/logging/liblog/include/log/log.h. As of API level 30, messages +// longer than this will be be truncated by logcat. This limit has already been +// reduced at least once in the history of Android (from 4076 to 4068 between API +// level 23 and 26), so leave some headroom. +static const int MAX_BYTES_PER_WRITE = 4000; + +static void *redirection_thread(void *arg) { + StreamInfo *si = (StreamInfo*)arg; + ssize_t read_size; + char buf[MAX_BYTES_PER_WRITE]; + while ((read_size = read(si->pipe[0], buf, sizeof buf - 1)) > 0) { + buf[read_size] = '\0'; /* add null-terminator */ + __android_log_write(si->priority, si->tag, buf); + } + return 0; +} + +static char *redirect_stream(StreamInfo *si) { + /* make the FILE unbuffered, to ensure messages are never lost */ + if (setvbuf(si->file, 0, _IONBF, 0)) { + return "setvbuf"; + } + + /* create the pipe and redirect the file descriptor */ + if (pipe(si->pipe)) { + return "pipe"; + } + if (dup2(si->pipe[1], si->fd) == -1) { + return "dup2"; + } + + /* start the logging thread */ + pthread_t thr; + if ((errno = pthread_create(&thr, 0, redirection_thread, si))) { + return "pthread_create"; + } + if ((errno = pthread_detach(thr))) { + return "pthread_detach"; + } + return 0; +} + +JNIEXPORT void JNICALL Java_org_python_testbed_MainActivity_redirectStdioToLogcat( + JNIEnv *env, jobject obj +) { + for (StreamInfo *si = STREAMS; si->file; si++) { + char *error_prefix; + if ((error_prefix = redirect_stream(si))) { + char error_message[1024]; + snprintf(error_message, sizeof(error_message), + "%s: %s", error_prefix, strerror(errno)); + throw_runtime_exception(env, error_message); + return; + } + } +} + + +// --- Python intialization ---------------------------------------------------- + +static PyStatus set_config_string( + JNIEnv *env, PyConfig *config, wchar_t **config_str, jstring value +) { + const char *value_utf8 = (*env)->GetStringUTFChars(env, value, NULL); + PyStatus status = PyConfig_SetBytesString(config, config_str, value_utf8); + (*env)->ReleaseStringUTFChars(env, value, value_utf8); + return status; +} + +static void throw_status(JNIEnv *env, PyStatus status) { + throw_runtime_exception(env, status.err_msg ? status.err_msg : ""); +} + +JNIEXPORT void JNICALL Java_org_python_testbed_MainActivity_runPython( + JNIEnv *env, jobject obj, jstring home, jstring runModule +) { + PyConfig config; + PyStatus status; + PyConfig_InitIsolatedConfig(&config); + + status = set_config_string(env, &config, &config.home, home); + if (PyStatus_Exception(status)) { + throw_status(env, status); + return; + } + + status = set_config_string(env, &config, &config.run_module, runModule); + if (PyStatus_Exception(status)) { + throw_status(env, status); + return; + } + + // Some tests generate SIGPIPE and SIGXFSZ, which should be ignored. + config.install_signal_handlers = 1; + + status = Py_InitializeFromConfig(&config); + if (PyStatus_Exception(status)) { + throw_status(env, status); + return; + } + + Py_RunMain(); +} diff --git a/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt b/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt new file mode 100644 index 000000000000..5a590d5d04e9 --- /dev/null +++ b/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt @@ -0,0 +1,61 @@ +package org.python.testbed + +import android.os.* +import android.system.Os +import android.widget.TextView +import androidx.appcompat.app.* +import java.io.* + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // Python needs this variable to help it find the temporary directory, + // but Android only sets it on API level 33 and later. + Os.setenv("TMPDIR", cacheDir.toString(), false) + + val pythonHome = extractAssets() + System.loadLibrary("main_activity") + redirectStdioToLogcat() + runPython(pythonHome.toString(), "main") + findViewById(R.id.tvHello).text = "Python complete" + } + + private fun extractAssets() : File { + val pythonHome = File(filesDir, "python") + if (pythonHome.exists() && !pythonHome.deleteRecursively()) { + throw RuntimeException("Failed to delete $pythonHome") + } + extractAssetDir("python", filesDir) + return pythonHome + } + + private fun extractAssetDir(path: String, targetDir: File) { + val names = assets.list(path) + ?: throw RuntimeException("Failed to list $path") + val targetSubdir = File(targetDir, path) + if (!targetSubdir.mkdirs()) { + throw RuntimeException("Failed to create $targetSubdir") + } + + for (name in names) { + val subPath = "$path/$name" + val input: InputStream + try { + input = assets.open(subPath) + } catch (e: FileNotFoundException) { + extractAssetDir(subPath, targetDir) + continue + } + input.use { + File(targetSubdir, name).outputStream().use { output -> + input.copyTo(output) + } + } + } + } + + private external fun redirectStdioToLogcat() + private external fun runPython(home: String, runModule: String) +} \ No newline at end of file diff --git a/Android/testbed/app/src/main/python/main.py b/Android/testbed/app/src/main/python/main.py new file mode 100644 index 000000000000..a1b6def34ede --- /dev/null +++ b/Android/testbed/app/src/main/python/main.py @@ -0,0 +1,17 @@ +import runpy +import signal +import sys + +# Some tests use SIGUSR1, but that's blocked by default in an Android app in +# order to make it available to `sigwait` in the "Signal Catcher" thread. That +# thread's functionality is only relevant to the JVM ("forcing GC (no HPROF) and +# profile save"), so disabling it should not weaken the tests. +signal.pthread_sigmask(signal.SIG_UNBLOCK, [signal.SIGUSR1]) + +# To run specific tests, or pass any other arguments to the test suite, edit +# this command line. +sys.argv[1:] = [ + "--use", "all,-cpu", + "--verbose3", +] +runpy.run_module("test") diff --git a/Android/testbed/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/Android/testbed/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..741d6580d60e05080e71e845240863f6b273333c GIT binary patch literal 3110 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 literal 0 Hc-jL100001 diff --git a/Android/testbed/app/src/main/res/layout/activity_main.xml b/Android/testbed/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000000..21398609ec9c --- /dev/null +++ b/Android/testbed/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/Android/testbed/app/src/main/res/values/strings.xml b/Android/testbed/app/src/main/res/values/strings.xml new file mode 100644 index 000000000000..352d2f9e885a --- /dev/null +++ b/Android/testbed/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Python testbed + \ No newline at end of file diff --git a/Android/testbed/build.gradle.kts b/Android/testbed/build.gradle.kts new file mode 100644 index 000000000000..53f4a67287fc --- /dev/null +++ b/Android/testbed/build.gradle.kts @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id("com.android.application") version "8.2.2" apply false + id("org.jetbrains.kotlin.android") version "1.9.22" apply false +} \ No newline at end of file diff --git a/Android/testbed/gradle.properties b/Android/testbed/gradle.properties new file mode 100644 index 000000000000..3c5031eb7d63 --- /dev/null +++ b/Android/testbed/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/Android/testbed/gradle/wrapper/gradle-wrapper.properties b/Android/testbed/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..2dc3339a3ef2 --- /dev/null +++ b/Android/testbed/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Feb 19 20:29:06 GMT 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Android/testbed/settings.gradle.kts b/Android/testbed/settings.gradle.kts new file mode 100644 index 000000000000..5e08773e0245 --- /dev/null +++ b/Android/testbed/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Python testbed" +include(":app") + \ No newline at end of file diff --git a/Misc/NEWS.d/next/Build/2024-04-14-19-35-35.gh-issue-116622.8lpX-7.rst b/Misc/NEWS.d/next/Build/2024-04-14-19-35-35.gh-issue-116622.8lpX-7.rst new file mode 100644 index 000000000000..c270e59cd54c --- /dev/null +++ b/Misc/NEWS.d/next/Build/2024-04-14-19-35-35.gh-issue-116622.8lpX-7.rst @@ -0,0 +1 @@ +A testbed project was added to run the test suite on Android. -- 2.47.3