Skip to content

Commit

Permalink
bdist_appimage: remove zip file, propagate options, fixes docs (#2463)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelotduarte authored Jul 14, 2024
1 parent 9c3daf1 commit 195b4f3
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 53 deletions.
118 changes: 82 additions & 36 deletions cx_Freeze/command/bdist_appimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
import platform
import shutil
import stat
from ctypes.util import find_library
from logging import INFO, WARNING
from pathlib import Path
from textwrap import dedent
from typing import ClassVar
from urllib.request import urlretrieve
from zipfile import ZipFile

from filelock import FileLock
from setuptools import Command
Expand All @@ -23,10 +27,10 @@

__all__ = ["bdist_appimage"]

APPIMAGEKIT_URL = (
"https://github.com/AppImage/AppImageKit/releases/download/continuous"
)
APPIMAGEKIT_TOOL = os.path.expanduser("~/.local/bin/appimagetool")
ARCH = platform.machine()
APPIMAGEKIT_URL = "https://github.com/AppImage/AppImageKit/releases"
APPIMAGEKIT_PATH = f"download/continuous/appimagetool-{ARCH}.AppImage"
APPIMAGEKIT_TOOL = "~/.local/bin/appimagetool"


class bdist_appimage(Command):
Expand All @@ -49,7 +53,11 @@ class bdist_appimage(Command):
"b",
"directory of built executables and dependent files",
),
("dist-dir=", "d", "directory to put final built distributions in"),
(
"dist-dir=",
"d",
"directory to put final built distributions in [default: dist]",
),
(
"skip-build",
None,
Expand All @@ -66,7 +74,6 @@ class bdist_appimage(Command):

def initialize_options(self) -> None:
self.appimagekit = None
self._appimage_extract_and_run = False

self.bdist_base = None
self.build_dir = None
Expand All @@ -78,6 +85,8 @@ def initialize_options(self) -> None:
self.fullname = None
self.silent = None

self._warnings = []

def finalize_options(self) -> None:
if os.name != "posix":
msg = (
Expand All @@ -86,56 +95,77 @@ def finalize_options(self) -> None:
)
raise PlatformError(msg)

self.set_undefined_options("build_exe", ("build_exe", "build_dir"))
# inherit options
self.set_undefined_options(
"build_exe",
("build_exe", "build_dir"),
("silent", "silent"),
)
self.set_undefined_options(
"bdist",
("bdist_base", "bdist_base"),
("dist_dir", "dist_dir"),
("skip_build", "skip_build"),
)
# for the bdist commands, there is a chance that build_exe has already
# been executed, so check skip_build if build_exe have_run
if not self.skip_build and self.distribution.have_run.get("build_exe"):
self.skip_build = 1

if self.target_name is None:
self.target_name = self.distribution.get_name()
if self.distribution.metadata.name:
self.target_name = self.distribution.metadata.name
else:
executables = self.distribution.executables
executable = executables[0]
self.warn_delayed(
"using the first executable as target_name: "
f"{executable.target_name}"
)
self.target_name = executable.target_name

if self.target_version is None and self.distribution.metadata.version:
self.target_version = self.distribution.metadata.version
arch = platform.machine()

name = self.target_name
version = self.target_version or self.distribution.get_version()
version = self.target_version
name, ext = os.path.splitext(name)
if ext == ".AppImage":
self.app_name = self.target_name
self.fullname = name
elif self.target_version:
self.app_name = f"{name}-{version}-{arch}.AppImage"
elif version:
self.app_name = f"{name}-{version}-{ARCH}.AppImage"
self.fullname = f"{name}-{version}"
else:
self.app_name = f"{name}-{arch}.AppImage"
self.app_name = f"{name}-{ARCH}.AppImage"
self.fullname = name

if self.silent is not None:
self.verbose = 0 if self.silent else 2
build_exe = self.distribution.command_obj.get("build_exe")
if build_exe:
build_exe.silent = self.silent

# validate or download appimagekit
self._get_appimagekit()

def _get_appimagekit(self) -> None:
"""Fetch AppImageKit from the web if not available locally."""
if self.appimagekit is None:
self.appimagekit = APPIMAGEKIT_TOOL
appimagekit = self.appimagekit
appimagekit = os.path.expanduser(self.appimagekit or APPIMAGEKIT_TOOL)
appimagekit_dir = os.path.dirname(appimagekit)
self.mkpath(appimagekit_dir)
with FileLock(appimagekit + ".lock"):
if not os.path.exists(appimagekit):
self.announce(
f"download and install AppImageKit from {APPIMAGEKIT_URL}"
f"download and install AppImageKit from {APPIMAGEKIT_URL}",
INFO,
)
arch = platform.machine()
name = f"appimagetool-{arch}.AppImage"
name = os.path.basename(APPIMAGEKIT_PATH)
filename = os.path.join(appimagekit_dir, name)
if not os.path.exists(filename):
urlretrieve( # noqa: S310
os.path.join(APPIMAGEKIT_URL, name), filename
os.path.join(APPIMAGEKIT_URL, APPIMAGEKIT_PATH),
filename,
)
os.chmod(filename, stat.S_IRWXU)
if not os.path.exists(appimagekit):
Expand All @@ -144,11 +174,7 @@ def _get_appimagekit(self) -> None:
(filename, appimagekit),
msg=f"linking {appimagekit} -> {filename}",
)

