Skip to content
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

Make it raise an error if an old ndk is used #1883

Merged
merged 5 commits into from
Jul 26, 2019
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
115 changes: 96 additions & 19 deletions pythonforandroid/recommendations.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,103 @@
MIN_NDK_VERSION = 17
MAX_NDK_VERSION = 17

RECOMMENDED_NDK_VERSION = '17c'
OLD_NDK_MESSAGE = 'Older NDKs may not be compatible with all p4a features.'
RECOMMENDED_NDK_VERSION = "17c"
NDK_DOWNLOAD_URL = "https://developer.android.com/ndk/downloads/"

# Important log messages
NEW_NDK_MESSAGE = 'Newer NDKs may not be fully supported by p4a.'
UNKNOWN_NDK_MESSAGE = (
'Could not determine NDK version, no source.properties in the NDK dir'
)
PARSE_ERROR_NDK_MESSAGE = (
'Could not parse $NDK_DIR/source.properties, not checking NDK version'
)
READ_ERROR_NDK_MESSAGE = (
'Unable to read the NDK version from the given directory {ndk_dir}'
)
ENSURE_RIGHT_NDK_MESSAGE = (
'Make sure your NDK version is greater than {min_supported}. If you get '
'build errors, download the recommended NDK {rec_version} from {ndk_url}'
)
NDK_LOWER_THAN_SUPPORTED_MESSAGE = (
'The minimum supported NDK version is {min_supported}. '
'You can download it from {ndk_url}'
)
UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE = (
'Asked to build for armeabi architecture with API '
'{req_ndk_api}, but API {max_ndk_api} or greater does not support armeabi'
)
CURRENT_NDK_VERSION_MESSAGE = (
'Found NDK version {ndk_version}'
)
RECOMMENDED_NDK_VERSION_MESSAGE = (
'Maximum recommended NDK version is {recommended_ndk_version}'
)


def check_ndk_version(ndk_dir):
# Check the NDK version against what is currently recommended
"""
Check the NDK version against what is currently recommended and raise an
exception of :class:`~pythonforandroid.util.BuildInterruptingException` in
case that the user tries to use an NDK lower than minimum supported,
specified via attribute `MIN_NDK_VERSION`.

.. versionchanged:: 2019.06.06.1.dev0
Added the ability to get android's NDK `letter version` and also
rewrote to raise an exception in case that an NDK version lower than
the minimum supported is detected.
"""
version = read_ndk_version(ndk_dir)

if version is None:
return # if we failed to read the version, just don't worry about it
warning(READ_ERROR_NDK_MESSAGE.format(ndk_dir=ndk_dir))
warning(
ENSURE_RIGHT_NDK_MESSAGE.format(
min_supported=MIN_NDK_VERSION,
rec_version=RECOMMENDED_NDK_VERSION,
ndk_url=NDK_DOWNLOAD_URL,
)
)
return

# create a dictionary which will describe the relationship of the android's
# NDK minor version with the `human readable` letter version, egs:
# Pkg.Revision = 17.1.4828580 => ndk-17b
# Pkg.Revision = 17.2.4988734 => ndk-17c
# Pkg.Revision = 19.0.5232133 => ndk-19 (No letter)
minor_to_letter = {0: ''}
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
)

info('Found NDK revision {}'.format(version))
info(CURRENT_NDK_VERSION_MESSAGE.format(ndk_version=string_version))

if major_version < MIN_NDK_VERSION:
warning('Minimum recommended NDK version is {}'.format(
RECOMMENDED_NDK_VERSION))
warning(OLD_NDK_MESSAGE)
raise BuildInterruptingException(
NDK_LOWER_THAN_SUPPORTED_MESSAGE.format(
min_supported=MIN_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL
),
instructions=(
'Please, go to the android NDK page ({ndk_url}) and download a'
' supported version.\n*** The currently recommended NDK'
' version is {rec_version} ***'.format(
ndk_url=NDK_DOWNLOAD_URL,
rec_version=RECOMMENDED_NDK_VERSION,
)
),
)
elif major_version > MAX_NDK_VERSION:
warning('Maximum recommended NDK version is {}'.format(
RECOMMENDED_NDK_VERSION))
warning(
RECOMMENDED_NDK_VERSION_MESSAGE.format(
recommended_ndk_version=RECOMMENDED_NDK_VERSION
)
)
warning(NEW_NDK_MESSAGE)


Expand All @@ -41,16 +115,14 @@ def read_ndk_version(ndk_dir):
with open(join(ndk_dir, 'source.properties')) as fileh:
ndk_data = fileh.read()
except IOError:
info('Could not determine NDK version, no source.properties '
'in the NDK dir')
info(UNKNOWN_NDK_MESSAGE)
return

