Skip to content
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
18 changes: 16 additions & 2 deletions doc/source/buildoptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ options (this list may not be exhaustive):
- ``--private``: The directory containing your project files.
- ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``.
- ``--name``: The app name.
- ``--version``: The version number.
- ``--version``: The display version shown to users. On Android this is
rendered as ``versionName``.
- ``--numeric-version``: The Android ``versionCode`` used for update ordering.
This must be a positive integer no greater than ``2100000000``. If omitted,
python-for-android computes it from ``--version``. If the computed value is
too large, keep the display version in ``--version`` and set a valid
``--numeric-version``. See Android's
`versionCode documentation <https://developer.android.com/tools/publishing/versioning>`__.
- ``--orientation``: The orientations that the app will display in.
(Available options are ``portrait``, ``landscape``, ``portrait-reverse``, ``landscape-reverse``).
Since Android ignores ``android:screenOrientation`` when in multi-window mode
Expand Down Expand Up @@ -145,7 +152,14 @@ ready.
- ``--private``: The directory containing your project files.
- ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``.
- ``--name``: The app name.
- ``--version``: The version number.
- ``--version``: The display version shown to users. On Android this is
rendered as ``versionName``.
- ``--numeric-version``: The Android ``versionCode`` used for update ordering.
This must be a positive integer no greater than ``2100000000``. If omitted,
python-for-android computes it from ``--version``. If the computed value is
too large, keep the display version in ``--version`` and set a valid
``--numeric-version``. See Android's
`versionCode documentation <https://developer.android.com/tools/publishing/versioning>`__.
- ``--orientation``: The orientations that the app will display in.
(Available options are ``portrait``, ``landscape``, ``portrait-reverse``, ``landscape-reverse``).
Since Android ignores ``android:screenOrientation`` when in multi-window mode
Expand Down
106 changes: 86 additions & 20 deletions pythonforandroid/bootstraps/common/build/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,73 @@ def get_bootstrap_name():

DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity'
DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService'
# Google Play's documented maximum Android versionCode.
# https://developer.android.com/tools/publishing/versioning
MAX_ANDROID_VERSION_CODE = 2100000000


def get_android_numeric_version(version, min_sdk_version):
"""
Generate the default Android versionCode value from --version.

The format is (10 + minsdk + app_version). Older versioning was
(arch + minsdk + app_version), with arch expressed with a single digit
from 6 to 9. Since multi-arch support, this uses 10.
"""
version_code = 0
try:
for part in version.split('.'):
version_code *= 100
version_code += int(part)
except ValueError as exc:
raise ValueError(
"Could not generate Android versionCode from --version "
"{!r}. --version is Android versionName; when it is not numeric "
"dot-separated text, set --numeric-version to a positive Android "
"versionCode integer no greater than {}.".format(
version, MAX_ANDROID_VERSION_CODE
)
) from exc
return "{}{}{}".format("10", min_sdk_version, version_code)


def validate_android_numeric_version(numeric_version, *, generated_from_version=None):
try:
normalized_version = int(numeric_version)
except (TypeError, ValueError) as exc:
raise ValueError(
"--numeric-version must be a decimal integer Android versionCode "
"greater than 0 and no greater than {}; got {!r}.".format(
MAX_ANDROID_VERSION_CODE, numeric_version
)
) from exc

if normalized_version <= 0:
raise ValueError(
"--numeric-version must be a positive Android versionCode "
"greater than 0; got {!r}.".format(numeric_version)
)

if normalized_version > MAX_ANDROID_VERSION_CODE:
if generated_from_version is not None:
raise ValueError(
"Generated Android versionCode {} from --version {!r}, "
"which exceeds the maximum {}. --version is Android "
"versionName; keep this display version by setting "
"--numeric-version to a positive Android versionCode no "
"greater than {}.".format(
normalized_version,
generated_from_version,
MAX_ANDROID_VERSION_CODE,
MAX_ANDROID_VERSION_CODE,
)
)
raise ValueError(
"--numeric-version is Android versionCode and must not exceed "
"{}; got {!r}.".format(MAX_ANDROID_VERSION_CODE, numeric_version)
)

return str(normalized_version)


def render(template, dest, **kwargs):
Expand Down Expand Up @@ -420,19 +487,17 @@ def make_package(args):
versioned_name = (args.name.replace(' ', '').replace('\'', '') +
'-' + args.version)

version_code = 0
if not args.numeric_version:
"""
Set version code in format (10 + minsdk + app_version)
Historically versioning was (arch + minsdk + app_version),
with arch expressed with a single digit from 6 to 9.
Since the multi-arch support, has been changed to 10.
"""
min_sdk = args.min_sdk_version
for i in args.version.split('.'):
version_code *= 100
version_code += int(i)
args.numeric_version = "{}{}{}".format("10", min_sdk, version_code)
generated_from_version = None
if args.numeric_version is None:
generated_from_version = args.version
args.numeric_version = get_android_numeric_version(
args.version,
args.min_sdk_version,
)
args.numeric_version = validate_android_numeric_version(
args.numeric_version,
generated_from_version=generated_from_version,
)

