Skip to content

Remove distutils usage, as is not available anymore on Python 3.12 #2912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pythonforandroid/archs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from distutils.spawn import find_executable
from os import environ
from os.path import join
from multiprocessing import cpu_count
import shutil

from pythonforandroid.recipe import Recipe
from pythonforandroid.util import BuildInterruptingException, build_platform
Expand Down Expand Up @@ -172,7 +172,7 @@ def get_env(self, with_flags_in_cc=True):

# Compiler: `CC` and `CXX` (and make sure that the compiler exists)
env['PATH'] = self.ctx.env['PATH']
cc = find_executable(self.clang_exe, path=env['PATH'])
cc = shutil.which(self.clang_exe, path=env['PATH'])
if cc is None:
print('Searching path are: {!r}'.format(env['PATH']))
raise BuildInterruptingException(
Expand Down
7 changes: 2 additions & 5 deletions pythonforandroid/bootstraps/common/build/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
import tempfile
import time

from distutils.version import LooseVersion
from fnmatch import fnmatch
import jinja2

from pythonforandroid.util import rmdir, ensure_dir
from pythonforandroid.util import rmdir, ensure_dir, max_build_tool_version


def get_dist_info_for(key, error_if_missing=True):
Expand Down Expand Up @@ -512,9 +511,7 @@ def make_package(args):
# Try to build with the newest available build tools
ignored = {".DS_Store", ".ds_store"}
build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
build_tools_versions = sorted(build_tools_versions,
key=LooseVersion)
build_tools_version = build_tools_versions[-1]
build_tools_version = max_build_tool_version(build_tools_versions)

# Folder name for launcher (used by SDL2 bootstrap)
url_scheme = 'kivy'
Expand Down
7 changes: 5 additions & 2 deletions pythonforandroid/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse

import packaging.version

from pythonforandroid.logger import (
logger, info, warning, debug, shprint, info_main)
from pythonforandroid.util import (
Expand Down Expand Up @@ -1145,8 +1148,8 @@ def link_root(self):

@property
def major_minor_version_string(self):
from distutils.version import LooseVersion
return '.'.join([str(v) for v in LooseVersion(self.version).version[:2]])
parsed_version = packaging.version.parse(self.version)
return f"{parsed_version.major}.{parsed_version.minor}"

def create_python_bundle(self, dirn, arch):
"""
Expand Down
27 changes: 12 additions & 15 deletions pythonforandroid/recommendations.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Simple functions for checking dependency versions."""

import sys
from distutils.version import LooseVersion
from os.path import join

import packaging.version

from pythonforandroid.logger import info, warning
from pythonforandroid.util import BuildInterruptingException

Expand Down Expand Up @@ -59,9 +60,9 @@ def check_ndk_version(ndk_dir):
rewrote to raise an exception in case that an NDK version lower than
the minimum supported is detected.
"""
version = read_ndk_version(ndk_dir)
ndk_version = read_ndk_version(ndk_dir)

if version is None:
if ndk_version is None:
warning(READ_ERROR_NDK_MESSAGE.format(ndk_dir=ndk_dir))
warning(
ENSURE_RIGHT_NDK_MESSAGE.format(
Expand All @@ -81,16 +82,11 @@ def check_ndk_version(ndk_dir):
minor_to_letter.update(
{n + 1: chr(i) for n, i in enumerate(range(ord('b'), ord('b') + 25))}
)

major_version = version.version[0]
letter_version = minor_to_letter[version.version[1]]
string_version = '{major_version}{letter_version}'.format(
major_version=major_version, letter_version=letter_version
)
string_version = f"{ndk_version.major}{minor_to_letter[ndk_version.minor]}"

info(CURRENT_NDK_VERSION_MESSAGE.format(ndk_version=string_version))

if major_version < MIN_NDK_VERSION:
if ndk_version.major < MIN_NDK_VERSION:
raise BuildInterruptingException(
NDK_LOWER_THAN_SUPPORTED_MESSAGE.format(
min_supported=MIN_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL
Expand All @@ -104,7 +100,7 @@ def check_ndk_version(ndk_dir):
)
),
)
elif major_version > MAX_NDK_VERSION:
elif ndk_version.major > MAX_NDK_VERSION:
warning(
RECOMMENDED_NDK_VERSION_MESSAGE.format(
recommended_ndk_version=RECOMMENDED_NDK_VERSION
Expand All @@ -130,9 +126,9 @@ def read_ndk_version(ndk_dir):
return

# Line should have the form "Pkg.Revision = ..."
ndk_version = LooseVersion(line.split('=')[-1].strip())
unparsed_ndk_version = line.split('=')[-1].strip()

return ndk_version
return packaging.version.parse(unparsed_ndk_version)


MIN_TARGET_API = 30
Expand Down Expand Up @@ -191,8 +187,9 @@ def check_ndk_api(ndk_api, android_api):

MIN_PYTHON_MAJOR_VERSION = 3
MIN_PYTHON_MINOR_VERSION = 6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope, but I'm wondering, is 3.6 still the minor recommended version?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd encourage upgrading to Python 3.8 as a minimum, but I see that as a project decision.

  • Python 3.6 and Python 3.7 are end of life. Python 3.8 has just under a year left of security support.

  • The latest released version of Buildozer claims to support back to 3.6. The in-dev version is 3.8 minimum.

  • The latest released version of Kivy claims to support back to 3.7. The in-dev version is 3.7 minimum.

  • p4a's setup.py advertises 3.7, not 3.6.

My attitude is if you don't test it, you aren't really supporting it, so whatever the decision, please ensure the CI tests cover what setup.py advertises.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About this specific issue (the one suggested by @AndreMiras ):
I will (immediately, so we do not miss it), open a new issue, and target with a separate proper PR.

Generally, about the minimum Python version:

  • kivy/kivy: We still need to include 3.7 even if reached EOL, at least for 2.3.0 as piwheels still requires 3.7 wheels (due to debian buster). Then, I will check out with piwheels maintainer, if there's a chance to avoid distributing wheels for debian buster.
  • Same applies for kivy/pyobjus, kivy/pyjnius and kivy/plyer, as are quite tied with kivy/kivy.
  • For kivy/python-for-android, kivy/kivy-ios and kivy/buildozer: what are we waiting to set the minimum version to 3.8 ? 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great insight, thanks @misl6
And yeah of course it makes sense to address that in dedicated PR, it caught my attention in this PR, but it's totally out of this scope

MIN_PYTHON_VERSION = LooseVersion('{major}.{minor}'.format(major=MIN_PYTHON_MAJOR_VERSION,
minor=MIN_PYTHON_MINOR_VERSION))
MIN_PYTHON_VERSION = packaging.version.Version(
f"{MIN_PYTHON_MAJOR_VERSION}.{MIN_PYTHON_MINOR_VERSION}"
)
PY2_ERROR_TEXT = (
'python-for-android no longer supports running under Python 2. Either upgrade to '
'Python {min_version} or higher (recommended), or revert to python-for-android 2019.07.08.'
Expand Down
22 changes: 8 additions & 14 deletions pythonforandroid/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from pythonforandroid.checkdependencies import check
check()

from packaging.version import Version, InvalidVersion
from packaging.version import Version
import sh

from pythonforandroid import __version__
Expand All @@ -41,7 +41,12 @@
from pythonforandroid.recommendations import (
RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API, print_recommendations)
from pythonforandroid.util import (
current_directory, BuildInterruptingException, load_source, rmdir)
current_directory,
BuildInterruptingException,
load_source,
rmdir,
max_build_tool_version,
)

user_dir = dirname(realpath(os.path.curdir))
toolchain_dir = dirname(__file__)
Expand Down Expand Up @@ -1009,18 +1014,7 @@ def _build_package(self, args, package_type):
self.hook("before_apk_assemble")
build_tools_versions = os.listdir(join(ctx.sdk_dir,
'build-tools'))

def sort_key(version_text):
try:
# Historically, Android build release candidates have had
# spaces in the version number.
return Version(version_text.replace(" ", ""))
except InvalidVersion:
# Put badly named versions at worst position.
return Version("0")

build_tools_versions.sort(key=sort_key)
build_tools_version = build_tools_versions[-1]
build_tools_version = max_build_tool_version(build_tools_versions)
info(('Detected highest available build tools '
'version to be {}').format(build_tools_version))

Expand Down
35 changes: 35 additions & 0 deletions pythonforandroid/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import shutil
from tempfile import mkdtemp

import packaging.version

from pythonforandroid.logger import (logger, Err_Fore, error, info)

LOGGER = logging.getLogger("p4a.util")
Expand Down Expand Up @@ -128,3 +130,36 @@ def move(source, destination):

def touch(filename):
Path(filename).touch()


def build_tools_version_sort_key(
version_string: str,
) -> packaging.version.Version:
"""
Returns a packaging.version.Version object for comparison purposes.
It includes canonicalization of the version string to allow for
comparison of versions with spaces in them (historically, RC candidates)

If the version string is invalid, it returns a version object with
version 0, which will be sorted at worst position.
"""

try:
# Historically, Android build release candidates have had
# spaces in the version number.
return packaging.version.Version(version_string.replace(" ", ""))
except packaging.version.InvalidVersion:
# Put badly named versions at worst position.
return packaging.version.Version("0")


def max_build_tool_version(
build_tools_versions: list,
) -> str:
"""
Returns the maximum build tools version from a list of build tools
versions. It uses the :meth:`build_tools_version_sort_key` function to
canonicalize the version strings and then returns the maximum version.
"""

return max(build_tools_versions, key=build_tools_version_sort_key)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
install_reqs = [
'appdirs', 'colorama>=0.3.3', 'jinja2',
'sh>=1.10, <2.0; sys_platform!="win32"',
'build', 'toml', 'packaging',
'build', 'toml', 'packaging', 'setuptools'
]
# (build and toml are used by pythonpackage.py)

Expand Down
4 changes: 2 additions & 2 deletions testapps/on_device_unit_tests/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@

import os
import sys
from distutils.core import setup
from setuptools import find_packages

from setuptools import setup, find_packages

# define a basic test app, which can be override passing the proper args to cli
options = {
Expand Down
4 changes: 1 addition & 3 deletions testapps/setup_testapp_python3_sqlite_openssl.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

from distutils.core import setup
from setuptools import find_packages
from setuptools import setup, find_packages

options = {'apk': {'requirements': 'requests,peewee,sdl2,pyjnius,kivy,python3',
'android-api': 27,
Expand Down
4 changes: 1 addition & 3 deletions testapps/setup_vispy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

from distutils.core import setup
from setuptools import find_packages
from setuptools import setup, find_packages

options = {'apk': {'debug': None,
'requirements': 'python3,vispy',
Expand Down
3 changes: 1 addition & 2 deletions testapps/testlauncher_setup/sdl2.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from distutils.core import setup
from setuptools import find_packages
from setuptools import setup

options = {'apk': {'debug': None,
'bootstrap': 'sdl2',
Expand Down
3 changes: 1 addition & 2 deletions testapps/testlauncherreboot_setup/sdl2.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@

# pylint: disable=import-error,no-name-in-module
from subprocess import Popen
from distutils.core import setup
from os import listdir
from os.path import join, dirname, abspath, exists
from pprint import pprint
from setuptools import find_packages
from setuptools import setup, find_packages

ROOT = dirname(abspath(__file__))
LAUNCHER = join(ROOT, 'launcherapp')
Expand Down
24 changes: 12 additions & 12 deletions tests/recipes/recipe_lib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ def __init__(self, *args, **kwargs):

@mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices")
@mock.patch("pythonforandroid.build.ensure_dir")
@mock.patch("pythonforandroid.archs.find_executable")
@mock.patch("shutil.which")
def test_get_recipe_env(
self,
mock_find_executable,
mock_shutil_which,
mock_ensure_dir,
mock_check_recipe_choices,
):
"""
Test that get_recipe_env contains some expected arch flags and that
some internal methods has been called.
"""
mock_find_executable.return_value = self.expected_compiler.format(
mock_shutil_which.return_value = self.expected_compiler.format(
android_ndk=self.ctx._ndk_dir, system=system().lower()
)
mock_check_recipe_choices.return_value = sorted(
Expand All @@ -67,19 +67,19 @@ def test_get_recipe_env(

# make sure that the mocked methods are actually called
mock_ensure_dir.assert_called()
mock_find_executable.assert_called()
mock_shutil_which.assert_called()
mock_check_recipe_choices.assert_called()

@mock.patch("pythonforandroid.util.chdir")
@mock.patch("pythonforandroid.build.ensure_dir")
@mock.patch("pythonforandroid.archs.find_executable")
@mock.patch("shutil.which")
def test_build_arch(
self,
mock_find_executable,
mock_shutil_which,
mock_ensure_dir,
mock_current_directory,
):
mock_find_executable.return_value = self.expected_compiler.format(
mock_shutil_which.return_value = self.expected_compiler.format(
android_ndk=self.ctx._ndk_dir, system=system().lower()
)

Expand All @@ -101,7 +101,7 @@ def test_build_arch(
mock_make.assert_called()
mock_ensure_dir.assert_called()
mock_current_directory.assert_called()
mock_find_executable.assert_called()
mock_shutil_which.assert_called()


class BaseTestForCmakeRecipe(BaseTestForMakeRecipe):
Expand All @@ -116,14 +116,14 @@ class BaseTestForCmakeRecipe(BaseTestForMakeRecipe):

@mock.patch("pythonforandroid.util.chdir")
@mock.patch("pythonforandroid.build.ensure_dir")
@mock.patch("pythonforandroid.archs.find_executable")
@mock.patch("shutil.which")
def test_build_arch(
self,
mock_find_executable,
mock_shutil_which,
mock_ensure_dir,
mock_current_directory,
):
mock_find_executable.return_value = self.expected_compiler.format(
mock_shutil_which.return_value = self.expected_compiler.format(
android_ndk=self.ctx._ndk_dir, system=system().lower()
)

Expand All @@ -141,4 +141,4 @@ def test_build_arch(
mock_make.assert_called()
mock_ensure_dir.assert_called()
mock_current_directory.assert_called()
mock_find_executable.assert_called()
mock_shutil_which.assert_called()
12 changes: 6 additions & 6 deletions tests/recipes/test_icu.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ def test_get_recipe_dir(self):
@mock.patch("pythonforandroid.bootstrap.sh.Command")
@mock.patch("pythonforandroid.recipes.icu.sh.make")
@mock.patch("pythonforandroid.build.ensure_dir")
@mock.patch("pythonforandroid.archs.find_executable")
@mock.patch("shutil.which")
def test_build_arch(
self,
mock_find_executable,
mock_shutil_which,
mock_ensure_dir,
mock_sh_make,
mock_sh_command,
mock_chdir,
mock_makedirs,
):
mock_find_executable.return_value = os.path.join(
mock_shutil_which.return_value = os.path.join(
self.ctx._ndk_dir,
f"toolchains/llvm/prebuilt/{self.ctx.ndk.host_tag}/bin/clang",
)
Expand Down Expand Up @@ -89,10 +89,10 @@ def test_build_arch(
)
mock_makedirs.assert_called()

mock_find_executable.assert_called_once()
mock_shutil_which.assert_called_once()
self.assertEqual(
mock_find_executable.call_args[0][0],
mock_find_executable.return_value,
mock_shutil_which.call_args[0][0],
mock_shutil_which.return_value,
)

@mock.patch("pythonforandroid.recipes.icu.sh.cp")
Expand Down
Loading