Skip to content
New issue

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

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

Already on GitHub? # to your account

Fixes #420 - Ensure all libraries are fully deployed in AppImage. #451

Merged
merged 6 commits into from
Jul 15, 2020
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/420.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Binary modules in Linux AppImages are now processed correctly, ensuring that no references to system libraries are retained in the AppImage.
57 changes: 56 additions & 1 deletion src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
from urllib.parse import urlparse

import requests
import toml
from cookiecutter.main import cookiecutter
from cookiecutter.repository import is_repo_url

from briefcase import __version__, integrations
from briefcase.config import AppConfig, GlobalConfig, parse_config
from briefcase.config import AppConfig, BaseConfig, GlobalConfig, parse_config
from briefcase.console import Console
from briefcase.exceptions import (
BadNetworkResourceError,
Expand Down Expand Up @@ -111,6 +112,7 @@ def __init__(self, base_path, home_path=Path.home(), apps=None, input_enabled=Tr

self.global_config = None
self.apps = {} if apps is None else apps
self._path_index = {}

# Some details about the host machine
self.host_arch = platform.machine()
Expand Down Expand Up @@ -249,6 +251,59 @@ def distribution_path(self, app):
"""
...

def _load_path_index(self, app: BaseConfig):
"""
Load the path index from the index file provided by the app template

:param app: The config object for the app
:return: The contents of the application path index.
"""
with (self.bundle_path(app) / 'briefcase.toml').open() as f:
self._path_index[app] = toml.load(f)['paths']
return self._path_index[app]

def support_path(self, app: BaseConfig):
"""
Obtain the path into which the support package should be unpacked

:param app: The config object for the app
:return: The full path where the support package should be unpacked.
"""
# If the index file hasn't been loaded for this app, load it.
try:
path_index = self._path_index[app]
except KeyError:
path_index = self._load_path_index(app)
return self.bundle_path(app) / path_index['support_path']

def app_packages_path(self, app: BaseConfig):
"""
Obtain the path into which dependencies should be installed

:param app: The config object for the app
:return: The full path where application dependencies should be installed.
"""
# If the index file hasn't been loaded for this app, load it.
try:
path_index = self._path_index[app]
except KeyError:
path_index = self._load_path_index(app)
return self.bundle_path(app) / path_index['app_packages_path']

def app_path(self, app: BaseConfig):
"""
Obtain the path into which the application should be installed.

:param app: The config object for the app
:return: The full path where application code should be installed.
"""
# If the index file hasn't been loaded for this app, load it.
try:
path_index = self._path_index[app]
except KeyError:
path_index = self._load_path_index(app)
return self.bundle_path(app) / path_index['app_path']

def app_module_path(self, app):
"""
Find the path for the application module for an app.
Expand Down
55 changes: 0 additions & 55 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from typing import Optional
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

import toml
from cookiecutter import exceptions as cookiecutter_exceptions
from requests import exceptions as requests_exceptions

Expand Down Expand Up @@ -114,7 +113,6 @@ class CreateCommand(BaseCommand):

def __init__(self, *args, **options):
super().__init__(*args, **options)
self._path_index = {}
self._s3 = None

@property
Expand All @@ -141,59 +139,6 @@ def support_package_url(self):
query=urlencode(self.support_package_url_query)
)

def _load_path_index(self, app: BaseConfig):
"""
Load the path index from the index file provided by the app template

:param app: The config object for the app
:return: The contents of the application path index.
"""
with (self.bundle_path(app) / 'briefcase.toml').open() as f:
self._path_index[app] = toml.load(f)['paths']
return self._path_index[app]

def support_path(self, app: BaseConfig):
"""
Obtain the path into which the support package should be unpacked

:param app: The config object for the app
:return: The full path where the support package should be unpacked.
"""
# If the index file hasn't been loaded for this app, load it.
try:
path_index = self._path_index[app]
except KeyError:
path_index = self._load_path_index(app)
return self.bundle_path(app) / path_index['support_path']

def app_packages_path(self, app: BaseConfig):
"""
Obtain the path into which dependencies should be installed

:param app: The config object for the app
:return: The full path where application dependencies should be installed.
"""
# If the index file hasn't been loaded for this app, load it.
try:
path_index = self._path_index[app]
except KeyError:
path_index = self._load_path_index(app)
return self.bundle_path(app) / path_index['app_packages_path']

def app_path(self, app: BaseConfig):
"""
Obtain the path into which the application should be installed.

:param app: The config object for the app
:return: The full path where application code should be installed.
"""
# If the index file hasn't been loaded for this app, load it.
try:
path_index = self._path_index[app]
except KeyError:
path_index = self._load_path_index(app)
return self.bundle_path(app) / path_index['app_path']

def icon_targets(self, app: BaseConfig):
"""
Obtain the dictionary of icon targets that the template requires.
Expand Down
32 changes: 32 additions & 0 deletions src/briefcase/integrations/linuxdeploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from requests import exceptions as requests_exceptions

from briefcase.exceptions import NetworkFailure


def verify_linuxdeploy(command):
"""
Verify that LinuxDeploy is available.

:param command: The command that needs to use linuxdeploy
:returns: The path to the linuxdeploy AppImage.
"""

linuxdeploy_download_url = (
'https://github.com/linuxdeploy/linuxdeploy/'
'releases/download/continuous/linuxdeploy-{command.host_arch}.AppImage'.format(
command=command
)
)

try:
print()
print("Ensure we have the linuxdeploy AppImage...")
linuxdeploy_appimage_path = command.download_url(
url=linuxdeploy_download_url,
download_path=command.dot_briefcase_path / 'tools'
)
command.os.chmod(str(linuxdeploy_appimage_path), 0o755)
except requests_exceptions.ConnectionError:
raise NetworkFailure('downloading linuxdeploy AppImage')

return linuxdeploy_appimage_path
55 changes: 25 additions & 30 deletions src/briefcase/platforms/linux/appimage.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import subprocess
from contextlib import contextmanager

from requests import exceptions as requests_exceptions

from briefcase.commands import (
BuildCommand,
CreateCommand,
Expand All @@ -12,14 +10,18 @@
UpdateCommand
)
from briefcase.config import BaseConfig
from briefcase.exceptions import BriefcaseCommandError, NetworkFailure
from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.docker import verify_docker
from briefcase.integrations.linuxdeploy import verify_linuxdeploy
from briefcase.platforms.linux import LinuxMixin


class LinuxAppImageMixin(LinuxMixin):
output_format = 'appimage'

def appdir_path(self, app):
return self.bundle_path(app) / "{app.formal_name}.AppDir".format(app=app)

def binary_path(self, app):
binary_name = app.formal_name.replace(' ', '_')
return self.platform_path / '{binary_name}-{app.version}-{self.host_arch}.AppImage'.format(
Expand Down Expand Up @@ -150,28 +152,9 @@ class LinuxAppImageUpdateCommand(LinuxAppImageMixin, UpdateCommand):
class LinuxAppImageBuildCommand(LinuxAppImageMixin, BuildCommand):
description = "Build a Linux AppImage."

@property
def linuxdeploy_download_url(self):
return (
'https://github.com/linuxdeploy/linuxdeploy/'
'releases/download/continuous/linuxdeploy-{self.host_arch}.AppImage'.format(
self=self
)
)

def verify_tools(self):
super().verify_tools()

try:
print()
print("Ensure we have the linuxdeploy AppImage...")
self.linuxdeploy_appimage = self.download_url(
url=self.linuxdeploy_download_url,
download_path=self.dot_briefcase_path / 'tools'
)
self.os.chmod(str(self.linuxdeploy_appimage), 0o755)
except requests_exceptions.ConnectionError:
raise NetworkFailure('downloading linuxdeploy AppImage')
self.linuxdeploy_appimage_path = verify_linuxdeploy(self)

def build_app(self, app: BaseConfig, **kwargs):
"""
Expand All @@ -190,22 +173,34 @@ def build_app(self, app: BaseConfig, **kwargs):
env = {
'VERSION': app.version
}
appdir_path = self.bundle_path(app) / "{app.formal_name}.AppDir".format(
app=app
)

# Find all the .so files in app and app_packages,
# so they can be passed in to linuxdeploy to have their
# dependencies added to the AppImage. Looks for any .so file
# in the application, and make sure it is marked for deployment.
so_folders = set()
for so_file in self.appdir_path(app).glob('**/*.so'):
so_folders.add(so_file.parent)

deploy_deps_args = []
for folder in sorted(so_folders):
deploy_deps_args.extend(["--deploy-deps-only", str(folder)])

# Build the app image. We use `--appimage-extract-and-run`
# because AppImages won't run natively inside Docker.
with self.dockerize(app) as docker:
docker.run(
[
str(self.linuxdeploy_appimage),
str(self.linuxdeploy_appimage_path),
"--appimage-extract-and-run",
"--appdir={appdir_path}".format(appdir_path=appdir_path),
"--appdir={appdir_path}".format(appdir_path=self.appdir_path(app)),
"-d", str(
appdir_path / "{app.bundle}.{app.app_name}.desktop".format(
self.appdir_path(app) / "{app.bundle}.{app.app_name}.desktop".format(
app=app,
)
),
"-o", "appimage",
],
] + deploy_deps_args,
env=env,
check=True,
cwd=str(self.platform_path)
Expand Down
6 changes: 5 additions & 1 deletion tests/integrations/docker/test_verify_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import pytest

from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.docker import Docker, verify_docker, _verify_docker_can_run
from briefcase.integrations.docker import (
Docker,
_verify_docker_can_run,
verify_docker
)


@pytest.fixture
Expand Down
Empty file.
39 changes: 39 additions & 0 deletions tests/integrations/linuxdeploy/test_verify_linuxdeploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from unittest.mock import MagicMock

import pytest
from requests import exceptions as requests_exceptions

from briefcase.exceptions import NetworkFailure
from briefcase.integrations.linuxdeploy import verify_linuxdeploy


@pytest.fixture
def mock_command():
command = MagicMock()
command.host_os = 'wonky'

return command


def test_verify_linuxdeploy(mock_command):
"The build process invokes verify_tools, which retrieves linuxdeploy"
# Mock a successful download
mock_command.download_url.return_value = 'new-downloaded-file'

# Verify the
linuxdeploy_appimage = verify_linuxdeploy(mock_command)

# The downloaded file will be made executable
mock_command.os.chmod.assert_called_with('new-downloaded-file', 0o755)

# The build command retains the path to the downloaded file.
assert linuxdeploy_appimage == 'new-downloaded-file'


def test_verify_linuxdeploy_download_failure(mock_command):
"If the download of linuxdeploy fails, an error is raised"

mock_command.download_url.side_effect = requests_exceptions.ConnectionError

with pytest.raises(NetworkFailure):
verify_linuxdeploy(mock_command)
Loading