Skip to content

Commit 13c3b51

Browse files
authored
Merge pull request kivy#1883 from opacam/feature-ndk-check
Make it raise an error if an old ndk is used
2 parents e102f59 + 6688562 commit 13c3b51

File tree

2 files changed

+300
-19
lines changed

2 files changed

+300
-19
lines changed

pythonforandroid/recommendations.py

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,103 @@
99
MIN_NDK_VERSION = 17
1010
MAX_NDK_VERSION = 17
1111

12-
RECOMMENDED_NDK_VERSION = '17c'
13-
OLD_NDK_MESSAGE = 'Older NDKs may not be compatible with all p4a features.'
12+
RECOMMENDED_NDK_VERSION = "17c"
13+
NDK_DOWNLOAD_URL = "https://developer.android.com/ndk/downloads/"
14+
15+
# Important log messages
1416
NEW_NDK_MESSAGE = 'Newer NDKs may not be fully supported by p4a.'
17+
UNKNOWN_NDK_MESSAGE = (
18+
'Could not determine NDK version, no source.properties in the NDK dir'
19+
)
20+
PARSE_ERROR_NDK_MESSAGE = (
21+
'Could not parse $NDK_DIR/source.properties, not checking NDK version'
22+
)
23+
READ_ERROR_NDK_MESSAGE = (
24+
'Unable to read the NDK version from the given directory {ndk_dir}'
25+
)
26+
ENSURE_RIGHT_NDK_MESSAGE = (
27+
'Make sure your NDK version is greater than {min_supported}. If you get '
28+
'build errors, download the recommended NDK {rec_version} from {ndk_url}'
29+
)
30+
NDK_LOWER_THAN_SUPPORTED_MESSAGE = (
31+
'The minimum supported NDK version is {min_supported}. '
32+
'You can download it from {ndk_url}'
33+
)
34+
UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE = (
35+
'Asked to build for armeabi architecture with API '
36+
'{req_ndk_api}, but API {max_ndk_api} or greater does not support armeabi'
37+
)
38+
CURRENT_NDK_VERSION_MESSAGE = (
39+
'Found NDK version {ndk_version}'
40+
)
41+
RECOMMENDED_NDK_VERSION_MESSAGE = (
42+
'Maximum recommended NDK version is {recommended_ndk_version}'
43+
)
1544

1645

1746
def check_ndk_version(ndk_dir):
18-
# Check the NDK version against what is currently recommended
47+
"""
48+
Check the NDK version against what is currently recommended and raise an
49+
exception of :class:`~pythonforandroid.util.BuildInterruptingException` in
50+
case that the user tries to use an NDK lower than minimum supported,
51+
specified via attribute `MIN_NDK_VERSION`.
52+
53+
.. versionchanged:: 2019.06.06.1.dev0
54+
Added the ability to get android's NDK `letter version` and also
55+
rewrote to raise an exception in case that an NDK version lower than
56+
the minimum supported is detected.
57+
"""
1958
version = read_ndk_version(ndk_dir)
2059

2160
if version is None:
22-
return # if we failed to read the version, just don't worry about it
61+
warning(READ_ERROR_NDK_MESSAGE.format(ndk_dir=ndk_dir))
62+
warning(
63+
ENSURE_RIGHT_NDK_MESSAGE.format(
64+
min_supported=MIN_NDK_VERSION,
65+
rec_version=RECOMMENDED_NDK_VERSION,
66+
ndk_url=NDK_DOWNLOAD_URL,
67+
)
68+
)
69+
return
70+
71+
# create a dictionary which will describe the relationship of the android's
72+
# NDK minor version with the `human readable` letter version, egs:
73+
# Pkg.Revision = 17.1.4828580 => ndk-17b
74+
# Pkg.Revision = 17.2.4988734 => ndk-17c
75+
# Pkg.Revision = 19.0.5232133 => ndk-19 (No letter)
76+
minor_to_letter = {0: ''}
77+
minor_to_letter.update(
78+
{n + 1: chr(i) for n, i in enumerate(range(ord('b'), ord('b') + 25))}
79+
)
2380