try:
self.spawn([appimagekit, "--version"])
except Exception: # noqa: BLE001
self._appimage_extract_and_run = True
self.appimagekit = appimagekit

def run(self) -> None:
# Create the application bundle
Expand All @@ -166,20 +192,31 @@ def run(self) -> None:
appdir = os.path.join(self.bdist_base, "AppDir")
if os.path.exists(appdir):
self.execute(shutil.rmtree, (appdir,), msg=f"removing {appdir}")

self.mkpath(appdir)
share_icons = os.path.join("share", "icons")
icons_dir = os.path.join(appdir, share_icons)
self.mkpath(icons_dir)

# Copy from build_exe
self.copy_tree(self.build_dir, appdir, preserve_symlinks=True)

# Remove zip file after putting all files in the file system
# (appimage is a compressed file, no need of internal zip file)
library_data = Path(appdir, "lib", "library.dat")
if library_data.exists():
target_lib_dir = library_data.parent
filename = target_lib_dir / library_data.read_bytes().decode()
with ZipFile(filename) as outfile:
outfile.extractall(target_lib_dir)
filename.unlink()
library_data.unlink()

# Add icon, desktop file, entrypoint
share_icons = os.path.join("share", "icons")
icons_dir = os.path.join(appdir, share_icons)
self.mkpath(icons_dir)