for line in ndk_data.split('\n'):
if line.startswith('Pkg.Revision'):
break
else:
info('Could not parse $NDK_DIR/source.properties, not checking '
'NDK version')
info(PARSE_ERROR_NDK_MESSAGE)
return

# Line should have the form "Pkg.Revision = ..."
Expand Down Expand Up @@ -79,9 +151,9 @@ def check_target_api(api, arch):

if api >= ARMEABI_MAX_TARGET_API and arch == 'armeabi':
raise BuildInterruptingException(
'Asked to build for armeabi architecture with API '
'{}, but API {} or greater does not support armeabi'.format(
api, ARMEABI_MAX_TARGET_API),
UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE.format(
req_ndk_api=api, max_ndk_api=ARMEABI_MAX_TARGET_API
),
instructions='You probably want to build with --arch=armeabi-v7a instead')

if api < MIN_TARGET_API:
Expand All @@ -92,14 +164,19 @@ def check_target_api(api, arch):
MIN_NDK_API = 21
RECOMMENDED_NDK_API = 21
OLD_NDK_API_MESSAGE = ('NDK API less than {} is not supported'.format(MIN_NDK_API))
TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE = (
'Target NDK API is {ndk_api}, '
'higher than the target Android API {android_api}.'
)
Copy link
Member

Choose a reason for hiding this comment

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

Just wondering, why in p4a we don't have all the const on the top of the file? It's probably fine, it's just that I'm more used to do it this way, or even sometimes in a dedicated file



def check_ndk_api(ndk_api, android_api):
"""Warn if the user's NDK is too high or low."""
if ndk_api > android_api:
raise BuildInterruptingException(
'Target NDK API is {}, higher than the target Android API {}.'.format(
ndk_api, android_api),
TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE.format(
ndk_api=ndk_api, android_api=android_api
),
instructions=('The NDK API is a minimum supported API number and must be lower '
'than the target Android API'))

Expand Down
204 changes: 204 additions & 0 deletions tests/test_recommendations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import unittest
from os.path import join
from sys import version as py_version

try:
from unittest import mock
except ImportError:
# `Python 2` or lower than `Python 3.3` does not
# have the `unittest.mock` module built-in
import mock
from pythonforandroid.recommendations import (
check_ndk_api,
check_ndk_version,
check_target_api,
read_ndk_version,
MAX_NDK_VERSION,
RECOMMENDED_NDK_VERSION,
RECOMMENDED_TARGET_API,
MIN_NDK_API,
MIN_NDK_VERSION,
NDK_DOWNLOAD_URL,
ARMEABI_MAX_TARGET_API,
MIN_TARGET_API,
UNKNOWN_NDK_MESSAGE,
PARSE_ERROR_NDK_MESSAGE,
READ_ERROR_NDK_MESSAGE,
ENSURE_RIGHT_NDK_MESSAGE,
NDK_LOWER_THAN_SUPPORTED_MESSAGE,
UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE,
CURRENT_NDK_VERSION_MESSAGE,
RECOMMENDED_NDK_VERSION_MESSAGE,
TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE,
OLD_NDK_API_MESSAGE,
NEW_NDK_MESSAGE,
OLD_API_MESSAGE,
)
from pythonforandroid.util import BuildInterruptingException

running_in_py2 = int(py_version[0]) < 3


class TestRecommendations(unittest.TestCase):
"""
An inherited class of `unittest.TestCase`to test the module
:mod:`~pythonforandroid.recommendations`.
"""

def setUp(self):
self.ndk_dir = "/opt/android/android-ndk"

@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
@mock.patch("pythonforandroid.recommendations.read_ndk_version")
def test_check_ndk_version_greater_than_recommended(self, mock_read_ndk):
mock_read_ndk.return_value.version = [MAX_NDK_VERSION + 1, 0, 5232133]
with self.assertLogs(level="INFO") as cm:
check_ndk_version(self.ndk_dir)
mock_read_ndk.assert_called_once_with(self.ndk_dir)
self.assertEqual(
cm.output,
[
"INFO:p4a:[INFO]: {}".format(
CURRENT_NDK_VERSION_MESSAGE.format(
ndk_version=MAX_NDK_VERSION + 1
)
),
"WARNING:p4a:[WARNING]: {}".format(
RECOMMENDED_NDK_VERSION_MESSAGE.format(
recommended_ndk_version=RECOMMENDED_NDK_VERSION
)
),
"WARNING:p4a:[WARNING]: {}".format(NEW_NDK_MESSAGE),
],
)

