From f0dcf3f9c85c265086eaeb031b77246604b5b958 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Wed, 30 Sep 2026 22:26:58 +0100 Subject: [PATCH 1/7] Update edge-to-edge code from briefcase-android-gradle-template --- .../com/chaquo/python/utils/BaseActivity.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/demo/app/src/utils/java/com/chaquo/python/utils/BaseActivity.java b/demo/app/src/utils/java/com/chaquo/python/utils/BaseActivity.java index 1b0e22edbb..4627a442ec 100644 --- a/demo/app/src/utils/java/com/chaquo/python/utils/BaseActivity.java +++ b/demo/app/src/utils/java/com/chaquo/python/utils/BaseActivity.java @@ -51,20 +51,21 @@ public void setContentView(View view) { view, (v, insets) -> { // systemBars includes the status bar, navigation bar (which may be on - // the left or right on API level 28 and older), action bar, and display - // cutouts. - Insets systemBars = - insets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding( + // the left or right on API level 28 and older), and action bar. + Insets systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() + | WindowInsetsCompat.Type.displayCutout() + ); + ((ViewGroup.MarginLayoutParams) v.getLayoutParams()).setMargins( systemBars.left, systemBars.top, systemBars.right, systemBars.bottom ); - ViewGroup.MarginLayoutParams lp = + ViewGroup.MarginLayoutParams statusLp = (ViewGroup.MarginLayoutParams) statusBarBackground.getLayoutParams(); - lp.height = systemBars.top; - lp.leftMargin = systemBars.left; - lp.rightMargin = systemBars.right; + statusLp.height = systemBars.top; + statusLp.setMargins(systemBars.left, 0, systemBars.right, 0); statusBarBackground.requestLayout(); + return insets; } ); From ca5cc04260bc7ebf5930e27298c569bfca08dae4 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 1 Oct 2026 00:50:09 +0100 Subject: [PATCH 2/7] Make native libraries read-only after extraction --- .../src/main/python/java/android/importer.py | 11 ++++++++ .../chaquopy/test/android/test_import.py | 26 ++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/product/runtime/src/main/python/java/android/importer.py b/product/runtime/src/main/python/java/android/importer.py index cfae6e46a0..4e32785a16 100644 --- a/product/runtime/src/main/python/java/android/importer.py +++ b/product/runtime/src/main/python/java/android/importer.py @@ -851,6 +851,17 @@ def extract_if_changed(self, member, target_dir): extracted_filename = self.extract(member, target_dir) assert extracted_filename == out_filename, (extracted_filename, out_filename) os.utime(out_filename, (time.time(), timegm(member.date_time))) + + # In API level 37, native libraries loaded using System.load must be + # read-only. This doesn't affect lower-level APIs yet, but it's likely + # they'll tighten this in the future. + # + # In theory we could apply this to all files, but while writing to a virtual + # environment at runtime is certainly bad practice, it's usually possible, + # so there's probably someone out there doing it. + if re.search(r"\.so(\.|$)", basename(member.filename)): + os.chmod(out_filename, os.stat(out_filename).st_mode & ~0o222) + return out_filename def exists(self, path): diff --git a/product/runtime/src/test/python/chaquopy/test/android/test_import.py b/product/runtime/src/test/python/chaquopy/test/android/test_import.py index 61f9fd050a..7833475061 100644 --- a/product/runtime/src/test/python/chaquopy/test/android/test_import.py +++ b/product/runtime/src/test/python/chaquopy/test/android/test_import.py @@ -13,6 +13,8 @@ import pkgutil import re from shutil import rmtree +from stat import S_IMODE + import sys from traceback import format_exc from unittest import skipIf @@ -266,8 +268,8 @@ def test_non_package_data(self): ("non_package_data/subdir", "subdirectory")]: with self.subTest(dir_name=dir_name): extracted_dir = asset_path(APP_ZIP, dir_name) - self.assertCountEqual( - [entry.name for entry in os.scandir(extracted_dir) if entry.is_file()], + self.check_non_package_data( + extracted_dir, [ # Should extract everything except .py and .pyc files. "libnon_package_data.so.1", @@ -286,12 +288,18 @@ def test_non_package_data(self): # The chaquopy directory is also treated as non-package data. for dir_name, expected in [ - ("chaquopy", ["lib"]), + ("chaquopy", []), ("chaquopy/lib", ["libc++_shared.so"]), ]: - self.assertCountEqual( - os.listdir(asset_path(REQS_ABI_ZIP, dir_name)), expected - ) + self.check_non_package_data(asset_path(REQS_ABI_ZIP, dir_name), expected) + + def check_non_package_data(self, extracted_dir, expected): + self.assertCountEqual( + [entry.name for entry in os.scandir(extracted_dir) if entry.is_file()], + expected, + ) + for name in expected: + self.check_readonly(f"{extracted_dir}/{name}"), def test_package_data(self): # App ZIP @@ -339,6 +347,7 @@ def check_extract_if_changed(self, mod, cache_filename): os.remove(cache_filename) mod = self.clean_reload(mod) self.assertPredicate(exists, cache_filename) + self.check_readonly(cache_filename) # An unchanged file should not be extracted again. with self.assertNotModifies(cache_filename): @@ -351,6 +360,11 @@ def check_extract_if_changed(self, mod, cache_filename): self.clean_reload(mod) self.assertEqual(original_mtime, os.stat(cache_filename).st_mtime) + def check_readonly(self, path): + actual = (S_IMODE(os.stat(path).st_mode) & 0o222) == 0 + expected = bool(re.search(r"\.so(\.|$)", basename(path))) + self.assertEqual(actual, expected, path) + def test_extract_packages(self): self.check_extract_packages("ep_alpha", []) # Not extracted self.check_extract_packages("ep_bravo", [ # Top-level package extracted From 35d707a9453fe911c17ec99a19a2445da60acd7d Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 1 Oct 2026 00:58:50 +0100 Subject: [PATCH 3/7] Use S_IMODE to ensure a valid input to chmod --- product/runtime/src/main/python/java/android/importer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/product/runtime/src/main/python/java/android/importer.py b/product/runtime/src/main/python/java/android/importer.py index 4e32785a16..571bfb6361 100644 --- a/product/runtime/src/main/python/java/android/importer.py +++ b/product/runtime/src/main/python/java/android/importer.py @@ -13,6 +13,7 @@ import re from shutil import copyfileobj, rmtree import site +from stat import S_IMODE import sys from tempfile import NamedTemporaryFile import time @@ -860,7 +861,7 @@ def extract_if_changed(self, member, target_dir): # environment at runtime is certainly bad practice, it's usually possible, # so there's probably someone out there doing it. if re.search(r"\.so(\.|$)", basename(member.filename)): - os.chmod(out_filename, os.stat(out_filename).st_mode & ~0o222) + os.chmod(out_filename, S_IMODE(os.stat(out_filename).st_mode) & ~0o222) return out_filename From d1a60f97ed65e10b722ad3c6de618ecb395fa635 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 1 Oct 2026 02:17:18 +0100 Subject: [PATCH 4/7] Set mtime after making files read-only --- product/runtime/src/main/python/java/android/importer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/product/runtime/src/main/python/java/android/importer.py b/product/runtime/src/main/python/java/android/importer.py index 571bfb6361..2fce7e5a2a 100644 --- a/product/runtime/src/main/python/java/android/importer.py +++ b/product/runtime/src/main/python/java/android/importer.py @@ -851,7 +851,6 @@ def extract_if_changed(self, member, target_dir): if need_extract: extracted_filename = self.extract(member, target_dir) assert extracted_filename == out_filename, (extracted_filename, out_filename) - os.utime(out_filename, (time.time(), timegm(member.date_time))) # In API level 37, native libraries loaded using System.load must be # read-only. This doesn't affect lower-level APIs yet, but it's likely @@ -863,6 +862,9 @@ def extract_if_changed(self, member, target_dir): if re.search(r"\.so(\.|$)", basename(member.filename)): os.chmod(out_filename, S_IMODE(os.stat(out_filename).st_mode) & ~0o222) + # Do this last to indicate that the file is in its final state. + os.utime(out_filename, (time.time(), timegm(member.date_time))) + return out_filename def exists(self, path): From ac246129ca365724c6679b0d657466e220272cdc Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 1 Oct 2026 16:09:08 +0100 Subject: [PATCH 5/7] Remove unnecessary locale tests: locale.getlocale() returns (None, None) in API level 37, but the point was to check the encoding, which is already covered by the other tests. --- .../src/test/python/chaquopy/test/android/test_stdlib.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/product/runtime/src/test/python/chaquopy/test/android/test_stdlib.py b/product/runtime/src/test/python/chaquopy/test/android/test_stdlib.py index c6a392efda..7b10b938c8 100644 --- a/product/runtime/src/test/python/chaquopy/test/android/test_stdlib.py +++ b/product/runtime/src/test/python/chaquopy/test/android/test_stdlib.py @@ -74,10 +74,6 @@ def test_lib2to3(self): def test_locale(self): import locale - self.assertEqual("UTF-8", locale.getlocale()[1]) - with catch_warnings(): - filterwarnings("default", category=DeprecationWarning) - self.assertEqual("UTF-8", locale.getdefaultlocale()[1]) self.assertEqual("utf-8", # Became lower-case in Python 3.11. locale.getpreferredencoding().lower()) self.assertEqual("utf-8", sys.getdefaultencoding()) From 0d24d91a0b88a77f5e1fd8c465b9ef2ea5ae2c21 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Fri, 2 Oct 2026 01:17:22 +0100 Subject: [PATCH 6/7] Update to targetSdk 37 --- demo/.idea/gradle.xml | 2 +- demo/app/build.gradle.kts | 4 ++-- .../src/main/java/com/chaquo/python/internal/Common.java | 2 +- server/pypi/pkgtest/app/build.gradle | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/.idea/gradle.xml b/demo/.idea/gradle.xml index 7b3006b6ee..5f67c0481e 100644 --- a/demo/.idea/gradle.xml +++ b/demo/.idea/gradle.xml @@ -11,9 +11,9 @@ - diff --git a/demo/app/build.gradle.kts b/demo/app/build.gradle.kts index c812a02dc0..24056591bf 100644 --- a/demo/app/build.gradle.kts +++ b/demo/app/build.gradle.kts @@ -22,12 +22,12 @@ afterEvaluate { android { namespace = "com.chaquo.python.demo" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = "com.chaquo.python.demo3" minSdk = 24 - targetSdk = 36 + targetSdk = 37 val plugins = buildscript.configurations.getByName("classpath") .resolvedConfiguration.resolvedArtifacts.map { diff --git a/product/buildSrc/src/main/java/com/chaquo/python/internal/Common.java b/product/buildSrc/src/main/java/com/chaquo/python/internal/Common.java index 76e7fbf171..8616f1f069 100644 --- a/product/buildSrc/src/main/java/com/chaquo/python/internal/Common.java +++ b/product/buildSrc/src/main/java/com/chaquo/python/internal/Common.java @@ -13,7 +13,7 @@ public class Common { // This should match api_level in target/android-env.sh. public static final int MIN_SDK_VERSION = 24; - public static final int COMPILE_SDK_VERSION = 36; + public static final int COMPILE_SDK_VERSION = 37; public static final Map PYTHON_VERSIONS = new LinkedHashMap<>(); static { diff --git a/server/pypi/pkgtest/app/build.gradle b/server/pypi/pkgtest/app/build.gradle index bc91592c03..8460a5fd44 100644 --- a/server/pypi/pkgtest/app/build.gradle +++ b/server/pypi/pkgtest/app/build.gradle @@ -32,12 +32,12 @@ ext.TEST_PACKAGES = [ android { namespace "com.chaquo.python.pkgtest3" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId "com.chaquo.python.pkgtest3" minSdk = 24 - targetSdk = 36 + targetSdk = 37 String pluginVersion = null for (art in rootProject.buildscript.configurations.getByName("classpath") From 764b34828a850ce3f8f8047829414e48b4c53875 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Fri, 2 Oct 2026 01:29:58 +0100 Subject: [PATCH 7/7] Add decimal place to COMPILE_SDK_VERSION --- .../src/main/java/com/chaquo/python/internal/Common.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product/buildSrc/src/main/java/com/chaquo/python/internal/Common.java b/product/buildSrc/src/main/java/com/chaquo/python/internal/Common.java index 8616f1f069..cb55a51c71 100644 --- a/product/buildSrc/src/main/java/com/chaquo/python/internal/Common.java +++ b/product/buildSrc/src/main/java/com/chaquo/python/internal/Common.java @@ -13,7 +13,7 @@ public class Common { // This should match api_level in target/android-env.sh. public static final int MIN_SDK_VERSION = 24; - public static final int COMPILE_SDK_VERSION = 37; + public static final String COMPILE_SDK_VERSION = "37.0"; public static final Map PYTHON_VERSIONS = new LinkedHashMap<>(); static {