Skip to content

Commit 8b09dea

Browse files
committed
Tweaks to out-of-template stub binary implementation
1 parent d2a59a3 commit 8b09dea

7 files changed

Lines changed: 232 additions & 77 deletions

File tree

changes/1871.misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Updated error handling and archive unpacking for the stub binary.

src/briefcase/commands/create.py

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from briefcase.config import AppConfig
1414
from briefcase.exceptions import (
1515
BriefcaseCommandError,
16+
InvalidStubBinary,
1617
InvalidSupportPackage,
1718
MissingAppSources,
1819
MissingNetworkResourceError,
@@ -86,31 +87,35 @@ class CreateCommand(BaseCommand):
8687
hidden_app_properties = {"permission"}
8788

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

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

97-
def support_package_url(self, support_revision):
98+
def support_package_url(self, support_revision: str) -> str:
9899
"""The URL of the support package to use for apps of this type."""
99100
return (
100-
f"https://briefcase-support.s3.amazonaws.com/python/{self.python_version_tag}/{self.platform}/"
101-
+ self.support_package_filename(support_revision)
101+
"https://briefcase-support.s3.amazonaws.com/python/"
102+
f"{self.python_version_tag}/"
103+
f"{self.platform}/"
104+
f"{self.support_package_filename(support_revision)}"
102105
)
103106

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

109-
def stub_binary_url(self, support_revision, is_console_app):
112+
def stub_binary_url(self, support_revision: str, is_console_app: bool) -> str:
110113
"""The URL of the stub binary to use for apps of this type."""
111114
return (
112-
f"https://briefcase-support.s3.amazonaws.com/python/{self.python_version_tag}/{self.platform}/"
113-
+ self.stub_binary_filename(support_revision, is_console_app)
115+
"https://briefcase-support.s3.amazonaws.com/python/"
116+
f"{self.python_version_tag}/"
117+
f"{self.platform}/"
118+
f"{self.stub_binary_filename(support_revision, is_console_app)}"
114119
)
115120

116121
def icon_targets(self, app: AppConfig):
@@ -260,22 +265,13 @@ def _unpack_support_package(self, support_file_path, support_path):
260265
:param support_file_path: The path to the support file to be unpacked.
261266
:param support_path: The path where support files should be unpacked.
262267
"""
263-
# Additional protections for unpacking tar files were introduced in Python 3.12.
264-
# This enables the behavior that will be the default in Python 3.14.
265-
# However, the protections can only be enabled for tar files...not zip files.
266-
is_zip = support_file_path.name.endswith("zip")
267-
if sys.version_info >= (3, 12) and not is_zip: # pragma: no-cover-if-lt-py312
268-
tarfile_kwargs = {"filter": "data"}
269-
else:
270-
tarfile_kwargs = {}
271-
272268
try:
273269
with self.input.wait_bar("Unpacking support package..."):
274270
support_path.mkdir(parents=True, exist_ok=True)
275271
self.tools.shutil.unpack_archive(
276272
support_file_path,
277273
extract_dir=support_path,
278-
**tarfile_kwargs,
274+
**self.tools.unpack_archive_kwargs(support_file_path),
279275
)
280276
except (shutil.ReadError, EOFError) as e:
281277
raise InvalidSupportPackage(support_file_path) from e
@@ -401,13 +397,8 @@ def cleanup_stub_binary(self, app: AppConfig):
401397
:param app: The config object for the app
402398
"""
403399
with self.input.wait_bar("Removing existing stub binary..."):
404-
binary_executable_path = self.binary_executable_path(app)
405-
if binary_executable_path.exists():
406-
binary_executable_path.unlink()
407-
408-
unbuilt_executable_path = self.unbuilt_executable_path(app)
409-
if unbuilt_executable_path.exists():
410-
unbuilt_executable_path.unlink()
400+
self.binary_executable_path(app).unlink(missing_ok=True)
401+
self.unbuilt_executable_path(app).unlink(missing_ok=True)
411402

412403
def install_stub_binary(self, app: AppConfig):
413404
"""Install the application stub binary into the "unbuilt" location.
@@ -420,19 +411,41 @@ def install_stub_binary(self, app: AppConfig):
420411
with self.input.wait_bar("Installing stub binary..."):
421412
# Ensure the folder for the stub binary exists
422413
unbuilt_executable_path.parent.mkdir(exist_ok=True, parents=True)
423-
# Install the stub binary into the unbuilt location. Allow for both raw
424-
# and compressed artefacts.
425-
if stub_binary_path.suffix in {".zip", ".tar.gz", ".tgz"}:
426-
self.tools.shutil.unpack_archive(
427-
stub_binary_path,
428-
extract_dir=unbuilt_executable_path.parent,
429-
)
414+
415+
# Determine if stub binary is a packed archive
416+
supported_archive_extensions = {
417+
ext for format in shutil.get_unpack_formats() for ext in format[1]
418+
}
419+
stub_path_exts = {
420+
# captures extensions like .tar.gz, .tar.bz2, etc.
421+
"".join(stub_binary_path.suffixes[-2:]),
422+
# as well as .tar, .zip, etc.
423+
stub_binary_path.suffix,
424+
}
425+
is_archive = not stub_path_exts.isdisjoint(supported_archive_extensions)
426+
427+
# Install the stub binary into the unbuilt location.
428+
# Allow for both raw and compressed artefacts.
429+
try:
430+
if is_archive:
431+
self.tools.shutil.unpack_archive(
432+
stub_binary_path,
433+
extract_dir=unbuilt_executable_path.parent,
434+
**self.tools.unpack_archive_kwargs(stub_binary_path),
435+
)
436+
elif stub_binary_path.is_file():
437+
self.tools.shutil.copyfile(
438+
stub_binary_path, unbuilt_executable_path
439+
)
440+
else:
441+
raise InvalidStubBinary(stub_binary_path)
442+
except (shutil.ReadError, EOFError, OSError) as e:
443+
raise InvalidStubBinary(stub_binary_path) from e
430444
else:
431-
self.tools.shutil.copyfile(stub_binary_path, unbuilt_executable_path)
432-
# Ensure the binary is executable
433-
self.tools.os.chmod(unbuilt_executable_path, 0o755)
445+
# Ensure the binary is executable
446+
self.tools.os.chmod(unbuilt_executable_path, 0o755)
434447

435-
def _download_stub_binary(self, app: AppConfig):
448+
def _download_stub_binary(self, app: AppConfig) -> Path:
436449
try:
437450
# Work out if the app defines a custom override for
438451
# the support package URL.

src/briefcase/exceptions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@ def __init__(self, platform):
156156
class InvalidSupportPackage(BriefcaseCommandError):
157157
def __init__(self, filename):
158158
self.filename = filename
159-
super().__init__(f"Unable to unpack support package {filename!r}")
159+
super().__init__(f"Unable to unpack support package '{filename}'.")
160+
161+
162+
class InvalidStubBinary(BriefcaseCommandError):
163+
def __init__(self, filename):
164+
self.filename = filename
165+
super().__init__(f"Unable to unpack or copy stub binary '{filename}'.")
160166

161167

162168
class MissingAppMetadata(BriefcaseCommandError):

src/briefcase/integrations/android_sdk.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import shlex
77
import shutil
88
import subprocess
9-
import sys
109
import time
1110
from datetime import datetime
1211
from pathlib import Path
@@ -812,7 +811,7 @@ def verify_emulator_skin(self, skin: str):
812811
self.tools.shutil.unpack_archive(
813812
skin_tgz_path,
814813
extract_dir=skin_path,
815-
**({"filter": "data"} if sys.version_info >= (3, 12) else {}),
814+
**self.tools.unpack_archive_kwargs(skin_tgz_path),
816815
)
817816
except (shutil.ReadError, EOFError) as e:
818817
raise BriefcaseCommandError(

src/briefcase/integrations/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,22 @@ def system_encoding(self) -> str:
234234

235235
return encoding.upper()
236236

237+
def unpack_archive_kwargs(self, archive_path: str | os.PathLike) -> dict[str, str]:
238+
"""Additional options for unpacking archives based on its type.
239+
240+
Should be used for all calls to `shutil.unpack_archive()`.
241+
242+
Additional protections for unpacking tar files were introduced in Python 3.12.
243+
This enables the behavior that will be the default in Python 3.14.
244+
However, the protections can only be enabled for tar files...not zip files.
245+
"""
246+
is_zip = str(archive_path).endswith(".zip")
247+
if sys.version_info >= (3, 12) and not is_zip: # pragma: no-cover-if-lt-py312
248+
unpack_kwargs = {"filter": "data"}
249+
else:
250+
unpack_kwargs = {}
251+
return unpack_kwargs
252+
237253
def __getitem__(self, app: AppConfig) -> ToolCache:
238254
return self.app_tools[app]
239255

0 commit comments

Comments
 (0)