Skip to content

Commit

Permalink
Reimplement upgrade Command for Tool base class
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Apr 8, 2023
1 parent 9e5cd04 commit 87d2272
Show file tree
Hide file tree
Showing 20 changed files with 378 additions and 330 deletions.
149 changes: 84 additions & 65 deletions src/briefcase/commands/upgrade.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
import sys
from typing import List
from operator import attrgetter
from typing import Collection, List, Set

from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.android_sdk import AndroidSDK
from briefcase.integrations.java import JDK
from briefcase.integrations.linuxdeploy import LinuxDeploy
from briefcase.integrations.rcedit import RCEdit
from briefcase.integrations.wix import WiX
from briefcase.integrations.base import Tool, tool_registry

from .base import BaseCommand


def stringify(
coll: Collection[str],
prefix: str = "tool",
plural: str = "",
conjunction: str = "or",
):
"""Create a user-facing string from a list of strings.
For instance:
Inputs:
coll: ["one", "two", "three"]
prefix: "number"
conjunction: "and"
Output:
"numbers 'one', 'two', and 'three'"
:param coll: Collection of strings to stringify
:param prefix: Noun that describes the strings
:param plural: Plural version of noun; assumes adding 's' is enough if not specified.
:param conjunction: Value to use to join the last string to the list; defaults to 'or'.
"""
comma_list = ", ".join(f"'{val}'" for val in coll)
if len(coll) > 1:
prefix = plural or f"{prefix}s"
return f"{prefix} {f', {conjunction}'.join(comma_list.rsplit(',', 1))}"


class UpgradeCommand(BaseCommand):
cmd_line = "briefcase upgrade"
command = "upgrade"
output_format = None
description = "Upgrade Briefcase-managed tools."

def __init__(self, *args, **options):
super().__init__(*args, **options)
self.sdks = [
AndroidSDK,
LinuxDeploy,
JDK,
WiX,
RCEdit,
]

@property
def platform(self):
"""The upgrade command always reports as the local platform."""
Expand Down Expand Up @@ -59,62 +72,68 @@ def add_options(self, parser):
help="The Briefcase-managed tool to upgrade. If no tool is named, all tools will be upgraded.",
)

def __call__(self, tool_list: List[str], list_tools=False, **options):
# Verify all the managed SDKs and plugins to see which are present.
managed_tools = {}
non_managed_tools = set()
def get_tools_to_upgrade(self, tool_list: Set[str]) -> List[Tool]:
"""Returns set of Tools that can be upgraded.
for klass in self.sdks:
try:
tool = klass.verify(self.tools, install=False)
if tool.managed_install:
managed_tools[klass.name] = tool
try:
for plugin_klass in tool.plugins.values():
try:
plugin = plugin_klass.verify(self.tools, install=False)
# All plugins are managed
managed_tools[plugin.name] = plugin
except BriefcaseCommandError:
# Plugin doesn't exist
non_managed_tools.add(klass.name)
except AttributeError:
# Tool doesn't have plugins
pass
else:
non_managed_tools.add(klass.name)
except BriefcaseCommandError:
# Tool doesn't exist
non_managed_tools.add(klass.name)

# If a tool list wasn't provided, use the list of installed tools
if not tool_list:
tool_list = sorted(managed_tools.keys())

# Build a list of requested tools that are managed.
found_tools = []
for name in tool_list:
if name in managed_tools:
found_tools.append(name)
elif name not in non_managed_tools:
Raises `BriefcaseCommandError` if user list contains any invalid tool names.
"""
# Validate user tool list against tool registry
if tool_list:
if invalid_tools := tool_list - set(tool_registry):
raise BriefcaseCommandError(
f"Briefcase doesn't know how to manage the tool '{name}'"
f"Briefcase does not know how to manage {stringify(invalid_tools)}."
)
upgrade_list = {
tool for name, tool in tool_registry.items() if name in tool_list
}
else:
upgrade_list = set(tool_registry.values())

if found_tools:
if list_tools:
self.logger.info("Briefcase is managing the following tools:")
for name in found_tools:
self.logger.info(f" - {name}")
# Filter list of tools to those that are being managed
tools_to_upgrade = set()
for tool_klass in upgrade_list:
try:
tool = tool_klass.verify(self.tools, install=False, app=object())
except (BriefcaseCommandError, TypeError, FileNotFoundError):
# BriefcaseCommandError: Tool isn't installed
# TypeError: Signature of tool.verify() was incomplete
# FileNotFoundError: An executable for subprocess.run() was not found
pass
else:
self.logger.info("Briefcase will upgrade the following tools:")
for name in found_tools:
self.logger.info(f" - {name}")
if tool.managed_install:
tools_to_upgrade.add(tool)

