Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce the File tool and tweak stub binary implementation #1871

Merged
merged 3 commits into from
Jun 10, 2024
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
1 change: 1 addition & 0 deletions changes/1871.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Consolidated file-oriented operations under a new File tool and updated error handling for stub binary.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ no-cover-if-is-macos = "'darwin' == os_environ.get('COVERAGE_PLATFORM', sys_plat
no-cover-if-not-macos = "'darwin' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'"
no-cover-if-is-windows = "'win32' == os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'"
no-cover-if-not-windows = "'win32' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'"
no-cover-if-gte-py312 = "sys_version_info > (3, 12) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
no-cover-if-is-py312 = "python_version == '3.12' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
no-cover-if-lt-py312 = "sys_version_info < (3, 12) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
no-cover-if-lt-py311 = "sys_version_info < (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"
Expand Down
4 changes: 2 additions & 2 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
UnsupportedHostError,
)
from briefcase.integrations.base import ToolCache
from briefcase.integrations.download import Download
from briefcase.integrations.file import File
from briefcase.integrations.subprocess import Subprocess
from briefcase.platforms import get_output_formats, get_platforms

Expand Down Expand Up @@ -173,7 +173,7 @@ def __init__(

# Immediately add tools that must be always available
Subprocess.verify(tools=self.tools)
Download.verify(tools=self.tools)
File.verify(tools=self.tools)

if not is_clone:
self.validate_locale()
Expand Down
79 changes: 39 additions & 40 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from briefcase.config import AppConfig
from briefcase.exceptions import (
BriefcaseCommandError,
InvalidStubBinary,
InvalidSupportPackage,
MissingAppSources,
MissingNetworkResourceError,
Expand Down Expand Up @@ -86,31 +87,35 @@ class CreateCommand(BaseCommand):
hidden_app_properties = {"permission"}

@property
def app_template_url(self):
def app_template_url(self) -> str:
"""The URL for a cookiecutter repository to use when creating apps."""
return f"https://github.com/beeware/briefcase-{self.platform}-{self.output_format}-template.git"

def support_package_filename(self, support_revision):
def support_package_filename(self, support_revision: str) -> str:
"""The query arguments to use in a support package query request."""
return f"Python-{self.python_version_tag}-{self.platform}-support.b{support_revision}.tar.gz"

def support_package_url(self, support_revision):
def support_package_url(self, support_revision: str) -> str:
"""The URL of the support package to use for apps of this type."""
return (
f"https://briefcase-support.s3.amazonaws.com/python/{self.python_version_tag}/{self.platform}/"
+ self.support_package_filename(support_revision)
"https://briefcase-support.s3.amazonaws.com/python/"
f"{self.python_version_tag}/"
f"{self.platform}/"
f"{self.support_package_filename(support_revision)}"
)

def stub_binary_filename(self, support_revision, is_console_app):
def stub_binary_filename(self, support_revision: str, is_console_app: bool) -> str:
"""The filename for the stub binary."""
stub_type = "Console" if is_console_app else "GUI"
return f"{stub_type}-Stub-{self.python_version_tag}-b{support_revision}.zip"

def stub_binary_url(self, support_revision, is_console_app):
def stub_binary_url(self, support_revision: str, is_console_app: bool) -> str:
"""The URL of the stub binary to use for apps of this type."""
return (
f"https://briefcase-support.s3.amazonaws.com/python/{self.python_version_tag}/{self.platform}/"
+ self.stub_binary_filename(support_revision, is_console_app)
"https://briefcase-support.s3.amazonaws.com/python/"
f"{self.python_version_tag}/"
f"{self.platform}/"
f"{self.stub_binary_filename(support_revision, is_console_app)}"
)

def icon_targets(self, app: AppConfig):
Expand Down Expand Up @@ -260,22 +265,12 @@ def _unpack_support_package(self, support_file_path, support_path):
:param support_file_path: The path to the support file to be unpacked.
:param support_path: The path where support files should be unpacked.
"""
# Additional protections for unpacking tar files were introduced in Python 3.12.
# This enables the behavior that will be the default in Python 3.14.
# However, the protections can only be enabled for tar files...not zip files.
is_zip = support_file_path.name.endswith("zip")
if sys.version_info >= (3, 12) and not is_zip: # pragma: no-cover-if-lt-py312
tarfile_kwargs = {"filter": "data"}
else:
tarfile_kwargs = {}

try:
with self.input.wait_bar("Unpacking support package..."):
support_path.mkdir(parents=True, exist_ok=True)
self.tools.shutil.unpack_archive(
self.tools.file.unpack_archive(
support_file_path,
extract_dir=support_path,
**tarfile_kwargs,
)
except (shutil.ReadError, EOFError) as e:
raise InvalidSupportPackage(support_file_path) from e
Expand Down Expand Up @@ -376,7 +371,7 @@ def _download_support_package(self, app: AppConfig):

# Download the support file, caching the result
# in the user's briefcase support cache directory.
return self.tools.download.file(
return self.tools.file.download(
url=support_package_url,
download_path=download_path,
role="support package",
Expand All @@ -401,13 +396,8 @@ def cleanup_stub_binary(self, app: AppConfig):
:param app: The config object for the app
"""
with self.input.wait_bar("Removing existing stub binary..."):
binary_executable_path = self.binary_executable_path(app)
if binary_executable_path.exists():
binary_executable_path.unlink()

unbuilt_executable_path = self.unbuilt_executable_path(app)
if unbuilt_executable_path.exists():
unbuilt_executable_path.unlink()
self.binary_executable_path(app).unlink(missing_ok=True)
self.unbuilt_executable_path(app).unlink(missing_ok=True)

def install_stub_binary(self, app: AppConfig):
"""Install the application stub binary into the "unbuilt" location.
Expand All @@ -420,19 +410,28 @@ def install_stub_binary(self, app: AppConfig):
with self.input.wait_bar("Installing stub binary..."):
# Ensure the folder for the stub binary exists
unbuilt_executable_path.parent.mkdir(exist_ok=True, parents=True)
# Install the stub binary into the unbuilt location. Allow for both raw
# and compressed artefacts.
if stub_binary_path.suffix in {".zip", ".tar.gz", ".tgz"}:
self.tools.shutil.unpack_archive(
stub_binary_path,
extract_dir=unbuilt_executable_path.parent,
)

# Install the stub binary into the unbuilt location.
# Allow for both raw and compressed artefacts.
try:
if self.tools.file.is_archive(stub_binary_path):
self.tools.file.unpack_archive(
stub_binary_path,
extract_dir=unbuilt_executable_path.parent,
)
elif stub_binary_path.is_file():
self.tools.shutil.copyfile(
stub_binary_path, unbuilt_executable_path
)
else:
raise InvalidStubBinary(stub_binary_path)
except (shutil.ReadError, EOFError, OSError) as e:
raise InvalidStubBinary(stub_binary_path) from e
else:
self.tools.shutil.copyfile(stub_binary_path, unbuilt_executable_path)
# Ensure the binary is executable
self.tools.os.chmod(unbuilt_executable_path, 0o755)
# Ensure the binary is executable
self.tools.os.chmod(unbuilt_executable_path, 0o755)

def _download_stub_binary(self, app: AppConfig):
def _download_stub_binary(self, app: AppConfig) -> Path:
try:
# Work out if the app defines a custom override for
# the support package URL.
Expand Down Expand Up @@ -481,7 +480,7 @@ def _download_stub_binary(self, app: AppConfig):

# Download the stub binary, caching the result
# in the user's briefcase stub cache directory.
return self.tools.download.file(
return self.tools.file.download(
url=stub_binary_url,
download_path=download_path,
role="stub binary",
Expand Down
14 changes: 10 additions & 4 deletions src/briefcase/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(self, requested, choices):

def __str__(self):
choices = ", ".join(sorted(self.choices, key=str.lower))
return f"Invalid format '{self.requested}'; (choose from: {choices})"
return f"Invalid format {self.requested!r}; (choose from: {choices})"


class UnsupportedCommandError(BriefcaseError):
Expand Down Expand Up @@ -156,12 +156,18 @@ def __init__(self, platform):
class InvalidSupportPackage(BriefcaseCommandError):
def __init__(self, filename):
self.filename = filename
super().__init__(f"Unable to unpack support package {filename!r}")
super().__init__(f"Unable to unpack support package {str(filename)!r}.")


class InvalidStubBinary(BriefcaseCommandError):
def __init__(self, filename):
self.filename = filename
super().__init__(f"Unable to unpack or copy stub binary {str(filename)!r}.")


class MissingAppMetadata(BriefcaseCommandError):
def __init__(self, app_bundle_path):
super().__init__(f"Unable to find '{app_bundle_path / 'briefcase.toml'}'")
super().__init__(f"Unable to find {str(app_bundle_path / 'briefcase.toml')!r}")


class MissingSupportPackage(BriefcaseCommandError):
Expand Down Expand Up @@ -223,7 +229,7 @@ class InvalidDeviceError(BriefcaseCommandError):
def __init__(self, id_type, device):
self.id_type = id_type
self.device = device
super().__init__(msg=f"Invalid device {id_type} '{device}'")
super().__init__(msg=f"Invalid device {id_type} {device!r}")


class CorruptToolError(BriefcaseCommandError):
Expand Down
4 changes: 2 additions & 2 deletions src/briefcase/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
android_sdk,
cookiecutter,
docker,
download,
file,
flatpak,
git,
java,
Expand All @@ -19,7 +19,7 @@
"android_sdk",
"cookiecutter",
"docker",
"download",
"file",
"flatpak",
"git",
"java",
Expand Down
10 changes: 4 additions & 6 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import shlex
import shutil
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
Expand Down Expand Up @@ -387,7 +386,7 @@ def uninstall(self):

def install(self):
"""Download and install the Android SDK."""
cmdline_tools_zip_path = self.tools.download.file(
cmdline_tools_zip_path = self.tools.file.download(
url=self.cmdline_tools_url,
download_path=self.tools.base_path,
role="Android SDK Command-Line Tools",
Expand All @@ -408,7 +407,7 @@ def install(self):
):
self.cmdline_tools_path.parent.mkdir(parents=True, exist_ok=True)
try:
self.tools.shutil.unpack_archive(
self.tools.file.unpack_archive(
cmdline_tools_zip_path, extract_dir=self.cmdline_tools_path.parent
)
except (shutil.ReadError, EOFError) as e:
Expand Down Expand Up @@ -800,7 +799,7 @@ def verify_emulator_skin(self, skin: str):
f"artwork/resources/device-art-resources/{skin}.tar.gz"
)

skin_tgz_path = self.tools.download.file(
skin_tgz_path = self.tools.file.download(
url=skin_url,
download_path=self.root_path,
role=f"{skin} device skin",
Expand All @@ -809,10 +808,9 @@ def verify_emulator_skin(self, skin: str):
# Unpack skin archive
with self.tools.input.wait_bar("Installing device skin..."):
try:
self.tools.shutil.unpack_archive(
self.tools.file.unpack_archive(
skin_tgz_path,
extract_dir=skin_path,
**({"filter": "data"} if sys.version_info >= (3, 12) else {}),
)
except (shutil.ReadError, EOFError) as e:
raise BriefcaseCommandError(
Expand Down
4 changes: 2 additions & 2 deletions src/briefcase/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

from briefcase.integrations.android_sdk import AndroidSDK
from briefcase.integrations.docker import Docker, DockerAppContext
from briefcase.integrations.download import Download
from briefcase.integrations.file import File
from briefcase.integrations.flatpak import Flatpak
from briefcase.integrations.java import JDK
from briefcase.integrations.linuxdeploy import LinuxDeploy
Expand Down Expand Up @@ -148,7 +148,7 @@ class ToolCache(Mapping):
android_sdk: AndroidSDK
app_context: Subprocess | DockerAppContext
docker: Docker
download: Download
file: File
flatpak: Flatpak
git: git_
java: JDK
Expand Down
Loading