if args.intent_filters:
with open(args.intent_filters) as fd:
Expand Down Expand Up @@ -793,14 +858,15 @@ def create_argument_parser():
help=('The human-readable name of the project.'),
required=True)
ap.add_argument('--numeric-version', dest='numeric_version',
help=('The numeric version number of the project. If not '
'given, this is automatically computed from the '
'version.'))
help=('The Android versionCode of the project. This must '
'be a positive decimal integer no greater than '
'{}. If not given, it is automatically computed '
'from --version.').format(MAX_ANDROID_VERSION_CODE))
ap.add_argument('--version', dest='version',
help=('The version number of the project. This should '
'consist of numbers and dots, and should have the '
'same number of groups of numbers as previous '
'versions.'),
help=('The Android versionName of the project, shown to '
'users as the display version. Use '
'--numeric-version to control Android versionCode '
'and update ordering.'),
required=True)
if is_sdl_bootstrap():
ap.add_argument('--launcher', dest='launcher', action='store_true',
Expand Down
69 changes: 60 additions & 9 deletions tests/test_bootstrap_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@
from pythonforandroid.util import load_source


class TestBootstrapBuild(unittest.TestCase):
def setUp(self):
os.environ["P4A_BUILD_IS_RUNNING_UNITTESTS"] = "1"
def load_bootstrap_build_module():
os.environ["P4A_BUILD_IS_RUNNING_UNITTESTS"] = "1"

build_src = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"../pythonforandroid/bootstraps/common/build/build.py",
)
build_src = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"../pythonforandroid/bootstraps/common/build/build.py",
)

self.buildpy = load_source("buildpy", build_src)
self.buildpy.get_bootstrap_name = mock.Mock(return_value="sdl2")
buildpy = load_source("buildpy", build_src)
buildpy.get_bootstrap_name = mock.Mock(return_value="sdl2")
return buildpy


class TestBootstrapBuild(unittest.TestCase):
def setUp(self):
self.buildpy = load_bootstrap_build_module()
self.ap = self.buildpy.create_argument_parser()

self.common_args = [
Expand Down Expand Up @@ -178,3 +182,50 @@ def test_sdl_orientation_hint_multiple(self):

assert "LandscapeLeft" in sdl_orientation_hint
assert "Portrait" in sdl_orientation_hint


class TestAndroidNumericVersion:
def setup_method(self):
self.buildpy = load_bootstrap_build_module()

def test_generates_default_three_part_version_code(self):
assert self.buildpy.get_android_numeric_version("1.0.5", 24) == "102410005"

def test_accepts_and_normalizes_explicit_numeric_version(self):
assert self.buildpy.validate_android_numeric_version("0000001") == "1"
assert self.buildpy.validate_android_numeric_version(2100000000) == "2100000000"

@pytest.mark.parametrize("numeric_version", ["abc", "1.2", "", None])
def test_rejects_non_integer_explicit_numeric_versions(self, numeric_version):
with pytest.raises(ValueError, match="--numeric-version.*decimal integer"):
self.buildpy.validate_android_numeric_version(numeric_version)

@pytest.mark.parametrize("numeric_version", ["0", 0, "-1", -1])
def test_rejects_non_positive_explicit_numeric_versions(self, numeric_version):
with pytest.raises(ValueError, match="--numeric-version.*greater than 0"):
self.buildpy.validate_android_numeric_version(numeric_version)

@pytest.mark.parametrize("numeric_version", ["2100000001", 2100000001])
def test_rejects_oversized_explicit_numeric_versions(self, numeric_version):
with pytest.raises(
ValueError, match="--numeric-version.*2100000000"
):
self.buildpy.validate_android_numeric_version(numeric_version)

def test_rejects_generated_overflow_and_mentions_version_name(self):
generated_version = self.buildpy.get_android_numeric_version("1.0.5.1", 24)

with pytest.raises(
ValueError,
match="Generated Android versionCode .*--version '1\\.0\\.5\\.1'.*--numeric-version.*2100000000",
):
self.buildpy.validate_android_numeric_version(
generated_version, generated_from_version="1.0.5.1"
)

def test_rejects_non_numeric_version_name_for_generation(self):
with pytest.raises(
ValueError,
match="Could not generate Android versionCode from --version '1\\.0\\.beta'.*versionName.*--numeric-version",
):
self.buildpy.get_android_numeric_version("1.0.beta", 24)
Loading