# Let the user know if any requested tools are not being managed
if tool_list:
if unmanaged_tools := tool_list - {tool.name for tool in tools_to_upgrade}:
error_msg = f"Briefcase is not managing {stringify(unmanaged_tools)}."
if not tools_to_upgrade:
raise BriefcaseCommandError(error_msg)
else:
self.logger.warning(error_msg)

return sorted(list(tools_to_upgrade), key=attrgetter("name"))

def __call__(self, tool_list: List[str], list_tools: bool = False, **options):
"""Perform tool upgrades or list tools qualifying for upgrades.
for name in found_tools:
tool = managed_tools[name]
:param tool_list: List of tool names. from user to upgrade.
:param list_tools: Boolean to only list upgradeable tools (default False).
"""
tool_list = set(tool_list)
tools_to_upgrade = self.get_tools_to_upgrade(tool_list)

if tools_to_upgrade:
action = "is managing" if list_tools else "will upgrade"
self.logger.info(
f"Briefcase {action} the following tools:", prefix=self.command
)
for tool in tools_to_upgrade:
self.logger.info(f" - {tool.full_name} ({tool.name})")

if not list_tools:
for tool in tools_to_upgrade:
self.logger.info(f"Upgrading {tool.full_name}...", prefix=tool.name)
tool.upgrade()

else:
self.logger.info("Briefcase is not managing any tools.")
2 changes: 1 addition & 1 deletion src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def DEFAULT_SYSTEM_IMAGE(self) -> str:
return f"system-images;android-31;default;{self.emulator_abi}"

@classmethod
def verify(cls, tools: ToolCache, install=True) -> AndroidSDK:
def verify(cls, tools: ToolCache, install: bool = True, **kwargs) -> AndroidSDK:
"""Verify an Android SDK is available.
If the ANDROID_SDK_ROOT environment variable is set, that location will
Expand Down
17 changes: 14 additions & 3 deletions src/briefcase/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,29 @@
from briefcase.integrations.xcode import Xcode, XcodeCliTools


# Registry of all defined Tools
tool_registry: dict[str, type[Tool]] = dict()


class Tool(ABC):
"""Tool Base."""

name: str
full_name: str

def __init__(self, tools: ToolCache):
def __init__(self, tools: ToolCache, **kwargs):
self.tools = tools

def __init_subclass__(tool, **kwargs):
"""Register each tool at definition."""
try:
tool_registry[tool.name] = tool
except AttributeError:
tool_registry[tool.__name__] = tool

@classmethod
@abstractmethod
def verify(cls, tools: ToolCache):
def verify(cls, tools: ToolCache, **kwargs):
"""Confirm the tool is available and usable on the host platform."""
...

Expand Down Expand Up @@ -79,7 +90,7 @@ def uninstall(self, *a, **kw):
else:
raise NonManagedToolError(self.full_name)

def upgrade(self, *a, **kw):
def upgrade(self):
"""Upgrade a managed tool."""
if self.managed_install:
if not self.exists():
Expand Down
5 changes: 3 additions & 2 deletions src/briefcase/integrations/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class Docker(Tool):
}

@classmethod
def verify(cls, tools: ToolCache) -> Docker:
def verify(cls, tools: ToolCache, **kwargs) -> Docker:
"""Verify Docker is installed and operational."""
# short circuit since already verified and available
if hasattr(tools, "docker"):
Expand Down Expand Up @@ -236,7 +236,7 @@ def prepare(self, image_tag):


class DockerAppContext(Tool):
name = "docker"
name = "dockerappcontext"
full_name = "Docker"

def __init__(self, tools: ToolCache, app: AppConfig):
Expand Down Expand Up @@ -265,6 +265,7 @@ def verify(
host_bundle_path: Path,
host_data_path: Path,
python_version: str,
**kwargs,
) -> DockerAppContext:
"""Verify that docker is available as an app-bound tool.
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/integrations/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Download(Tool):
full_name = "Download"

@classmethod
def verify(cls, tools: ToolCache):
def verify(cls, tools: ToolCache, **kwargs):
"""Make downloader available in tool cache."""
# short circuit since already verified and available
if hasattr(tools, "download"):
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/integrations/flatpak.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Flatpak(Tool):
DEFAULT_SDK = "org.freedesktop.Sdk"