executables = self.distribution.executables
executable = executables[0]
if len(executables) > 1:
self.warn(
self.warn_delayed(
"using the first executable as entrypoint: "
f"{executable.target_name}"
)
Expand Down Expand Up @@ -207,7 +244,7 @@ def run(self) -> None:
Icon=/{share_icons}/{os.path.splitext(icon_name)[0]}
Categories=Development;
Terminal=true
X-AppImage-Arch={platform.machine()}
X-AppImage-Arch={ARCH}
X-AppImage-Name={self.target_name}
X-AppImage-Version={self.target_version or ''}
"""
Expand All @@ -230,26 +267,28 @@ def run(self) -> None:
)

# Build an AppImage from an AppDir
os.environ["ARCH"] = platform.machine()
os.environ["ARCH"] = ARCH
cmd = [self.appimagekit, "--no-appstream", appdir, output]
if self._appimage_extract_and_run:
if find_library("fuse") is None: # libfuse.so.2 is not found
cmd.insert(1, "--appimage-extract-and-run")
with FileLock(self.appimagekit + ".lock"):
self.spawn(cmd)
self.spawn(cmd, search_path=0)
if not os.path.exists(output):
msg = "Could not build AppImage"
raise ExecError(msg)

self.warnings()

def save_as_file(self, data, outfile, mode="r") -> tuple[str, int]:
"""Save an input data to a file respecting verbose, dry-run and force
flags.
"""
if not self.force and os.path.exists(outfile):
if self.verbose >= 1:
self.warn(f"not creating {outfile} (output exists)")
self.warn_delayed(f"not creating {outfile} (output exists)")
return (outfile, 0)
if self.verbose >= 1:
self.announce(f"creating {outfile}")
self.announce(f"creating {outfile}", INFO)

if self.dry_run:
return (outfile, 1)
Expand All @@ -265,3 +304,10 @@ def save_as_file(self, data, outfile, mode="r") -> tuple[str, int]:
st_mode = st_mode | stat.S_IXUSR
os.chmod(outfile, st_mode)
return (outfile, 1)

def warn_delayed(self, msg) -> None:
self._warnings.append(msg)

def warnings(self) -> None:
for msg in self._warnings:
self.announce(f"WARNING: {msg}", WARNING)
49 changes: 32 additions & 17 deletions doc/src/bdist_appimage.rst
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
bdist_appimage
``````````````
==============

This command is available on Linux systems, to create an AppImage application
bundle (a .AppImage file); bdist_appimage automates the process.
An `AppImage <https://docs.appimage.org/>`_ is a downloadable file for Linux
that contains an application and everything the application needs to run
(e.g., libraries, icons, fonts, translations, etc.) that cannot be reasonably
expected to be part of each target system.

An AppImage is a downloadable file for Linux that contains an application and
everything the application needs to run (e.g., libraries, icons, fonts,
translations, etc.) that cannot be reasonably expected to be part of each
target system.
AppImages are simple to understand. Every AppImage is a regular file, and every
AppImage contains exactly one app with all its dependencies. Once the AppImage
is made executable, a user can just run it, either by double clicking it in
their desktop environment’s file manager, by running it from the console etc.

.. versionadded:: 7.0
It is crucial to understand that AppImage is merely a format for distributing
applications. In this regard, AppImage is like a `.zip` file or an `.iso` file.

When cx_Freeze calls appimagetool to create an AppImage application bundle
(an :file:`.AppImage` file), it builds a read-only image of a
:ref:`cx_freeze_build_exe` directory, then prepends the runtime, and marks the
file executable.

.. list-table::
:header-rows: 1
Expand All @@ -19,27 +27,34 @@ target system.
* - option name
- description
* - .. option:: appimagekit
- path to AppImageKit (download the latest version if not specified).
* - .. option:: bdist_dir
- temporary directory for creating the distribution
- path to AppImageKit [default: the latest version is downloaded]
* - .. option:: bdist_base
- base directory for creating built distributions
* - .. option:: build_dir (-b)
- directory of built executables and dependent files
* - .. option:: dist_dir (-d)
- directory to put final built distributions in (default: dist)
- directory to put final built distributions in [default: dist]
* - .. option:: skip_build
- skip rebuilding everything (for testing/debugging)
* - .. option:: target_name
- name of the file to create
- name of the file to create; if the name ends with ".AppImage"
then it is used verbatim, otherwise, information about the
program version and platform will be added to the installer
name [default: use metadata name or name of the first executable].
* - .. option:: target_version
- version of the file to create
- version of the file to create [default: metadata version if available]
* - .. option:: silent (-s)
- suppress all output except warnings

This is the equivalent help to specify the same options on the command line:
.. versionadded:: 7.0


To specify the same options on the command line, this is the help command that
shows the equivalent options:

.. code-block:: console
python setup.py bdist_appimage --help
.. seealso::
`AppImage | Linux apps that run anywhere <https://appimage.org/>`_

`AppImage documentation <https://docs.appimage.org/>`_

0 comments on commit 195b4f3

Please sign in to comment.