Skip to content

Commit

Permalink
Merge pull request #993 from freakboy3742/linux-relative-refs
Browse files Browse the repository at this point in the history
Allow for local file references in requirements when building AppImages
  • Loading branch information
mhsmith authored Dec 7, 2022
2 parents aab0107 + 2f8c2dc commit 6620f4d
Show file tree
Hide file tree
Showing 17 changed files with 710 additions and 80 deletions.
1 change: 1 addition & 0 deletions changes/991.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``briefcase open linux appimage`` now starts a shell session in the Docker context, rather than opening the project folder.
1 change: 1 addition & 0 deletions changes/992.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Local file references in requirements no longer break AppImage builds.
77 changes: 54 additions & 23 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,6 @@ def _write_requirements_file(
:param requirements_path: The full path to a requirements.txt file that
will be written.
"""
# Windows allows both / and \ as a path separator in requirements.
separators = [os.sep]
if os.altsep:
separators.append(os.altsep)

with self.input.wait_bar("Writing requirements file..."):
with requirements_path.open("w", encoding="utf-8") as f:
Expand All @@ -439,14 +435,21 @@ def _write_requirements_file(
# If the requirement is a local path, convert it to
# absolute, because Flatpak moves the requirements file
# to a different place before using it.
if any(sep in requirement for sep in separators) and (
not _has_url(requirement)
):
if _is_local_requirement(requirement):
# We use os.path.abspath() rather than Path.resolve()
# because we *don't* want Path's symlink resolving behavior.
requirement = os.path.abspath(self.base_path / requirement)
f.write(f"{requirement}\n")

def _pip_requires(self, requires: List[str]):
"""Convert the list of requirements to be passed to pip into its final
form.
:param requires: The user-specified list of app requirements
:returns: The final list of requirement arguments to pass to pip
"""
return requires

def _extra_pip_args(self, app: BaseConfig):
"""Any additional arguments that must be passed to pip when installing
packages.
Expand All @@ -456,6 +459,26 @@ def _extra_pip_args(self, app: BaseConfig):
"""
return []

def _pip_kwargs(self, app: BaseConfig):
"""Generate the kwargs to pass when invoking pip.
:param app: The app configuration
:returns: The kwargs to pass to the pip call
"""
# If there is a support package provided, add the cross-platform
# folder of the support package to the PYTHONPATH. This allows
# a support package to specify a sitecustomize.py that will make
# pip behave as if it was being run on the target platform.
pip_kwargs = {}
try:
pip_kwargs["env"] = {
"PYTHONPATH": str(self.support_path(app) / "platform-site"),
}
except KeyError:
pass

return pip_kwargs

def _install_app_requirements(
self,
app: BaseConfig,
Expand All @@ -477,18 +500,6 @@ def _install_app_requirements(
# Install requirements
if requires:
with self.input.wait_bar("Installing app requirements..."):
# If there is a support package provided, add the cross-platform
# folder of the support package to the PYTHONPATH. This allows
# a support package to specify a sitecustomize.py that will make
# pip behave as if it was being run on the target platform.
pip_kwargs = {}
try:
pip_kwargs["env"] = {
"PYTHONPATH": str(self.support_path(app) / "platform-site"),
}
except KeyError:
pass

try:
self.tools[app].app_context.run(
[
Expand All @@ -502,9 +513,9 @@ def _install_app_requirements(
f"--target={app_packages_path}",
]
+ self._extra_pip_args(app)
+ requires,
+ self._pip_requires(requires),
check=True,
**pip_kwargs,
**self._pip_kwargs(app),
)
except subprocess.CalledProcessError as e:
raise RequirementsInstallError() from e
Expand Down Expand Up @@ -828,9 +839,15 @@ def __call__(self, app: Optional[BaseConfig] = None, **options):
return state


# Detects any of the URL schemes supported by pip
# (https://pip.pypa.io/en/stable/topics/vcs-support/).
def _has_url(requirement):
"""Determine if the requirement is defined as a URL.
Detects any of the URL schemes supported by pip
(https://pip.pypa.io/en/stable/topics/vcs-support/).
:param requirement: The requirement to check
:returns: True if the requirement is a URL supported by pip.
"""
return any(
f"{scheme}:" in requirement
for scheme in (
Expand All @@ -841,3 +858,17 @@ def _has_url(requirement):
+ ["bzr+http", "bzr+https", "bzr+ssh", "bzr+sftp", "bzr+ftp", "bzr+lp"]
)
)


def _is_local_requirement(requirement):
"""Determine if the requirement is a local file path.
:param requirement: The requirement to check
:returns: True if the requirement is a local file path
"""
# Windows allows both / and \ as a path separator in requirements.
separators = [os.sep]
if os.altsep:
separators.append(os.altsep)

return any(sep in requirement for sep in separators) and (not _has_url(requirement))
10 changes: 5 additions & 5 deletions src/briefcase/commands/open.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
class OpenCommand(BaseCommand):
command = "open"

def open_project(self, project_path):
def _open_app(self, app: BaseConfig):
if self.tools.host_os == "Windows":
self.tools.os.startfile(project_path)
self.tools.os.startfile(self.project_path(app))
elif self.tools.host_os == "Darwin":
self.tools.subprocess.Popen(["open", project_path])
self.tools.subprocess.Popen(["open", self.project_path(app)])
else:
self.tools.subprocess.Popen(["xdg-open", project_path])
self.tools.subprocess.Popen(["xdg-open", self.project_path(app)])

def open_app(self, app: BaseConfig, **options):
"""Open the project for an app.
Expand All @@ -33,7 +33,7 @@ def open_app(self, app: BaseConfig, **options):
f"Opening {self.project_path(app).relative_to(self.base_path)}...",
prefix=app.app_name,
)
self.open_project(project_path)
self._open_app(app)

return state

Expand Down
49 changes: 35 additions & 14 deletions src/briefcase/integrations/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ def _dockerize_path(self, arg: str):

return arg

def _dockerize_args(self, args, env=None):
def _dockerize_args(self, args, interactive=False, mounts=None, env=None):
"""Convert arguments and environment into a Docker-compatible form.
Convert an argument and environment specification into a form that can
be used as arguments to invoke Docker. This involves:
Expand All @@ -330,17 +330,30 @@ def _dockerize_args(self, args, env=None):
:returns: A list of arguments that can be used to invoke the command
inside a docker container.
"""
docker_args = ["docker", "run", "--rm"]

# Add "-it" if in interactive mode
if interactive:
docker_args.append("-it")

# Add default volume mounts for the app folder, plus the Briefcase data
# path.
#
# The :z suffix on volume mounts allows SELinux to modify the host
# mount; it is ignored on non-SELinux platforms.
docker_args = [
"docker",
"run",
"--volume",
f"{self.host_platform_path}:/app:z",
"--volume",
f"{self.host_data_path}:{self.docker_data_path}:z",
"--rm",
]
docker_args.extend(
[
"--volume",
f"{self.host_platform_path}:/app:z",
"--volume",
f"{self.host_data_path}:{self.docker_data_path}:z",
]
)

# Add any extra volume mounts
if mounts:
for source, target in mounts:
docker_args.extend(["--volume", f"{source}:{target}:z"])

# If any environment variables have been defined, pass them in
# as --env arguments to Docker.
Expand All @@ -356,24 +369,32 @@ def _dockerize_args(self, args, env=None):

return docker_args

def run(self, args, env=None, **kwargs):
def run(self, args, env=None, interactive=False, mounts=None, **kwargs):
"""Run a process inside a Docker container."""
# Any exceptions from running the process are *not* caught.
# This ensures that "docker.run()" behaves as closely to
# "subprocess.run()" as possible.
self.tools.logger.info("Entering Docker context...", prefix=self.app.app_name)
if interactive:
kwargs["stream_output"] = False

self.tools.subprocess.run(
self._dockerize_args(args, env=env),
self._dockerize_args(
args,
interactive=interactive,
mounts=mounts,
env=env,
),
**kwargs,
)
self.tools.logger.info("Leaving Docker context", prefix=self.app.app_name)

def check_output(self, args, env=None, **kwargs):
def check_output(self, args, env=None, mounts=None, **kwargs):
"""Run a process inside a Docker container, capturing output."""
# Any exceptions from running the process are *not* caught.
# This ensures that "docker.check_output()" behaves as closely to
# "subprocess.check_output()" as possible.
return self.tools.subprocess.check_output(
self._dockerize_args(args, env=env),
self._dockerize_args(args, mounts=mounts, env=env),
**kwargs,
)
Loading

0 comments on commit 6620f4d

Please sign in to comment.