@classmethod
def verify(cls, tools: ToolCache) -> Flatpak:
def verify(cls, tools: ToolCache, **kwargs) -> Flatpak:
"""Verify that the Flatpak toolchain is available.
:param tools: ToolCache of available tools
Expand Down
3 changes: 3 additions & 0 deletions src/briefcase/integrations/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@


class Git(Tool):
name = "git"
full_name = "Git"

@classmethod
def verify(cls, tools: ToolCache):
"""Verify if git is installed.
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/integrations/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def adoptOpenJDK_download_url(self) -> str:
)

@classmethod
def verify(cls, tools: ToolCache, install: bool = True) -> JDK:
def verify(cls, tools: ToolCache, install: bool = True, **kwargs) -> JDK:
"""Verify that a Java 8 JDK exists.
If ``JAVA_HOME`` is set, try that version. If it is a JRE, or its *not*
Expand Down
27 changes: 16 additions & 11 deletions src/briefcase/integrations/linuxdeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import hashlib
import shlex
from abc import abstractmethod
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TypeVar
from urllib.parse import urlparse
Expand All @@ -22,7 +22,7 @@
ELF_PATCH_PATCHED_BYTES = bytes.fromhex("000000")


class LinuxDeployBase(Tool):
class LinuxDeployBase(Tool, ABC):
name: str
full_name: str
install_msg: str
Expand Down Expand Up @@ -90,6 +90,9 @@ def verify(
:returns: A valid tool wrapper. If the tool/plugin is not
available, and was not installed, raises MissingToolError.
"""
if cls is LinuxDeployBase:
raise BriefcaseCommandError(f"{cls.__name__} cannot be used as a Tool.")

is_plugin = issubclass(cls, LinuxDeployPluginBase)

# short circuit since already verified and available
Expand Down Expand Up @@ -189,7 +192,8 @@ def file_path(self) -> Path:


class LinuxDeployGtkPlugin(LinuxDeployPluginBase):
full_name = "linuxdeploy GTK plugin"
name = "linuxdeploygtkplugin"
full_name = "LinuxDeploy GTK plugin"

@property
def file_name(self) -> str:
Expand All @@ -204,7 +208,8 @@ def download_url(self) -> str:


class LinuxDeployQtPlugin(LinuxDeployPluginBase):
full_name = "linuxdeploy Qt plugin"
name = "linuxdeployqtplugin"
full_name = "LinuxDeploy Qt plugin"

@property
def file_name(self) -> str:
Expand All @@ -219,10 +224,11 @@ def download_url(self) -> str:


class LinuxDeployLocalFilePlugin(LinuxDeployPluginBase):
name = "linuxdeploy_user_file_plugin"
full_name = "user-provided linuxdeploy plugin from local file"
install_msg = "Copying user-provided plugin into project"

def __init__(self, tools, plugin_path, bundle_path):
def __init__(self, tools, plugin_path, bundle_path, **kwargs):
self._file_name = plugin_path.name
self.local_path = plugin_path.parent
self._file_path = bundle_path
Expand Down Expand Up @@ -261,9 +267,10 @@ def install(self):


class LinuxDeployURLPlugin(LinuxDeployPluginBase):
name = "linuxdeploy_user_url_plugin"
full_name = "user-provided linuxdeploy plugin from URL"

def __init__(self, tools, url):
def __init__(self, tools, url, **kwargs):
self._download_url = url

url_parts = urlparse(url)
Expand Down Expand Up @@ -302,7 +309,7 @@ def download_url(self) -> str:

class LinuxDeploy(LinuxDeployBase):
name = "linuxdeploy"
full_name = "linuxdeploy"
full_name = "LinuxDeploy"
install_msg = "linuxdeploy was not found; downloading and installing..."

@property
Expand Down Expand Up @@ -353,10 +360,8 @@ def verify_plugins(
`FOO` in the environment. The definition will be split the same way as
shell arguments, so spaces should be escaped.
:param plugin_definitions: A list of strings defining the required
plugins.
:param bundle_path: The location of the app bundle that requires the
plugins.
:param plugin_definitions: A list of strings defining the required plugins.
:param bundle_path: The location of the app bundle that requires the plugins.
:returns: A dictionary of plugin ID->instantiated plugin instances.
"""
plugins = {}
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/integrations/rcedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def rcedit_path(self) -> Path:
return self.tools.base_path / "rcedit-x64.exe"

@classmethod
def verify(cls, tools: ToolCache, install: bool = True) -> RCEdit:
def verify(cls, tools: ToolCache, install: bool = True, **kwargs) -> RCEdit:
"""Verify that rcedit is available.
:param tools: ToolCache of available tools
Expand Down
Loading

0 comments on commit 87d2272

Please sign in to comment.