2481
major_version = version.version[0]
82+
letter_version = minor_to_letter[version.version[1]]
83+
string_version = '{major_version}{letter_version}'.format(
84+
major_version=major_version, letter_version=letter_version
85+
)
2586

26-
info('Found NDK revision {}'.format(version))
87+
info(CURRENT_NDK_VERSION_MESSAGE.format(ndk_version=string_version))
2788

2889
if major_version < MIN_NDK_VERSION:
29-
warning('Minimum recommended NDK version is {}'.format(
30-
RECOMMENDED_NDK_VERSION))
31-
warning(OLD_NDK_MESSAGE)
90+
raise BuildInterruptingException(
91+
NDK_LOWER_THAN_SUPPORTED_MESSAGE.format(
92+
min_supported=MIN_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL
93+
),
94+
instructions=(
95+
'Please, go to the android NDK page ({ndk_url}) and download a'
96+
' supported version.\n*** The currently recommended NDK'
97+
' version is {rec_version} ***'.format(
98+
ndk_url=NDK_DOWNLOAD_URL,
99+
rec_version=RECOMMENDED_NDK_VERSION,
100+
)
101+
),
102+
)
32103
elif major_version > MAX_NDK_VERSION:
33-
warning('Maximum recommended NDK version is {}'.format(
34-
RECOMMENDED_NDK_VERSION))
104+
warning(
105+
RECOMMENDED_NDK_VERSION_MESSAGE.format(
106+
recommended_ndk_version=RECOMMENDED_NDK_VERSION
107+
)
108+
)
35109
warning(NEW_NDK_MESSAGE)
36110

37111

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

48121
for line in ndk_data.split('\n'):
49122
if line.startswith('Pkg.Revision'):
50123
break
51124
else:
52-
info('Could not parse $NDK_DIR/source.properties, not checking '
53-
'NDK version')
125+
info(PARSE_ERROR_NDK_MESSAGE)
54126
return
55127

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

80152
if api >= ARMEABI_MAX_TARGET_API and arch == 'armeabi':
81153
raise BuildInterruptingException(
82-
'Asked to build for armeabi architecture with API '
83-
'{}, but API {} or greater does not support armeabi'.format(
84-
api, ARMEABI_MAX_TARGET_API),
154+
UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE.format(
155+
req_ndk_api=api, max_ndk_api=ARMEABI_MAX_TARGET_API
156+
),
85157
instructions='You probably want to build with --arch=armeabi-v7a instead')
86158

87159
if api < MIN_TARGET_API:
@@ -92,14 +164,19 @@ def check_target_api(api, arch):
92164
MIN_NDK_API = 21
93165
RECOMMENDED_NDK_API = 21
94166
OLD_NDK_API_MESSAGE = ('NDK API less than {} is not supported'.format(MIN_NDK_API))
167+
TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE = (
168+
'Target NDK API is {ndk_api}, '
169+
'higher than the target Android API {android_api}.'
170+
)
95171

96172

97173
def check_ndk_api(ndk_api, android_api):
98174
"""Warn if the user's NDK is too high or low."""
99175
if ndk_api > android_api:
100176
raise BuildInterruptingException(
101-
'Target NDK API is {}, higher than the target Android API {}.'.format(
102-
ndk_api, android_api),
177+
TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE.format(
178+
ndk_api=ndk_api, android_api=android_api
179+
),
103180
instructions=('The NDK API is a minimum supported API number and must be lower '
104181
'than the target Android API'))
105182

