Skip to content

Commit

Permalink
Tweaks to out-of-template stub binary implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Jun 9, 2024
1 parent d2a59a3 commit 8b09dea
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 77 deletions.
1 change: 1 addition & 0 deletions changes/1871.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Updated error handling and archive unpacking for the stub binary.
87 changes: 50 additions & 37 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,13 @@ 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(
support_file_path,
extract_dir=support_path,
**tarfile_kwargs,
**self.tools.unpack_archive_kwargs(support_file_path),
)
except (shutil.ReadError, EOFError) as e:
raise InvalidSupportPackage(support_file_path) from e
Expand Down Expand Up @@ -401,13 +397,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 +411,41 @@ 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,
)

# Determine if stub binary is a packed archive
supported_archive_extensions = {
ext for format in shutil.get_unpack_formats() for ext in format[1]
}
stub_path_exts = {
# captures extensions like .tar.gz, .tar.bz2, etc.
"".join(stub_binary_path.suffixes[-2:]),
# as well as .tar, .zip, etc.
stub_binary_path.suffix,
}
is_archive = not stub_path_exts.isdisjoint(supported_archive_extensions)

# Install the stub binary into the unbuilt location.
# Allow for both raw and compressed artefacts.
try:
if is_archive:
self.tools.shutil.unpack_archive(
stub_binary_path,
extract_dir=unbuilt_executable_path.parent,
**self.tools.unpack_archive_kwargs(stub_binary_path),
)
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
8 changes: 7 additions & 1 deletion src/briefcase/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,13 @@ 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 '{filename}'.")


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


class MissingAppMetadata(BriefcaseCommandError):
Expand Down
3 changes: 1 addition & 2 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 @@ -812,7 +811,7 @@ def verify_emulator_skin(self, skin: str):
self.tools.shutil.unpack_archive(
skin_tgz_path,
extract_dir=skin_path,
**({"filter": "data"} if sys.version_info >= (3, 12) else {}),
**self.tools.unpack_archive_kwargs(skin_tgz_path),
)
except (shutil.ReadError, EOFError) as e:
raise BriefcaseCommandError(
Expand Down
16 changes: 16 additions & 0 deletions src/briefcase/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,22 @@ def system_encoding(self) -> str:

return encoding.upper()

def unpack_archive_kwargs(self, archive_path: str | os.PathLike) -> dict[str, str]:
"""Additional options for unpacking archives based on its type.
Should be used for all calls to `shutil.unpack_archive()`.
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 = str(archive_path).endswith(".zip")
if sys.version_info >= (3, 12) and not is_zip: # pragma: no-cover-if-lt-py312
unpack_kwargs = {"filter": "data"}
else:
unpack_kwargs = {}
return unpack_kwargs

def __getitem__(self, app: AppConfig) -> ToolCache:
return self.app_tools[app]

Expand Down
Loading

0 comments on commit 8b09dea

Please sign in to comment.