diff --git a/buildozer/targets/ios.py b/buildozer/targets/ios.py index b588d97..05eeedd 100644 --- a/buildozer/targets/ios.py +++ b/buildozer/targets/ios.py @@ -60,14 +60,20 @@ li { padding: 1em; } class TargetIos(Target): targetname = "ios" + def __init__(self, buildozer): + super().__init__(buildozer) + executable = sys.executable or 'python' + self._toolchain_cmd = f"{executable} toolchain.py " + self._xcodebuild_cmd = "xcodebuild " + # set via install_platform() + self.ios_dir = None + self.ios_deploy_dir = None + def check_requirements(self): if sys.platform != "darwin": raise NotImplementedError("Only macOS is supported for iOS target") checkbin = self.buildozer.checkbin cmd = self.buildozer.cmd - executable = sys.executable or 'python' - self._toolchain_cmd = f"{executable} toolchain.py " - self._xcodebuild_cmd = "xcodebuild " checkbin('Xcode xcodebuild', 'xcodebuild') checkbin('Xcode xcode-select', 'xcode-select') @@ -95,6 +101,10 @@ class TargetIos(Target): self.buildozer.debug(' -> found {0}'.format(xcode)) def install_platform(self): + """ + Clones `kivy/kivy-ios` and `phonegap/ios-deploy` then sets `ios_dir` + and `ios_deploy_dir` accordingly. + """ self.ios_dir = self.install_or_update_repo('kivy-ios', platform='ios') self.ios_deploy_dir = self.install_or_update_repo('ios-deploy', platform='ios', @@ -385,10 +395,10 @@ class TargetIos(Target): if not error: correct = True break - self.error('Invalid keychain password') + self.buildozer.error('Invalid keychain password') if not correct: - self.error('Unable to unlock the keychain, exiting.') + self.buildozer.error('Unable to unlock the keychain, exiting.') raise BuildozerCommandException() # maybe user want to save it for further reuse? diff --git a/tests/targets/test_android.py b/tests/targets/test_android.py index 4f64567..a98c287 100644 --- a/tests/targets/test_android.py +++ b/tests/targets/test_android.py @@ -9,21 +9,15 @@ from tests.targets.utils import ( init_buildozer, patch_buildozer, patch_buildozer_checkbin, + patch_buildozer_cmd, + patch_buildozer_file_exists, ) -def patch_buildozer_cmd(): - return patch_buildozer("cmd") - - def patch_buildozer_cmd_expect(): return patch_buildozer("cmd_expect") -def patch_buildozer_file_exists(): - return patch_buildozer("file_exists") - - def patch_buildozer_download(): return patch_buildozer("download") diff --git a/tests/targets/test_ios.py b/tests/targets/test_ios.py index 7ef1b04..f5c5709 100644 --- a/tests/targets/test_ios.py +++ b/tests/targets/test_ios.py @@ -4,8 +4,19 @@ from unittest import mock import pytest +from buildozer import BuildozerCommandException from buildozer.targets.ios import TargetIos -from tests.targets.utils import init_buildozer, patch_buildozer_checkbin +from tests.targets.utils import ( + init_buildozer, + patch_buildozer_checkbin, + patch_buildozer_cmd, + patch_buildozer_error, + patch_buildozer_file_exists, +) + + +def patch_target_ios(method): + return mock.patch("buildozer.targets.ios.TargetIos.{method}".format(method=method)) def init_target(temp_dir, options=None): @@ -78,3 +89,131 @@ class TestTargetIos: ] ) ] + + def test_get_available_packages(self): + """Checks the toolchain `recipes --compact` output is parsed correctly to return recipe list.""" + target = init_target(self.temp_dir) + with patch_target_ios("toolchain") as m_toolchain: + m_toolchain.return_value = ("hostpython3 kivy pillow python3 sdl2", None, 0) + available_packages = target.get_available_packages() + assert m_toolchain.call_args_list == [ + mock.call("recipes --compact", get_stdout=True) + ] + assert available_packages == [ + "hostpython3", + "kivy", + "pillow", + "python3", + "sdl2", + ] + + def test_install_platform(self): + """Checks `install_platform()` calls clone commands and sets `ios_dir` and `ios_deploy_dir` attributes.""" + target = init_target(self.temp_dir) + assert target.ios_dir is None + assert target.ios_deploy_dir is None + with patch_buildozer_cmd() as m_cmd: + target.install_platform() + assert m_cmd.call_args_list == [ + mock.call("git clone https://github.com/kivy/kivy-ios", cwd=mock.ANY), + mock.call( + "git clone --branch 1.10.0 https://github.com/phonegap/ios-deploy", + cwd=mock.ANY, + ), + ] + assert target.ios_dir.endswith(".buildozer/ios/platform/kivy-ios") + assert target.ios_deploy_dir.endswith(".buildozer/ios/platform/ios-deploy") + + def test_compile_platform(self): + """Checks the `toolchain build` command is called on the ios requirements.""" + target = init_target(self.temp_dir) + target.ios_deploy_dir = "/ios/deploy/dir" + # fmt: off + with patch_target_ios("get_available_packages") as m_get_available_packages, \ + patch_target_ios("toolchain") as m_toolchain, \ + patch_buildozer_file_exists() as m_file_exists: + m_get_available_packages.return_value = ["hostpython3", "python3"] + m_file_exists.return_value = True + target.compile_platform() + # fmt: on + assert m_get_available_packages.call_args_list == [mock.call()] + assert m_toolchain.call_args_list == [mock.call("build python3")] + assert m_file_exists.call_args_list == [ + mock.call(target.ios_deploy_dir, "ios-deploy") + ] + + def test_get_package(self): + """Checks default package values and checks it can be overridden.""" + # default value + target = init_target(self.temp_dir) + package = target._get_package() + assert package == "org.test.myapp" + # override + target = init_target( + self.temp_dir, + {"package.domain": "com.github.kivy", "package.name": "buildozer"}, + ) + package = target._get_package() + assert package == "com.github.kivy.buildozer" + + def test_unlock_keychain_wrong_password(self): + """A `BuildozerCommandException` should be raised on wrong password 3 times.""" + target = init_target(self.temp_dir) + # fmt: off + with mock.patch("buildozer.targets.ios.getpass") as m_getpass, \ + patch_buildozer_cmd() as m_cmd, \ + pytest.raises(BuildozerCommandException): + m_getpass.return_value = "password" + # the `security unlock-keychain` command returned an error + # hence we'll get prompted to enter the password + m_cmd.return_value = (None, None, 123) + target._unlock_keychain() + # fmt: on + assert m_getpass.call_args_list == [ + mock.call("Password to unlock the default keychain:"), + mock.call("Password to unlock the default keychain:"), + mock.call("Password to unlock the default keychain:"), + ] + + def test_build_package_no_signature(self): + """Code signing is currently required to go through final `xcodebuild` steps.""" + target = init_target(self.temp_dir) + target.ios_dir = "/ios/dir" + # fmt: off + with patch_target_ios("_unlock_keychain") as m_unlock_keychain, \ + patch_buildozer_error() as m_error, \ + patch_target_ios("xcodebuild") as m_xcodebuild, \ + mock.patch("buildozer.targets.ios.plistlib.readPlist") as m_readplist, \ + mock.patch("buildozer.targets.ios.plistlib.writePlist") as m_writeplist, \ + patch_buildozer_cmd() as m_cmd: + m_readplist.return_value = {} + target.build_package() + # fmt: on + assert m_unlock_keychain.call_args_list == [mock.call()] + assert m_error.call_args_list == [ + mock.call( + "Cannot create the IPA package without signature. " + 'You must fill the "ios.codesign.debug" token.' + ) + ] + assert m_xcodebuild.call_args_list == [ + mock.call( + "-configuration Debug ENABLE_BITCODE=NO " + "CODE_SIGNING_ALLOWED=NO clean build", + cwd="/ios/dir/myapp-ios", + ) + ] + assert m_readplist.call_args_list == [ + mock.call("/ios/dir/myapp-ios/myapp-Info.plist") + ] + assert m_writeplist.call_args_list == [ + mock.call( + { + "CFBundleIdentifier": "org.test.myapp", + "CFBundleShortVersionString": "0.1", + "CFBundleVersion": "0.1.None", + }, + "/ios/dir/myapp-ios/myapp-Info.plist", + ) + ] + assert m_cmd.call_args_list == [mock.call(mock.ANY, cwd=target.ios_dir)] diff --git a/tests/targets/utils.py b/tests/targets/utils.py index 971bfe9..062820c 100644 --- a/tests/targets/utils.py +++ b/tests/targets/utils.py @@ -10,10 +10,22 @@ def patch_buildozer(method): return mock.patch("buildozer.Buildozer.{method}".format(method=method)) +def patch_buildozer_cmd(): + return patch_buildozer("cmd") + + def patch_buildozer_checkbin(): return patch_buildozer("checkbin") +def patch_buildozer_file_exists(): + return patch_buildozer("file_exists") + + +def patch_buildozer_error(): + return patch_buildozer("error") + + def default_specfile_path(): return os.path.join(os.path.dirname(buildozer_module.__file__), "default.spec")