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 235bc78599400e..1f5f7e57dc4859 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 5ed186e06e3951..f5f463ca116589 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 5c57e53c415d2b..0a1393e61ddb0e 100755 --- a/Android/android.py +++ b/Android/android.py @@ -7,8 +7,9 @@ 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 00000000000000..b9a7d611c943cf --- /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 00000000000000..42afabfd2abebf --- /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 00000000000000..7690d3fd86b2fd --- /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 00000000000000..2be8a82d426099 --- /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 00000000000000..1d5df9a73465b6 --- /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 00000000000000..73aba4164d000f --- /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 00000000000000..5a590d5d04e954 --- /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 00000000000000..a1b6def34ede81 --- /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 HcmV?d00001 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 00000000000000..21398609ec9c78 --- /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 00000000000000..352d2f9e885a2a --- /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 00000000000000..53f4a67287fcc5 --- /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 00000000000000..3c5031eb7d63f7 --- /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 00000000000000..2dc3339a3ef213 --- /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 00000000000000..5e08773e02450f --- /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 00000000000000..c270e59cd54c18 --- /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.