Skip to content

Commit

Permalink
Attempt to install cmdline-tools;9.0 for an existing Android SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Aug 1, 2023
1 parent a8d458b commit 71c68d7
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 72 deletions.
52 changes: 45 additions & 7 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,12 @@ def verify_install(
)
sdk.verify_license()
elif sdk.cmdline_tools_path.parent.exists():
# a cmdline-tools directory exists but doesn't provide the expected
# version of Command-Line Tools
sdk = None
tools.logger.warning(
f"""
# a cmdline-tools directory exists but the required version isn't installed.
# try to install the required version using the 'latest' version.
if not sdk.install_cmdline_tools():
sdk = None
tools.logger.warning(
f"""
*************************************************************************
** WARNING: Incompatible Command-Line Tools Version **
*************************************************************************
Expand All @@ -271,7 +272,7 @@ def verify_install(
*************************************************************************
"""
)
)
else:
sdk = None
tools.logger.warning(
Expand All @@ -290,7 +291,7 @@ def verify_install(
If {sdk_env_source} is an Android SDK, ensure it is the root directory
of the Android SDK instance such that
${sdk_env_source}/cmdline-tools/latest/bin/sdkmanager
${sdk_env_source}/cmdline-tools/{cls.SDK_MANAGER_VER}/bin/sdkmanager
is a valid filepath.
Expand Down Expand Up @@ -455,6 +456,43 @@ def upgrade(self):
"""
) from e

def install_cmdline_tools(self) -> bool:
"""Attempt to use 'latest' cmdline-tools to install the currently required
version of the Command-Line Tools.
The Briefcase-managed SDK should always have the required version of cmdline-
tools installed; however, user-provided SDKs may not have it.
:returns: True if successfully installed; False otherwise
"""
self.tools.logger.info(
f"Installing Android Command-Line Tools {self.SDK_MANAGER_VER}...",
prefix=self.full_name,
)
latest_sdkmanager_path = (
self.root_path
/ "cmdline-tools"
/ "latest"
/ "bin"
/ ("sdkmanager.bat" if self.tools.host_os == "Windows" else "sdkmanager")
)
try:
self.tools.subprocess.run(
[
latest_sdkmanager_path,
f"cmdline-tools;{self.SDK_MANAGER_VER}",
],
check=True,
stream_output=False,
)
except (OSError, subprocess.CalledProcessError) as e:
self.tools.logger.debug(str(e))
self.tools.logger.warning(
f"Failed to install cmdline-tools;{self.SDK_MANAGER_VER}"
)
return False
return True

def list_packages(self):
"""In debug output, list the packages currently managed by the SDK."""
try:
Expand Down
156 changes: 91 additions & 65 deletions tests/integrations/android_sdk/AndroidSDK/test_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

from ..conftest import SDK_MGR_DL_VER, SDK_MGR_VER

SDKMANAGER_FILENAME = (
"sdkmanager.bat" if platform.system() == "Windows" else "sdkmanager"
)


@pytest.fixture
def mock_tools(mock_tools) -> ToolCache:
Expand Down Expand Up @@ -86,19 +90,11 @@ def test_succeeds_immediately_in_happy_path(mock_tools, tmp_path):
# exists, verify() should succeed, create no subprocesses, make no requests, and
# return an SDK wrapper.

# On Windows, this requires `sdkmanager.bat`; on non-Windows, it requires
# `sdkmanager`.

# Create `sdkmanager` and the license file.
android_sdk_root_path = tmp_path / "tools" / "android_sdk"
tools_bin = android_sdk_root_path / "cmdline-tools" / SDK_MGR_VER / "bin"
tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
sdk_manager = tools_bin / "sdkmanager.bat"
sdk_manager.touch()
else:
sdk_manager = tools_bin / "sdkmanager"
sdk_manager.touch(mode=0o755)
(tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Pre-accept the license
accept_license(android_sdk_root_path)()
Expand All @@ -125,12 +121,7 @@ def test_user_provided_sdk(mock_tools, env_var, tmp_path, capsys):
existing_android_sdk_root_path = tmp_path / "other_sdk"
tools_bin = existing_android_sdk_root_path / "cmdline-tools" / SDK_MGR_VER / "bin"
tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
sdk_manager = tools_bin / "sdkmanager.bat"
sdk_manager.touch()
else:
sdk_manager = tools_bin / "sdkmanager"
sdk_manager.touch(mode=0o755)
(tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Pre-accept the license
accept_license(existing_android_sdk_root_path)()
Expand Down Expand Up @@ -169,12 +160,7 @@ def test_consistent_user_provided_sdk(mock_tools, tmp_path, capsys):
existing_android_sdk_root_path = tmp_path / "other_sdk"
tools_bin = existing_android_sdk_root_path / "cmdline-tools" / SDK_MGR_VER / "bin"
tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
sdk_manager = tools_bin / "sdkmanager.bat"
sdk_manager.touch()
else:
sdk_manager = tools_bin / "sdkmanager"
sdk_manager.touch(mode=0o755)
(tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Pre-accept the license
accept_license(existing_android_sdk_root_path)()
Expand Down Expand Up @@ -212,12 +198,7 @@ def test_inconsistent_user_provided_sdk(mock_tools, tmp_path, capsys):
existing_android_sdk_root_path = tmp_path / "other_sdk"
tools_bin = existing_android_sdk_root_path / "cmdline-tools" / SDK_MGR_VER / "bin"
tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
sdk_manager = tools_bin / "sdkmanager.bat"
sdk_manager.touch()
else:
sdk_manager = tools_bin / "sdkmanager"
sdk_manager.touch(mode=0o755)
(tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Pre-accept the license
accept_license(existing_android_sdk_root_path)()
Expand Down Expand Up @@ -247,26 +228,20 @@ def test_inconsistent_user_provided_sdk(mock_tools, tmp_path, capsys):
@pytest.mark.parametrize("env_var", ["ANDROID_HOME", "ANDROID_SDK_ROOT"])
def test_invalid_user_provided_sdk(mock_tools, env_var, tmp_path, capsys):
"""If the user's environment specifies an invalid Android SDK, it is ignored."""

# Create `sdkmanager` and the license file
# for the *briefcase* managed version of the SDK.
android_sdk_root_path = tmp_path / "tools" / "android_sdk"
tools_bin = android_sdk_root_path / "cmdline-tools" / SDK_MGR_VER / "bin"
tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
sdk_manager = tools_bin / "sdkmanager.bat"
sdk_manager.touch()
else:
sdk_manager = tools_bin / "sdkmanager"
sdk_manager.touch(mode=0o755)
(tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Pre-accept the license
accept_license(android_sdk_root_path)()

# Set the environment to specify an ANDROID_SDK_ROOT that doesn't exist
mock_tools.os.environ = {env_var: os.fsdecode(tmp_path / "other_sdk")}

# Expect verify() to succeed
# Expect verify() to succeed"
sdk = AndroidSDK.verify(mock_tools)

# No calls to download, run or unpack anything.
Expand All @@ -282,48 +257,109 @@ def test_invalid_user_provided_sdk(mock_tools, env_var, tmp_path, capsys):


@pytest.mark.parametrize("env_var", ["ANDROID_HOME", "ANDROID_SDK_ROOT"])
def test_invalid_user_provided_sdk_wrong_cmdline_tools_ver(
mock_tools, env_var, tmp_path, capsys
def test_user_provided_sdk_wrong_cmdline_tools_ver(
mock_tools,
env_var,
tmp_path,
capsys,
):
"""If the user's environment specifies an Android SDK without the expected cmdline
tools, it is ignored."""
tools and the cmdline-tools install fails, the Briefcase SDK is used."""
# Create `sdkmanager` and the license file for the *user's* version of the SDK
user_sdk_path = tmp_path / "other_sdk"
users_tools_bin = user_sdk_path / "cmdline-tools" / "latest" / "bin"
users_tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
(users_tools_bin / "sdkmanager.bat").touch()
else:
(users_tools_bin / "sdkmanager").touch(mode=0o755)
user_tools_bin = user_sdk_path / "cmdline-tools" / "6.0" / "bin"
user_tools_bin.mkdir(parents=True, mode=0o755)
(user_tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Create `sdkmanager` and the license file for the *briefcase* managed version of the SDK
# Create `sdkmanager` and the license file
# for the *briefcase* managed version of the SDK.
android_sdk_root_path = tmp_path / "tools" / "android_sdk"
tools_bin = android_sdk_root_path / "cmdline-tools" / SDK_MGR_VER / "bin"
tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
(tools_bin / "sdkmanager.bat").touch()
else:
(tools_bin / "sdkmanager").touch(mode=0o755)
(tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Pre-accept the license
accept_license(android_sdk_root_path)()

# Set the environment to specify an ANDROID_SDK_ROOT that doesn't exist
mock_tools.os.environ = {env_var: os.fsdecode(user_sdk_path)}

# Mock `cmdline-tools/latest` directory not existing
mock_tools.subprocess.run.side_effect = OSError

# Expect verify() to succeed
sdk = AndroidSDK.verify(mock_tools)

# No calls to download, run or unpack anything.
# No calls to download and nothing unpacked
mock_tools.download.file.assert_not_called()
mock_tools.subprocess.run.assert_not_called()
mock_tools.shutil.unpack_archive.assert_not_called()

# Required Command-line Tools installed
mock_tools.subprocess.run.assert_called_once_with(
[
tmp_path
/ "other_sdk"
/ "cmdline-tools"
/ "latest"
/ "bin"
/ SDKMANAGER_FILENAME,
f"cmdline-tools;{SDK_MGR_VER}",
],
check=True,
stream_output=False,
)

# The returned SDK has the expected root path.
assert sdk.root_path == android_sdk_root_path

# User is informed about invalid env var setting
assert "Incompatible Command-Line Tools Version" in capsys.readouterr().out
# User is informed about failed install and invalid SDK
output = capsys.readouterr().out
assert f"Failed to install cmdline-tools;{SDK_MGR_VER}" in output
assert "Incompatible Command-Line Tools Version" in output


@pytest.mark.parametrize("env_var", ["ANDROID_HOME", "ANDROID_SDK_ROOT"])
def test_user_provided_sdk_with_latest_cmdline_tools(
mock_tools,
env_var,
tmp_path,
capsys,
):
"""If the user's environment specifies an Android SDK without the expected cmdline
tools, the required cmdline-tools is installed in to it."""
# Create `sdkmanager` and the license file for the *user's* version of the SDK
user_sdk_path = tmp_path / "other_sdk"
user_tools_bin = user_sdk_path / "cmdline-tools" / "latest" / "bin"
user_tools_bin.mkdir(parents=True, mode=0o755)
(user_tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Set the environment to specify an ANDROID_SDK_ROOT that doesn't exist
mock_tools.os.environ = {env_var: os.fsdecode(user_sdk_path)}

# Expect verify() to succeed
sdk = AndroidSDK.verify(mock_tools)

# No calls to download and nothing unpacked
mock_tools.download.file.assert_not_called()
mock_tools.shutil.unpack_archive.assert_not_called()

# Required Command-line Tools installed
mock_tools.subprocess.run.assert_called_once_with(
[
tmp_path
/ "other_sdk"
/ "cmdline-tools"
/ "latest"
/ "bin"
/ SDKMANAGER_FILENAME,
f"cmdline-tools;{SDK_MGR_VER}",
],
check=True,
stream_output=False,
)

# The returned SDK has the expected root path.
assert sdk.root_path == user_sdk_path


def test_consistent_invalid_user_provided_sdk(mock_tools, tmp_path, capsys):
Expand All @@ -335,12 +371,7 @@ def test_consistent_invalid_user_provided_sdk(mock_tools, tmp_path, capsys):
android_sdk_root_path = tmp_path / "tools" / "android_sdk"
tools_bin = android_sdk_root_path / "cmdline-tools" / SDK_MGR_VER / "bin"
tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
sdk_manager = tools_bin / "sdkmanager.bat"
sdk_manager.touch()
else:
sdk_manager = tools_bin / "sdkmanager"
sdk_manager.touch(mode=0o755)
(tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Pre-accept the license
accept_license(android_sdk_root_path)()
Expand Down Expand Up @@ -375,12 +406,7 @@ def test_inconsistent_invalid_user_provided_sdk(mock_tools, tmp_path, capsys):
android_sdk_root_path = tmp_path / "tools" / "android_sdk"
tools_bin = android_sdk_root_path / "cmdline-tools" / SDK_MGR_VER / "bin"
tools_bin.mkdir(parents=True, mode=0o755)
if platform.system() == "Windows":
sdk_manager = tools_bin / "sdkmanager.bat"
sdk_manager.touch()
else:
sdk_manager = tools_bin / "sdkmanager"
sdk_manager.touch(mode=0o755)
(tools_bin / SDKMANAGER_FILENAME).touch(mode=0o755)

# Pre-accept the license
accept_license(android_sdk_root_path)()
Expand Down

0 comments on commit 71c68d7

Please sign in to comment.