tests/test_recommendations.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import unittest
2+
from os.path import join
3+
from sys import version as py_version
4+
5+
try:
6+
from unittest import mock
7+
except ImportError:
8+
# `Python 2` or lower than `Python 3.3` does not
9+
# have the `unittest.mock` module built-in
10+
import mock
11+
from pythonforandroid.recommendations import (
12+
check_ndk_api,
13+
check_ndk_version,
14+
check_target_api,
15+
read_ndk_version,
16+
MAX_NDK_VERSION,
17+
RECOMMENDED_NDK_VERSION,
18+
RECOMMENDED_TARGET_API,
19+
MIN_NDK_API,
20+
MIN_NDK_VERSION,
21+
NDK_DOWNLOAD_URL,
22+
ARMEABI_MAX_TARGET_API,
23+
MIN_TARGET_API,
24+
UNKNOWN_NDK_MESSAGE,
25+
PARSE_ERROR_NDK_MESSAGE,
26+
READ_ERROR_NDK_MESSAGE,
27+
ENSURE_RIGHT_NDK_MESSAGE,
28+
NDK_LOWER_THAN_SUPPORTED_MESSAGE,
29+
UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE,
30+
CURRENT_NDK_VERSION_MESSAGE,
31+
RECOMMENDED_NDK_VERSION_MESSAGE,
32+
TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE,
33+
OLD_NDK_API_MESSAGE,
34+
NEW_NDK_MESSAGE,
35+
OLD_API_MESSAGE,
36+
)
37+
from pythonforandroid.util import BuildInterruptingException
38+
39+
running_in_py2 = int(py_version[0]) < 3
40+
41+
42+
class TestRecommendations(unittest.TestCase):
43+
"""
44+
An inherited class of `unittest.TestCase`to test the module
45+
:mod:`~pythonforandroid.recommendations`.
46+
"""
47+
48+
def setUp(self):
49+
self.ndk_dir = "/opt/android/android-ndk"
50+
51+
@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
52+
@mock.patch("pythonforandroid.recommendations.read_ndk_version")
53+
def test_check_ndk_version_greater_than_recommended(self, mock_read_ndk):
54+
mock_read_ndk.return_value.version = [MAX_NDK_VERSION + 1, 0, 5232133]
55+
with self.assertLogs(level="INFO") as cm:
56+
check_ndk_version(self.ndk_dir)
57+
mock_read_ndk.assert_called_once_with(self.ndk_dir)
58+
self.assertEqual(
59+
cm.output,
60+
[
61+
"INFO:p4a:[INFO]: {}".format(
62+
CURRENT_NDK_VERSION_MESSAGE.format(
63+
ndk_version=MAX_NDK_VERSION + 1
64+
)
65+
),
66+
"WARNING:p4a:[WARNING]: {}".format(
67+
RECOMMENDED_NDK_VERSION_MESSAGE.format(
68+
recommended_ndk_version=RECOMMENDED_NDK_VERSION
69+
)
70+
),
71+
"WARNING:p4a:[WARNING]: {}".format(NEW_NDK_MESSAGE),
72+
],
73+
)
74+
75+
@mock.patch("pythonforandroid.recommendations.read_ndk_version")
76+
def test_check_ndk_version_lower_than_recommended(self, mock_read_ndk):
77+
mock_read_ndk.return_value.version = [MIN_NDK_VERSION - 1, 0, 5232133]
78+
with self.assertRaises(BuildInterruptingException) as e:
79+
check_ndk_version(self.ndk_dir)
80+
self.assertEqual(
81+
e.exception.args[0],
82+
NDK_LOWER_THAN_SUPPORTED_MESSAGE.format(
83+
min_supported=MIN_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL
84+
),
85+
)
86+
mock_read_ndk.assert_called_once_with(self.ndk_dir)
87+
88+
@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
89+
def test_check_ndk_version_error(self):
90+
"""
91+
Test that a fake ndk dir give us two messages:
92+
- first should be an `INFO` log
93+
- second should be an `WARNING` log
94+
"""
95+
with self.assertLogs(level="INFO") as cm:
96+
check_ndk_version(self.ndk_dir)
97+
self.assertEqual(
98+
cm.output,
99+
[
100+
"INFO:p4a:[INFO]: {}".format(UNKNOWN_NDK_MESSAGE),
101+
"WARNING:p4a:[WARNING]: {}".format(
102+
READ_ERROR_NDK_MESSAGE.format(ndk_dir=self.ndk_dir)
103+
),
104+
"WARNING:p4a:[WARNING]: {}".format(
105+
ENSURE_RIGHT_NDK_MESSAGE.format(
106+
min_supported=MIN_NDK_VERSION,
107+
rec_version=RECOMMENDED_NDK_VERSION,
108+
ndk_url=NDK_DOWNLOAD_URL,
109+
)
110+
),
111+
],
112+
)
113+
114+
@mock.patch("pythonforandroid.recommendations.open")
115+
def test_read_ndk_version(self, mock_open_src_prop):
116+
mock_open_src_prop.side_effect = [
117+
mock.mock_open(
118+
read_data="Pkg.Revision = 17.2.4988734"
119+
).return_value
120+
]
121+
version = read_ndk_version(self.ndk_dir)
122+
mock_open_src_prop.assert_called_once_with(
123+
join(self.ndk_dir, "source.properties")
124+
)
125+
assert version == "17.2.4988734"
126+
127+
@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
128+
@mock.patch("pythonforandroid.recommendations.open")
129+
def test_read_ndk_version_error(self, mock_open_src_prop):
130+
mock_open_src_prop.side_effect = [
131+
mock.mock_open(read_data="").return_value
132+
]
133+
with self.assertLogs(level="INFO") as cm:
134+
version = read_ndk_version(self.ndk_dir)
135+
self.assertEqual(
136+
cm.output,
137+
["INFO:p4a:[INFO]: {}".format(PARSE_ERROR_NDK_MESSAGE)],
138+
)
139+
mock_open_src_prop.assert_called_once_with(
140+
join(self.ndk_dir, "source.properties")
141+
)
142+
assert version is None
143+
144+
def test_check_target_api_error_arch_armeabi(self):
145+
146+
with self.assertRaises(BuildInterruptingException) as e:
147+
check_target_api(RECOMMENDED_TARGET_API, "armeabi")
148+
self.assertEqual(
149+
e.exception.args[0],
150+
UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE.format(
151+
req_ndk_api=RECOMMENDED_TARGET_API,
152+
max_ndk_api=ARMEABI_MAX_TARGET_API,
153+
),
154+
)
155+
156+
@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
157+
def test_check_target_api_warning_target_api(self):
158+
159+
with self.assertLogs(level="INFO") as cm:
160+
check_target_api(MIN_TARGET_API - 1, MIN_TARGET_API)
161+
self.assertEqual(
162+
cm.output,
163+
[
164+
"WARNING:p4a:[WARNING]: Target API 25 < 26",
165+
"WARNING:p4a:[WARNING]: {old_api_msg}".format(
166+
old_api_msg=OLD_API_MESSAGE
167+
),
168+
],
169+
)
170+
171+
def test_check_ndk_api_error_android_api(self):
172+
"""
173+
Given an `android api` greater than an `ndk_api`, we should get an
174+
`BuildInterruptingException`.
175+
"""
176+
ndk_api = MIN_NDK_API + 1
177+
android_api = MIN_NDK_API
178+
with self.assertRaises(BuildInterruptingException) as e:
179+
check_ndk_api(ndk_api, android_api)
180+
self.assertEqual(
181+
e.exception.args[0],
182+
TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE.format(
183+
ndk_api=ndk_api, android_api=android_api
184+
),
185+
)
186+
187+
@unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
188+
def test_check_ndk_api_warning_old_ndk(self):
189+
"""
190+
Given an `android api` lower than the supported by p4a, we should
191+
get an `BuildInterruptingException`.
192+
"""
193+
ndk_api = MIN_NDK_API - 1
194+
android_api = RECOMMENDED_TARGET_API
195+
with self.assertLogs(level="INFO") as cm:
196+
check_ndk_api(ndk_api, android_api)
197+
self.assertEqual(
198+
cm.output,
199+
[
200+
"WARNING:p4a:[WARNING]: {}".format(
201+
OLD_NDK_API_MESSAGE.format(MIN_NDK_API)
202+
)
203+
],
204+
)

0 commit comments

Comments
 (0)