@mock.patch("pythonforandroid.recommendations.read_ndk_version")
def test_check_ndk_version_lower_than_recommended(self, mock_read_ndk):
mock_read_ndk.return_value.version = [MIN_NDK_VERSION - 1, 0, 5232133]
with self.assertRaises(BuildInterruptingException) as e:
check_ndk_version(self.ndk_dir)
self.assertEqual(
e.exception.args[0],
NDK_LOWER_THAN_SUPPORTED_MESSAGE.format(
min_supported=MIN_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL
),
)
mock_read_ndk.assert_called_once_with(self.ndk_dir)

@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
def test_check_ndk_version_error(self):
"""
Test that a fake ndk dir give us two messages:
- first should be an `INFO` log
- second should be an `WARNING` log
"""
with self.assertLogs(level="INFO") as cm:
check_ndk_version(self.ndk_dir)
self.assertEqual(
cm.output,
[
"INFO:p4a:[INFO]: {}".format(UNKNOWN_NDK_MESSAGE),
"WARNING:p4a:[WARNING]: {}".format(
READ_ERROR_NDK_MESSAGE.format(ndk_dir=self.ndk_dir)
),
"WARNING:p4a:[WARNING]: {}".format(
ENSURE_RIGHT_NDK_MESSAGE.format(
min_supported=MIN_NDK_VERSION,
rec_version=RECOMMENDED_NDK_VERSION,
ndk_url=NDK_DOWNLOAD_URL,
)
),
],
)

@mock.patch("pythonforandroid.recommendations.open")
def test_read_ndk_version(self, mock_open_src_prop):
mock_open_src_prop.side_effect = [
mock.mock_open(
read_data="Pkg.Revision = 17.2.4988734"
).return_value
]
version = read_ndk_version(self.ndk_dir)
mock_open_src_prop.assert_called_once_with(
join(self.ndk_dir, "source.properties")
)
assert version == "17.2.4988734"
Copy link
Member

Choose a reason for hiding this comment

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

minor: inconsistent assert. I personally prefer this version, but it's not what you seem to be using in the rest of the file.
Same thing in test_read_ndk_version_error below.
But anyway we're far from being consistent in this with p4a currently. Don't know if we want to address or not


@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
@mock.patch("pythonforandroid.recommendations.open")
def test_read_ndk_version_error(self, mock_open_src_prop):
mock_open_src_prop.side_effect = [
mock.mock_open(read_data="").return_value
]
with self.assertLogs(level="INFO") as cm:
version = read_ndk_version(self.ndk_dir)
self.assertEqual(
cm.output,
["INFO:p4a:[INFO]: {}".format(PARSE_ERROR_NDK_MESSAGE)],
)
mock_open_src_prop.assert_called_once_with(
join(self.ndk_dir, "source.properties")
)
assert version is None

def test_check_target_api_error_arch_armeabi(self):

Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: why are we sometimes leaving a blank space after method definition and sometimes not?

with self.assertRaises(BuildInterruptingException) as e:
check_target_api(RECOMMENDED_TARGET_API, "armeabi")
self.assertEqual(
e.exception.args[0],
UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE.format(
req_ndk_api=RECOMMENDED_TARGET_API,
max_ndk_api=ARMEABI_MAX_TARGET_API,
),
)

@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
def test_check_target_api_warning_target_api(self):

with self.assertLogs(level="INFO") as cm:
check_target_api(MIN_TARGET_API - 1, MIN_TARGET_API)
self.assertEqual(
cm.output,
[
"WARNING:p4a:[WARNING]: Target API 25 < 26",
"WARNING:p4a:[WARNING]: {old_api_msg}".format(
old_api_msg=OLD_API_MESSAGE
),
],
)

def test_check_ndk_api_error_android_api(self):
"""
Given an `android api` greater than an `ndk_api`, we should get an
`BuildInterruptingException`.
"""
ndk_api = MIN_NDK_API + 1
android_api = MIN_NDK_API
with self.assertRaises(BuildInterruptingException) as e:
check_ndk_api(ndk_api, android_api)
self.assertEqual(
e.exception.args[0],
TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE.format(
ndk_api=ndk_api, android_api=android_api
),
)

@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
def test_check_ndk_api_warning_old_ndk(self):
"""
Given an `android api` lower than the supported by p4a, we should
get an `BuildInterruptingException`.
Copy link
Member

Choose a reason for hiding this comment

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

This comment seem to be a lie, right?

"""
ndk_api = MIN_NDK_API - 1
android_api = RECOMMENDED_TARGET_API
with self.assertLogs(level="INFO") as cm:
check_ndk_api(ndk_api, android_api)
self.assertEqual(
cm.output,
[
"WARNING:p4a:[WARNING]: {}".format(
OLD_NDK_API_MESSAGE.format(MIN_NDK_API)
)
],
)