diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..030749b503a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +# Run on all tags +on: + push: + tags: + - '*' + +jobs: + runtime_builder: + name: Runtime ${{ matrix.cfg.id }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cfg: + - { id: x86_64 } + - { id: i386 } + env: + NONINTERACTIVE: 1 + + steps: + - uses: actions/checkout@v2 + - name: Building ${{ matrix.cfg.id }} Meson runtime + run: ./packaging/appimage/build_${{ matrix.cfg.id }}.sh + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: appimage/meson-${{ matrix.cfg.id }}.runtime + tag: ${{ github.ref }} + overwrite: true + body: ${{ github.ref }} diff --git a/.gitignore b/.gitignore index 8ff5e7869517..879ed063b757 100644 --- a/.gitignore +++ b/.gitignore @@ -21,9 +21,11 @@ __pycache__ .DS_Store *~ *.swp +meson*.runtime packagecache /MANIFEST /build +/appimage /dist /meson.egg-info diff --git a/CODEOWNERS b/CODEOWNERS index b051ece9d557..1107a4ad6c49 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -7,3 +7,4 @@ /mesonbuild/cmake/ @mensinda /mesonbuild/compilers/ @dcbaker /mesonbuild/linkers.py @dcbaker +/packaging/appimage @mensinda diff --git a/mesonbuild/mesonlib/universal.py b/mesonbuild/mesonlib/universal.py index 65d21eee1172..309559c7d503 100644 --- a/mesonbuild/mesonlib/universal.py +++ b/mesonbuild/mesonlib/universal.py @@ -152,7 +152,9 @@ from glob import glob -if os.path.basename(sys.executable) == 'meson.exe': +if 'MESON_PYTHON_BIN' in os.environ: + python_command = shlex.split(os.environ['MESON_PYTHON_BIN']) +elif os.path.basename(sys.executable) == 'meson.exe': # In Windows and using the MSI installed executable. python_command = [sys.executable, 'runpython'] else: @@ -219,7 +221,9 @@ def set_meson_command(mainfile: str) -> None: global _meson_command # On UNIX-like systems `meson` is a Python script # On Windows `meson` and `meson.exe` are wrapper exes - if not mainfile.endswith('.py'): + if 'MESON_COMMAND' in os.environ: + _meson_command = shlex.split(os.environ['MESON_COMMAND']) + elif not mainfile.endswith('.py'): _meson_command = [mainfile] elif os.path.isabs(mainfile) and mainfile.endswith('mesonmain.py'): # Can't actually run meson with an absolute path to mesonmain.py, it must be run as -m mesonbuild.mesonmain diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py index 9ed298160dc3..ada0eed5aaab 100644 --- a/mesonbuild/msetup.py +++ b/mesonbuild/msetup.py @@ -17,12 +17,16 @@ import sys, stat import datetime import os.path +import os import platform import cProfile as profile import argparse import tempfile import shutil import glob +import subprocess + +from pathlib import Path from . import environment, interpreter, mesonlib from . import build @@ -74,6 +78,20 @@ def __init__(self, options: argparse.Namespace) -> None: options.sourcedir, options.reconfigure, options.wipe) + # Detect if we are running from the AppImage runtime. Then self-extract + # and relaunch with the extracted binaries + if 'APPIMAGE' in os.environ: + rc = subprocess.run([os.environ['APPIMAGE'], '--runtime-setup', self.build_dir]).returncode + if rc == 0: + del os.environ['APPIMAGE'] # avoid infinite loops + del os.environ['APPDIR'] + meson_exe = Path(self.build_dir) / 'meson-runtime' / 'fakebin' / 'meson' + meson_exe = meson_exe.resolve() + os.execve(meson_exe.as_posix(), [meson_exe.as_posix(), *sys.argv[1:]], os.environ) + # execve never returns. See `man 3 exec`. + + mlog.warning('Failed to self extract', mlog.bold(os.environ['APPIMAGE']), 'to', mlog.bold(options.builddir), f'(rc = {rc})') + if options.wipe: # Make a copy of the cmd line file to make sure we can always # restore that file if anything bad happens. For example if diff --git a/packaging/appimage/build.py b/packaging/appimage/build.py new file mode 100755 index 000000000000..d8cda951832a --- /dev/null +++ b/packaging/appimage/build.py @@ -0,0 +1,566 @@ +#!/usr/bin/env python3 + +# Copyright 2020 Daniel Mensinger + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import os +import argparse +import shutil +import hashlib +import urllib.request +import subprocess +import multiprocessing +import re +import time +import stat +import tempfile +import textwrap +from pathlib import Path + +import typing as T + +appimage_dir = Path(__file__).resolve().parent +root_dir = appimage_dir.parents[1] +timers = [] + +def as_string(x) -> str: + if x is None: + return None + if isinstance(x, Path): + return x.resolve().as_posix() + elif not isinstance(x, str): + return str(x) + return x + +def as_string_rel(x) -> str: + if isinstance(x, Path): + try: + return x.resolve().relative_to(root_dir).as_posix() + except ValueError: + return x.resolve().as_posix() + return as_string(x) + +def run_checked(args, cwd=None, env=None, short: bool = False, quiet: bool = False) -> None: + cwd_str = as_string(cwd) + args_abs = [as_string(x) for x in args] + args_rel = [as_string_rel(x) for x in args] + + sys.stdout.flush() + sys.stderr.flush() + + if short: + print(f'\x1b[32mRunning \x1b[0;1m{" ".join(args_rel)}\x1b[0;33m\x1b[0m') + elif not quiet: + print(f'\n\n\x1b[32mRunning \x1b[0;1m{" ".join(args_rel)}\x1b[0;33m in \x1b[1;35m{as_string_rel(cwd)}\x1b[0m') + try: + rc = subprocess.run(args_abs, cwd=cwd_str, env=env) + sys.stdout.flush() + sys.stderr.flush() + except Exception: + rc = None + finally: + if rc is None or rc.returncode != 0: + print('''\x1b[31;1m + _________________ ___________ + | ___| ___ \ ___ \ _ | ___ \\ + | |__ | |_/ / |_/ / | | | |_/ / + | __|| /| /| | | | / + | |___| |\ \| |\ \\\\ \_/ / |\ \\ + \____/\_| \_\_| \_|\___/\_| \_|\x1b[0m + ''') + sys.stdout.flush() + sys.stderr.flush() + raise RuntimeError('Process Failed ({})'.format(args)) + + +def timed_code(func): + def wrapper(*args, **kwargs): + global timers + start = time.perf_counter() + func(*args, **kwargs) + end = time.perf_counter() + timers += [(func.__name__, end - start)] + return wrapper + +class SourceBase: + def __init__(self, url: str, filename: str, sha256sum: str, dirname: str, strip: int = 1): + self.url = url + self.filename = filename + self.dirname = dirname + self.sha256sum = sha256sum + self.strip = strip + + def check_hash(self, out_file: Path) -> bool: + curr_hash = hashlib.sha256(out_file.read_bytes()) + if curr_hash.hexdigest() != self.sha256sum: + print(f'Hash validation for {self.filename} failed') + return False + return True + + def download(self, dest: Path) -> bool: + out_file = dest / self.filename + if not out_file.exists(): + urllib.request.urlretrieve(self.url, str(out_file)) + return self.check_hash(out_file) + + def extract(self, dest: Path) -> None: + raise RuntimeError('Not implemented') + +class ArchiveSource(SourceBase): + def extract(self, dest: Path) -> None: + out_file = dest / self.filename + out_dir = dest.parent / self.dirname + if out_dir.exists(): + shutil.rmtree(out_dir) + out_dir.mkdir() + run_checked([ + 'tar', + '-xf', out_file, + f'--strip-components={self.strip}', + '-C', out_dir + ], quiet=True) + +class AppImageSource(SourceBase): + def extract(self, dest: Path) -> None: + out_file = dest / self.filename + st = os.stat(out_file.as_posix()) + os.chmod(out_file.as_posix(), st.st_mode | stat.S_IEXEC) + +SOURCES = { + 'python': ArchiveSource( + 'https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz', + 'python_3_9_6.tar.gz', + 'd0a35182e19e416fc8eae25a3dcd4d02d4997333e4ad1f2eee6010aadc3fe866', + 'python', + ), + 'ninja': ArchiveSource( + 'https://github.com/ninja-build/ninja/archive/v1.10.2.tar.gz', + 'ninja_1_10_2.tar.gz', + 'ce35865411f0490368a8fc383f29071de6690cbadc27704734978221f25e2bed', + 'ninja', + ), + 'cmake': ArchiveSource( + 'https://github.com/Kitware/CMake/releases/download/v3.20.5/cmake-3.20.5.tar.gz', + 'cmake_3_20_5.tar.gz', + '12c8040ef5c6f1bc5b8868cede16bb7926c18980f59779e299ab52cbc6f15bb0', + 'cmake', + ), + 'pkgconf': ArchiveSource( + 'https://github.com/pkgconf/pkgconf/archive/refs/tags/pkgconf-1.7.4.tar.gz', + 'pkgconf_1_7_4.tar.gz', + '2828dcdef88098748c306281d64a630b4ccd0703b1d92b532c31411e531d3088', + 'pkgconf', + ), + 'appimagekit': AppImageSource( + 'https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage', + 'appimagetool-x86_64.AppImage', + 'df3baf5ca5facbecfc2f3fa6713c29ab9cefa8fd8c1eac5d283b79cab33e4acb', + 'appimagetool', + ), +} + + +class AppImageBuilder: + def __init__(self) -> None: + self.build_dir = root_dir / 'appimage' + self.app_dir = self.build_dir / 'AppDir' + self.download_dir = self.build_dir / 'downloads' + self.wrappers_bld_dir = self.build_dir / 'wrappers' + self.runtime_bld_dir = self.build_dir / 'runtime' + self.runtime_exe = self.runtime_bld_dir / 'runtime' + + self.meson = root_dir / 'meson.py' + self.python = self.app_dir / 'usr' / 'bin' / 'python3' + self.ninja = self.app_dir / 'usr' / 'bin' / 'ninja' + self.cmake = self.app_dir / 'usr' / 'bin' / 'cmake' + self.patchelf = shutil.which('patchelf') + self.appimagetool = self.download_dir / 'appimagetool-x86_64.AppImage' + self.env = os.environ.copy() + self.nproc = multiprocessing.cpu_count() + + if not self.patchelf: + raise RuntimeError('Unable to find patchelf') + + @timed_code + def install_python(self) -> None: + python_dir = self.build_dir / SOURCES['python'].dirname + run_checked([ + './configure', + '--prefix=/usr', + '--with-computed-gotos', + '--enable-ipv6', + '--enable-optimizations', + '--with-lto', + '--without-ensurepip', + ], cwd=python_dir, env=self.env) + run_checked(['make', '-j', self.nproc], cwd=python_dir, env=self.env) + run_checked(['make', 'install'], cwd=python_dir, env=self.env) + run_checked([self.python, '-E', '-m', 'ensurepip'], cwd=self.app_dir, env=self.env) + + # Create symlink + raw_python = self.app_dir / 'usr' / 'bin' / 'python' + raw_python.symlink_to(self.python.resolve().name) + + @timed_code + def install_ninja(self) -> None: + ninja_dir = self.build_dir / SOURCES['ninja'].dirname + run_checked([self.python, './configure.py', '--bootstrap'], cwd=ninja_dir, env=self.env) + shutil.copy2(ninja_dir / 'ninja', self.app_dir / 'usr' / 'bin' / 'ninja') + + @timed_code + def install_meson(self) -> None: + run_checked([ + self.python, '-m', 'pip', + 'install', + '--prefix', self.app_dir / 'usr', + '--ignore-installed', + '.', + ], cwd=root_dir, env=self.env) + + # Use our meson.py script instead of the broken stuff setuptools generates + meson_exe = self.app_dir / 'usr' / 'bin' / 'meson' + meson_py = root_dir / 'meson.py' + meson_exe.unlink() + shutil.copy2(meson_py, meson_exe) + + @timed_code + def install_cmake(self) -> None: + cmake_dir = self.build_dir / SOURCES['cmake'].dirname + run_checked([ + './bootstrap', + '--prefix=/usr', + '--no-system-libs', + '--no-qt-gui', + f'--parallel={self.nproc}', + '--', + '-DCMAKE_BUILD_TYPE=RELEASE', + '-DCMAKE_USE_OPENSSL=OFF', + '-DBUILD_CursesDialog=OFF', + ], cwd=cmake_dir, env=self.env) + run_checked(['make', '-j', self.nproc], cwd=cmake_dir, env=self.env) + run_checked(['make', 'install'], cwd=cmake_dir, env=self.env) + + @timed_code + def pip_install(self, what: str) -> None: + run_checked([ + self.python, '-m', 'pip', + 'install', + '--prefix', self.app_dir / 'usr', + '--ignore-installed', + what, + ]) + + @timed_code + def install_pkgconf(self) -> None: + pkg_config_dir = self.build_dir / SOURCES['pkgconf'].dirname + run_checked([self.meson, 'build', '-Dtests=false', '-Dprefix=/usr'], cwd=pkg_config_dir, env=self.env) + run_checked(['ninja', '-C', 'build'], cwd=pkg_config_dir, env=self.env) + run_checked(['ninja', '-C', 'build', 'install'], cwd=pkg_config_dir, env=self.env) + + @timed_code + def build_wrappers(self) -> None: + # Build AppRun binaries + apprun_dir = appimage_dir / 'wrappers' + run_checked([self.meson, self.wrappers_bld_dir], cwd=apprun_dir, env=self.env) + run_checked(['ninja'], cwd=self.wrappers_bld_dir, env=self.env) + run_checked(['ninja', 'install'], cwd=self.wrappers_bld_dir, env=self.env) + + @timed_code + def build_runtime(self) -> None: + # Build our custom AppImage runtime + runtime_dir = appimage_dir / 'runtime' + run_checked([self.python, self.meson, '--wrap-mode=forcefallback', self.runtime_bld_dir], cwd=runtime_dir, env=self.env) + run_checked(['ninja'], cwd=self.runtime_bld_dir, env=self.env) + + @timed_code + def cleanup(self) -> None: + to_remove = [] + to_remove += [x for x in self.app_dir.glob('**/*.a')] + to_remove += [x for x in self.app_dir.glob('**/*.pc')] + to_remove += [x for x in self.app_dir.glob('**/*.rst')] + to_remove += [x for x in self.app_dir.glob('**/Help/**/*.txt')] + to_remove += [x for x in self.app_dir.glob('**/Help/**/*.png')] + to_remove += [x for x in self.app_dir.glob('**/bin/2to3*')] + to_remove += [x for x in self.app_dir.glob('**/bin/easy*')] + to_remove += [x for x in self.app_dir.glob('**/bin/idle*')] + to_remove += [x for x in self.app_dir.glob('**/bin/pip*')] + to_remove += [x for x in self.app_dir.glob('**/bin/python3*-config*')] + to_remove += [x for x in self.app_dir.glob('**/bin/pydoc*')] + to_remove += [x for x in self.app_dir.glob('**/bin/cpack')] + to_remove += [x for x in self.app_dir.glob('**/bin/ctest')] + to_remove += [x for x in self.app_dir.glob('**/site-packages/pip*')] + to_remove += [x for x in self.app_dir.glob('usr/doc')] + to_remove += [x for x in self.app_dir.glob('usr/include')] + to_remove += [x for x in self.app_dir.glob('usr/share/vim*')] + to_remove += [x for x in self.app_dir.glob('usr/share/man*')] + to_remove += [x for x in self.app_dir.glob('usr/share/doc*')] + to_remove += [x for x in self.app_dir.glob('usr/share/emacs*')] + to_remove += [x for x in self.app_dir.glob('usr/share/ac*')] + + # all test dirs + to_remove += [x for x in self.app_dir.glob('**/lib/python*/**/test')] + to_remove += [x for x in self.app_dir.glob('**/lib/python*/**/tests')] + + def rm_module(mod: str) -> None: + nonlocal to_remove + to_remove += [x for x in self.app_dir.glob('**/lib/python*/{}*'.format(mod))] + to_remove += [x for x in self.app_dir.glob('**/lib/python*/_{}*'.format(mod))] + to_remove += [x for x in self.app_dir.glob('**/lib-dynload/{}*.so'.format(mod))] + to_remove += [x for x in self.app_dir.glob('**/lib-dynload/_{}*.so'.format(mod))] + + # Remove big modules we don't use/need + rm_module('tkinter') + rm_module('unittest') + rm_module('lib2to3') + rm_module('ensurepip') + rm_module('idlelib') + rm_module('pydoc') + + for i in to_remove: + print('Deleting {}'.format(i)) + if i.is_dir(): + shutil.rmtree(str(i)) + else: + i.unlink() + + # Remove empty directories + while True: + to_remove = [] + for i in self.app_dir.glob('**'): + if not [x for x in i.iterdir()] : + to_remove += [i] + + if not to_remove: + break + + for i in to_remove: + print('Deleting empty dir {}'.format(i)) + shutil.rmtree(str(i)) + + @timed_code + def move_libs_to_bin(self) -> None: + # Move the libraries next to the binaries so we don't need to + # mess with LD_LIBRARY_PATH + usr_lib = self.app_dir / 'usr' / 'lib' + usr_bin = self.app_dir / 'usr' / 'bin' + targets = [x for x in usr_lib.glob('*.so')] + for i in targets: + shutil.move(i, usr_bin) + + @timed_code + def copy_libs(self) -> None: + # 1st: Search all *.so and executables + targets = [] + targets += [x for x in self.app_dir.glob('**/*.so*')] + targets += [x for x in self.app_dir.glob('**/bin/*')] + targets = [x for x in targets if x.is_file() and not x.is_symlink()] + + path_reg = re.compile(r'=>\s*([^ ]+)') + libs_set: T.Set[Path] = set() + + # 2nd: use `ldd` to extract the libraries + for i in targets: + res = subprocess.run(['ldd', str(i)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + lines = res.stdout.decode().split('\n') + for l in lines: + m = path_reg.search(l) + if not m: + continue + libs_set.add(Path(m.group(1))) + + # 3rd: detect ld-linux.so and handle it seperately + ld_so = Path() + def extract_ld(libp: Path) -> bool: + nonlocal ld_so + if libp.name.startswith('ld'): + ld_so = libp + return True + return False + + libs = sorted(libs_set) + libs = [x for x in libs if not extract_ld(x)] + dest = self.app_dir / 'usr' / 'lib' + + # 4th: Copy the remaining libraries to AppDir/usr/bin + for p in libs: + print('Copying {}'.format(p)) + shutil.copy2(p, dest) + + # 5th: Copy ld-linux.so + print('ld-linux is {}'.format(ld_so)) + shutil.copy2(ld_so, dest / 'ld-linux.so') + + @timed_code + def copy_metadata(self) -> None: + icon_src = root_dir / 'graphics' / 'meson_logo_big.png' + icon_dst = self.app_dir / 'meson.png' + metainfo_dir = self.app_dir / 'usr' / 'share' / 'metainfo' + metainfo_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(appimage_dir / 'meson.desktop', self.app_dir) + shutil.copy2(appimage_dir / 'meson.appdata.xml', metainfo_dir) + shutil.copy2(icon_src, icon_dst) + + @timed_code + def rebuild_pycache(self) -> None: + # Rebuild the pycache with --invalidation-mode unchecked-hash because + # the AppImage will mess up the timestamps and thus invalidate the current + # timestamp based pycache + dirs: T.List[str] = [] + for i in self.app_dir.glob('**/__pycache__'): + dirs += [i.parent.resolve().as_posix()] + with tempfile.NamedTemporaryFile(mode='w', delete=False) as fd: + fd.write('\n'.join(dirs)) + fd.flush() + run_checked([ + self.python, + '-m', 'compileall', + '--invalidation-mode', 'unchecked-hash', + '-l', '-f', + '-i', fd.name, + ]) + + @timed_code + def process_binaries(self) -> None: + binaries: T.List[Path] = [] + binaries += [x for x in self.app_dir.glob('**/bin/*')] + binaries += [x for x in self.app_dir.glob('**/*.so')] + binaries += [x for x in self.app_dir.glob('**/*.so.*')] + binaries = [x for x in binaries if not x.read_bytes().startswith(b'#!')] + binaries = [x for x in binaries if x.name != 'ld-linux.so'] + binaries = sorted(set(binaries)) + for i in binaries: + print(f' - stripping and setting RPATH for {i}') + relpath = os.path.relpath(self.app_dir / "usr" / "lib", i.parent) + run_checked([self.env['STRIP'], i], env=self.env, short=True) + run_checked([self.patchelf, '--set-rpath', f'$ORIGIN/{relpath}', i], env=self.env, short=True) + + @timed_code + def build_appimage(self, output: str, delayed_packageing: bool) -> None: + apprun = self.app_dir / 'AppRun' + apprun.symlink_to(Path('fakebin') / 'meson') + args = [self.appimagetool, '-n', self.app_dir, '--runtime-file', self.runtime_exe, output] + args = ['./' + x.resolve().relative_to(self.build_dir).as_posix() if isinstance(x, Path) else x for x in args] + if delayed_packageing: + args_str = ' '.join([str(x) for x in args]) + out_file = self.build_dir / 'package.sh' + out_file.write_text( + encoding='utf-8', + data=textwrap.dedent( + f'''\ + #!/bin/sh + + cd "$(dirname "$0")" + + {args_str} + ''' + ) + ) + st = os.stat(out_file.as_posix()) + os.chmod(out_file.as_posix(), st.st_mode | stat.S_IEXEC) + else: + run_checked(args, cwd=root_dir, env=self.env) + + def run(self) -> int: + parser = argparse.ArgumentParser(description='builds the meson AppImage') + parser.add_argument('-o', '--output', type=str, help='Output filename', default='meson.runtime') + parser.add_argument('-d', '--delayed-packageing', action='store_true', help='Write the packaging command to a script instead of executing it directly') + args = parser.parse_args() + + if not self.build_dir.exists(): + self.build_dir.mkdir() + + for d in self.build_dir.iterdir(): + if d.name == 'downloads': + continue + if d.is_dir(): + shutil.rmtree(d) + else: + d.unlink() + + if self.app_dir.exists(): + shutil.rmtree(self.app_dir) + self.app_dir.mkdir() + + if not self.download_dir.exists(): + self.download_dir.mkdir() + + self.env['DESTDIR'] = str(self.app_dir) + #self.env['CFLAGS'] = '-static-libstdc++ -static-libgcc' + #self.env['CXXFLAGS'] = '-static-libstdc++ -static-libgcc' + self.env['PATH'] = str(self.app_dir / 'usr' / 'bin') + ':' + self.env['PATH'] + self.env['PYTHONHOME'] = str(self.app_dir / 'usr') + + ccache = shutil.which('ccache') or '' + if ccache: + ccache += ' ' + + if 'CC' not in self.env: + self.env['CC'] = f'{ccache}gcc' + if 'CXX' not in self.env: + self.env['CXX'] = f'{ccache}g++' + if 'STRIP' not in self.env: + self.env['STRIP'] = 'strip' + + print('Downloading files...') + for k, v in SOURCES.items(): + res = v.download(self.download_dir) + print(' - {}: {}'.format(k, res)) + if not res: + return 1 + + print('Extracting sources...') + for k, v in SOURCES.items(): + v.extract(self.download_dir) + print(' - {}: DONE'.format(k)) + + self.install_python() + self.install_ninja() + self.install_meson() + #self.install_cmake() + self.install_pkgconf() + self.pip_install('tqdm') + + self.cleanup() + self.rebuild_pycache() + self.copy_libs() + self.copy_metadata() + self.build_wrappers() + self.build_runtime() + self.process_binaries() + self.build_appimage(args.output, args.delayed_packageing) + + + print(f'''\n\n\x1b[32;1m + ______ _____ _ _ _____ + | _ \ _ | \ | || ___| + | | | | | | | \| || |__ + | | | | | | | . ` || __| + | |/ /\ \_/ / |\ || |___ + |___/ \___/\_| \_/\____/\x1b[0m + + `{args.output}` has been created in the project root + + Build dir: {self.build_dir} + + Timers: + ''') + + for i in timers: + print(' - {:<24} -- {:.2f}s'.format(i[0], i[1])) + + return 0 + +if __name__ == '__main__': + sys.exit(AppImageBuilder().run()) diff --git a/packaging/appimage/build_i386.sh b/packaging/appimage/build_i386.sh new file mode 100755 index 000000000000..1b7dd4b3e406 --- /dev/null +++ b/packaging/appimage/build_i386.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +cd "$(dirname "$0")" +SCRIPT="$(basename "$0")" + +if [ ! -f /.dockerenv ]; then + IT_ARG='-it' + [ "$NONINTERACTIVE" -eq 1 ] && IT_ARG='' + docker run --rm $IT_ARG --platform linux/i386 -v "$(realpath "$PWD/../..")":/meson i386/alpine sh -c "/meson/packaging/appimage/$SCRIPT $(id -u) $(id -g)" + $PWD/../../appimage/package.sh + exit $? +fi + +apk update +apk upgrade +apk add python3 patchelf gcc g++ make linux-headers libffi-dev zlib-dev xz-dev sqlite-dev openssl-dev openssl-libs-static argp-standalone ninja udev eudev-dev bash sudo git + +adduser -HD -u $1 -g $2 meson + +sudo -u meson ./build.py -d -o meson-i386.runtime diff --git a/packaging/appimage/build_x86_64.sh b/packaging/appimage/build_x86_64.sh new file mode 100755 index 000000000000..c77b9bc47818 --- /dev/null +++ b/packaging/appimage/build_x86_64.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +cd "$(dirname "$0")" +SCRIPT="$(basename "$0")" + +if [ ! -f /.dockerenv ]; then + IT_ARG='-it' + [ "$NONINTERACTIVE" -eq 1 ] && IT_ARG='' + docker run --rm $IT_ARG -v "$(realpath "$PWD/../..")":/meson alpine sh -c "/meson/packaging/appimage/$SCRIPT $(id -u) $(id -g)" + $PWD/../../appimage/package.sh + exit $? +fi + +apk update +apk upgrade +apk add python3 patchelf gcc g++ make linux-headers libffi-dev zlib-dev xz-dev sqlite-dev openssl-dev openssl-libs-static argp-standalone ninja udev eudev-dev bash sudo git + +adduser -HD -u $1 -g $2 meson + +sudo -u meson ./build.py -d -o meson-x86_64.runtime diff --git a/packaging/appimage/meson.appdata.xml b/packaging/appimage/meson.appdata.xml new file mode 100644 index 000000000000..a892cd1bd01a --- /dev/null +++ b/packaging/appimage/meson.appdata.xml @@ -0,0 +1,23 @@ + + + meson + CC0-1.0 + Apache-2.0 + Meson + The Meson Build System + +

Meson is an open source build system meant to be both extremely fast, and, even more importantly, as user friendly as possible. + +The main design point of Meson is that every moment a developer spends writing or debugging build definitions is a second wasted. So is every second spent waiting for the build system to actually start compiling code.

+
+ meson.desktop + https://mesonbuild.com + + + https://raw.githubusercontent.com/mesonbuild/meson/master/graphics/meson_logo_big.png + + + + meson.desktop + +
\ No newline at end of file diff --git a/packaging/appimage/meson.desktop b/packaging/appimage/meson.desktop new file mode 100755 index 000000000000..bd36a80de985 --- /dev/null +++ b/packaging/appimage/meson.desktop @@ -0,0 +1,10 @@ +#!/usr/bin/env xdg-open +[Desktop Entry] +Type=Application +Version=1.0 +Name=Meson +Comment=The Meson Build System +Exec=meson +Icon=meson +Terminal=true +Categories=Development; diff --git a/packaging/appimage/runtime/.clang-format b/packaging/appimage/runtime/.clang-format new file mode 100644 index 000000000000..7a3e9c878eb9 --- /dev/null +++ b/packaging/appimage/runtime/.clang-format @@ -0,0 +1,33 @@ +--- +AccessModifierOffset: '0' +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: 'true' +AlignConsecutiveAssignments: 'true' +AlignConsecutiveDeclarations: 'true' +AlignEscapedNewlines: Right +AlignOperands: 'true' +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'true' +AllowAllConstructorInitializersOnNextLine: 'true' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: 'true' +AlwaysBreakTemplateDeclarations: 'Yes' +BinPackArguments: 'false' +BinPackParameters: 'false' +BreakBeforeBraces: Attach +ColumnLimit: '120' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' +DerivePointerAlignment: 'false' +ExperimentalAutoDetectBinPacking: 'false' +IndentCaseLabels: 'true' +IndentWidth: '4' +Language: Cpp +PointerAlignment: Right +SortIncludes: 'false' +TabWidth: '4' +UseTab: Never + +... diff --git a/packaging/appimage/runtime/.clang-format-ignore b/packaging/appimage/runtime/.clang-format-ignore new file mode 100644 index 000000000000..708b88f45c88 --- /dev/null +++ b/packaging/appimage/runtime/.clang-format-ignore @@ -0,0 +1,2 @@ +subprojects/** + diff --git a/packaging/appimage/runtime/genid.py b/packaging/appimage/runtime/genid.py new file mode 100755 index 000000000000..f7471f5bae6f --- /dev/null +++ b/packaging/appimage/runtime/genid.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +# SPDX-license-identifier: Apache-2.0 + +import uuid + +print(uuid.uuid4()) diff --git a/packaging/appimage/runtime/main.c b/packaging/appimage/runtime/main.c new file mode 100644 index 000000000000..db2c92bfe7ce --- /dev/null +++ b/packaging/appimage/runtime/main.c @@ -0,0 +1,235 @@ +// SPDX-license-identifier: Apache-2.0 + +#define _DEFAULT_SOURCE +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "runtime-config.h" + +#if VERBOSE +#define LOG(fmt, ...) \ + { \ + printf(fmt "\n", ##__VA_ARGS__); \ + fflush(stdout); \ + } +#else +#define LOG(fmt, ...) +#endif + +#define CHECK_OPT(opt) ((strlen(argv[i]) > 10) && (strcmp(opt, argv[i] + 10) == 0)) + +void print_help() { + printf("Meson portable runtime " RUNTIME_VERSION " (based on AppImage)\n" + "\n" + "Runtime specific options:\n" + " --runtime-help Print this help message and exit\n" + " --runtime-version Print the runtime version (NOT the Meson version) and exit\n" + " --runtime-info Print runime information and exit\n" + " --runtime-setup Set up the /meson-runtime directory and exit\n" + "\n"); +} + +void print_info(appimage_context_t *context) { + printf("Meson runtime information:\n" + " - detected runtime path: %s\n" + " - squashfs offset: %" PRId64 "\n", + context->appimage_path, + (int64_t)context->fs_offset); +} + +bool setup_build_dir(appimage_context_t *context, const char *build_dir) { + bool ret = true; + FILE * fp = NULL; + const size_t max_len = strlen(build_dir) + 32; // enough to add '/meson-runtime/runtime-id.txt' + char * prefix = malloc(sizeof(char) * (max_len + 1)); + snprintf(prefix, max_len, "%s/meson-runtime", build_dir); + LOG("Setting up runtime dir %s", prefix); + + ret = appimage_self_extract(context, prefix, NULL, true, false); + if (!ret) { + fprintf(stderr, "Failed to self-extract squashfs filesystem.\n"); + goto cleanup; + } + + // Write build ID + strcat(prefix, "/runtime-id.txt"); + fp = fopen(prefix, "w"); + if (fp == NULL) { + perror("Failed to open runtime-id.txt"); + ret = false; + goto cleanup; + } + fprintf(fp, "%s", BUILD_ID); + +cleanup: + if (fp != NULL) { + fclose(fp); + } + free(prefix); + return ret; +} + +char *check_existing_runtime(const char *build_dir) { + const size_t max_len = strlen(build_dir) + 32; + char * id_txt = malloc(sizeof(char) * (max_len + 1)); + char UUID[37]; + snprintf(id_txt, max_len, "%s/meson-runtime/runtime-id.txt", build_dir); + + LOG("Checking possible meson-runtime %s", id_txt); + + // Check if the file exists + if (access(id_txt, F_OK) != 0) { + LOG("meson-runtime %s does not exist", id_txt); + free(id_txt); + return NULL; + } + + // Check that we have the correct version + FILE *fp = fopen(id_txt, "r"); + if (!fp) { + LOG("Failed to open %s", id_txt); + free(id_txt); + return NULL; + } + + memset(UUID, 0, 37); + for (int i = 0; i < 36;) { + int c = getc(fp); + if (c == EOF) { + break; + } + + // skip non UUID chars -- make the logic easier by accepting everything + // above '-' in the ASCII table + if (c < '-') { + continue; + } + + UUID[i++] = c; + } + + fclose(fp); + + if (strcmp(UUID, BUILD_ID) == 0) { + // Return the runtime dir with the AppRun by shortening id_txt + const size_t slash_idx = strlen(id_txt) - 15; // 15 == strlen("/runtime-id.txt"); + id_txt[slash_idx] = '\0'; + LOG("Found meson-runtime %s", id_txt); + } else { + // Wrong UUID + printf("Found exsisting meson-runtime, but the UUID in %s does not match\n.", id_txt); + free(id_txt); + id_txt = NULL; + } + return id_txt; +} + +void exec_already_extracted_runtime(const char *runtime_dir, int argc, char **argv) { + printf("Using exsisting meson-runtime in %s\n", runtime_dir); + unsetenv("APPIMAGE"); + + char *meson_path = malloc(strlen(runtime_dir) + 16); + sprintf(meson_path, "%s/fakebin/meson", runtime_dir); + + char **new_argv = malloc(sizeof(char *) * (argc + 1)); + int new_argc = 0; + new_argv[new_argc++] = strdup(meson_path); + for (int i = 1; i < argc; ++i) { + if (!appimage_starts_with("--runtime", argv[i])) { + new_argv[new_argc++] = strdup(argv[i]); + } + } + new_argv[new_argc] = NULL; + + execv(meson_path, new_argv); + + int error = errno; + fprintf(stderr, "Failed to run %s: %s\n", meson_path, strerror(error)); + exit(EXIT_EXECERROR); +} + +typedef struct mount_data { + char * mount_dir; + int argc; + char **argv; +} mount_data_t; + +void mounted_cb(appimage_context_t *const context, void *data_raw) { + mount_data_t *data = (mount_data_t *)data_raw; + appimage_execute_apprun(context, data->mount_dir, data->argc, data->argv, "--runtime", true); +} + +int main(int argc, char *argv[]) { + appimage_context_t context; + char * runtime_dir = NULL; + + if (!appimage_detect_context(&context, argc, argv)) { + return EXIT_EXECERROR; + } + + // Parse commandline arguments + for (int i = 1; i < argc; i++) { + // Check if the arg is a build and has a matching meson-runtime + if (!runtime_dir) { + runtime_dir = check_existing_runtime(argv[i]); + } + + if (!appimage_starts_with("--runtime", argv[i])) { + continue; + } + + if (CHECK_OPT("help")) { + print_help(); + return 0; + } else if (CHECK_OPT("version")) { + printf("%s\n", RUNTIME_VERSION); + return 0; + } else if (CHECK_OPT("info")) { + print_info(&context); + return 0; + } else if (CHECK_OPT("setup")) { + if (i >= (argc - 1)) { + fprintf(stderr, "--runtime-setup expects exactly one parameter\n"); + print_help(); + return 1; + } + + setup_build_dir(&context, argv[++i]); + return 0; + } else { + fprintf(stderr, "Unknown runtime option '%s'\n", argv[i]); + print_help(); + return 1; + } + } + + // Check if the current dir has a matching meson-runtime + if (!runtime_dir) { + runtime_dir = check_existing_runtime("."); + } + + // Use the first found meson-runtime + if (runtime_dir) { + exec_already_extracted_runtime(runtime_dir, argc, argv); + } + + // Self-mount and execute + char * mount_dir = appimage_generate_mount_path(&context, NULL); + mount_data_t cb_data; + cb_data.mount_dir = mount_dir; + cb_data.argc = argc; + cb_data.argv = argv; + + if (!appimage_self_mount(&context, mount_dir, &mounted_cb, &cb_data)) { + return EXIT_EXECERROR; + } + + return 0; +} diff --git a/packaging/appimage/runtime/meson.build b/packaging/appimage/runtime/meson.build new file mode 100644 index 000000000000..675ef1903897 --- /dev/null +++ b/packaging/appimage/runtime/meson.build @@ -0,0 +1,64 @@ +project( + 'meson-runtime', ['c'], + version: '0.0.1', + meson_version : '>=0.58.0', + default_options : [ + 'b_lto=true', + 'default_library=static', + 'warning_level=3', + 'werror=true', + 'debug=false', + 'optimization=s', + + 'fuse:werror=false', + 'fuse3:werror=false', + 'squashfuse:werror=false', + 'zstd:werror=false', + 'liblzma:werror=false', + 'liblzma:warning_level=0', + 'lz4:warning_level=0', + 'zlib:warning_level=0', + 'zstd:warning_level=0', + 'zstd:bin_programs=false', + ], +) + +add_project_link_arguments('-static', language: 'c') +add_project_arguments('-DFUSE_USE_VERSION=26', language: 'c') + +dd_prog = find_program('dd') +objcopy_prog = find_program('objcopy') + +libruntime_sp = subproject('libruntime') +libruntime_dep = libruntime_sp.get_variable('libruntime_dep') +patcher_prog = libruntime_sp.get_variable('patcher_prog') + +# Build the unpatched executable +conf_data = configuration_data() +conf_data.set_quoted('RUNTIME_VERSION', meson.project_version()) +conf_data.set_quoted('BUILD_ID', run_command(find_program('genid.py')).stdout().strip()) +conf_data.set10('VERBOSE', get_option('debug')) +configure_file(output: 'runtime-config.h', configuration: conf_data) + +runtime_raw = executable( + 'runtime_raw', ['main.c'], + dependencies: [libruntime_dep], +) + +# Patch the executable +custom_target( + 'runtime', + input: [runtime_raw], + output: ['runtime'], + install: true, + install_dir: get_option('bindir'), + command: [ + patcher_prog, + '-i', '@INPUT@', + '-o', '@OUTPUT@', + '-p', '@PRIVATE_DIR@', + # '-e', + '--dd', dd_prog, + '--objcopy', objcopy_prog, + ], +) diff --git a/packaging/appimage/runtime/subprojects/.gitignore b/packaging/appimage/runtime/subprojects/.gitignore new file mode 100644 index 000000000000..63ea916ef5f3 --- /dev/null +++ b/packaging/appimage/runtime/subprojects/.gitignore @@ -0,0 +1 @@ +/*/ diff --git a/packaging/appimage/runtime/subprojects/fuse.wrap b/packaging/appimage/runtime/subprojects/fuse.wrap new file mode 100644 index 000000000000..0428905de54b --- /dev/null +++ b/packaging/appimage/runtime/subprojects/fuse.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = libfuse-fuse-2.9.9 +source_url = https://github.com/libfuse/libfuse/archive/refs/tags/fuse-2.9.9.tar.gz +source_filename = fuse-2.9.9.tar.gz +source_hash = e57a24721177c3b3dd71cb9239ca46b4dee283db9388d48f7ccd256184982194 +patch_filename = fuse_2.9.9-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/fuse_2.9.9-1/get_patch +patch_hash = 62d8b30e494a465751124fdbec1fe0476cbbc65ffd7c865b16da7dba2b4a63a1 + +[provide] +fuse = libfuse_dep + diff --git a/packaging/appimage/runtime/subprojects/fuse3.wrap b/packaging/appimage/runtime/subprojects/fuse3.wrap new file mode 100644 index 000000000000..13709ae05e5e --- /dev/null +++ b/packaging/appimage/runtime/subprojects/fuse3.wrap @@ -0,0 +1,8 @@ +[wrap-file] +directory = fuse-3.10.4 +source_url = https://github.com/libfuse/libfuse/releases/download/fuse-3.10.4/fuse-3.10.4.tar.xz +source_filename = fuse-3.10.4.tar.xz +source_hash = 9365b74fd8471caecdb3cc5adf25a821f70a931317ee9103d15bd39089e3590d + +[provide] +fuse3 = libfuse_dep diff --git a/packaging/appimage/runtime/subprojects/liblzma.wrap b/packaging/appimage/runtime/subprojects/liblzma.wrap new file mode 100644 index 000000000000..ddf5230fc1e3 --- /dev/null +++ b/packaging/appimage/runtime/subprojects/liblzma.wrap @@ -0,0 +1,11 @@ +[wrap-file] +directory = xz-5.2.1 +source_url = http://tukaani.org/xz/xz-5.2.1.tar.xz +source_filename = xz-5.2.1.tar.xz +source_hash = 6ecdd4d80b12001497df0741d6037f918d270fa0f9a1ab4e2664bf4157ae323c +patch_url = https://wrapdb.mesonbuild.com/v2/liblzma_5.2.1-5/get_patch +patch_filename = liblzma-5.2.1-5-wrap.zip +patch_hash = cde35a0feaf438e3dc0b8d227910fc26051801e7b68bbac24ef6546d94be6049 + +[provide] +liblzma = lzma_dep diff --git a/packaging/appimage/runtime/subprojects/libruntime.wrap b/packaging/appimage/runtime/subprojects/libruntime.wrap new file mode 100644 index 000000000000..8dc3249fd799 --- /dev/null +++ b/packaging/appimage/runtime/subprojects/libruntime.wrap @@ -0,0 +1,6 @@ +[wrap-file] + +directory = libRuntime-0.0.3 +source_url = https://github.com/mensinda/libRuntime/archive/refs/tags/v0.0.3.tar.gz +source_filename = libRuntime-v0.0.3.tar.gz +source_hash = 9f52ebf9e6b4059cdf65d34caa1a755edf0856ae010d97cb8a17a099d9f3782f diff --git a/packaging/appimage/runtime/subprojects/lz4.wrap b/packaging/appimage/runtime/subprojects/lz4.wrap new file mode 100644 index 000000000000..c151ad0e47d1 --- /dev/null +++ b/packaging/appimage/runtime/subprojects/lz4.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = lz4-1.9.3 +source_url = https://github.com/lz4/lz4/archive/v1.9.3.tar.gz +source_filename = lz4-1.9.3.tgz +source_hash = 030644df4611007ff7dc962d981f390361e6c97a34e5cbc393ddfbe019ffe2c1 +patch_url = https://wrapdb.mesonbuild.com/v2/lz4_1.9.3-1/get_patch +patch_filename = lz4-1.9.3-1-wrap.zip +patch_hash = 5a0e6e5797a51d23d5dad0009d36dfebe354b5e5eef2a1302d29788b1ee067c1 + +[provide] +liblz4 = liblz4_dep + diff --git a/packaging/appimage/runtime/subprojects/squashfuse.wrap b/packaging/appimage/runtime/subprojects/squashfuse.wrap new file mode 100644 index 000000000000..14ec6a378da0 --- /dev/null +++ b/packaging/appimage/runtime/subprojects/squashfuse.wrap @@ -0,0 +1,4 @@ +[wrap-git] + +url = https://github.com/mensinda/squashfuse.git +revision = meson diff --git a/packaging/appimage/runtime/subprojects/zlib.wrap b/packaging/appimage/runtime/subprojects/zlib.wrap new file mode 100644 index 000000000000..d471d9b384d4 --- /dev/null +++ b/packaging/appimage/runtime/subprojects/zlib.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = zlib-1.2.11 +source_url = http://zlib.net/fossils/zlib-1.2.11.tar.gz +source_filename = zlib-1.2.11.tar.gz +source_hash = c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 +patch_url = https://wrapdb.mesonbuild.com/v2/zlib_1.2.11-5/get_patch +patch_filename = zlib-1.2.11-5-wrap.zip +patch_hash = 728c8e24acbc2e6682fbd950fec39e2fc77528af361adb87259f8a8511434004 + +[provide] +zlib = zlib_dep + diff --git a/packaging/appimage/runtime/subprojects/zstd.wrap b/packaging/appimage/runtime/subprojects/zstd.wrap new file mode 100644 index 000000000000..b736b50913ce --- /dev/null +++ b/packaging/appimage/runtime/subprojects/zstd.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = zstd-1.4.5 +source_url = https://github.com/facebook/zstd/releases/download/v1.4.5/zstd-1.4.5.tar.gz +source_filename = zstd-1.4.5.tar.gz +source_hash = 98e91c7c6bf162bf90e4e70fdbc41a8188b9fa8de5ad840c401198014406ce9e +patch_url = https://wrapdb.mesonbuild.com/v2/zstd_1.4.5-1/get_patch +patch_filename = zstd-1.4.5-1-wrap.zip +patch_hash = fd9cb7b9c8f7092ef1597ff68f170beef65fcf33e575a621955cf405a41db1cc + +[provide] +libzstd = libzstd_dep + diff --git a/packaging/appimage/wrappers/.clang-format b/packaging/appimage/wrappers/.clang-format new file mode 100644 index 000000000000..74d43f0c6982 --- /dev/null +++ b/packaging/appimage/wrappers/.clang-format @@ -0,0 +1,9 @@ +--- +AlignConsecutiveAssignments: 'true' +AlignConsecutiveDeclarations: 'true' +AlignOperands: 'true' +AlignTrailingComments: 'true' +PointerAlignment: Left +UseTab: Never + +... diff --git a/packaging/appimage/wrappers/.gitignore b/packaging/appimage/wrappers/.gitignore new file mode 100644 index 000000000000..5cd4947d02dd --- /dev/null +++ b/packaging/appimage/wrappers/.gitignore @@ -0,0 +1,2 @@ +/*build* +!meson.build \ No newline at end of file diff --git a/packaging/appimage/wrappers/common.c b/packaging/appimage/wrappers/common.c new file mode 100644 index 000000000000..39cbd77ae711 --- /dev/null +++ b/packaging/appimage/wrappers/common.c @@ -0,0 +1,95 @@ +#include "common.h" + +#include +#include +#include +#include + +int g_verbose; + +void info_autofill_paths(AppRunInfo_t* info, const char* exe_name) { + if (!info) { + DIE("invalid argument to info_autofill_paths"); + } + + if (!info->appdir && exe_name) { + // First sanity check that we are run in an environment set by AppRun + info->appdir = getenv("APPDIR"); + if (!info->appdir) { + LOG("Wrapper %s run without AppRun", exe_name); + info->appdir = dirname(realpath("/proc/self/exe", NULL)); + + // Calculate the appdir root + int components = 1; + for (const char* c = FAKEBIN; *c; c++) { + if (*c == '/') { + components++; + } + } + + for (int i = 0; i < components; i++) { + info->appdir = dirname(info->appdir); + } + } + } + + if (!info->appdir) { + DIE("info->appdir was not set or could not be computed"); + } + + info->path = absolute(info, FAKEBIN); + info->meson_bin = absolute(info, FAKEBIN "/meson"); + info->python_bin = absolute(info, FAKEBIN "/python"); + info->ld_linux = absolute(info, "usr/lib/ld-linux.so"); + info->pythonhome = absolute(info, "usr"); + + if (exe_name) { + char* bin_dir = absolute(info, "usr/bin"); + info->exe_path = absolute_raw(bin_dir, exe_name); + free(bin_dir); + } +} + +void logArgs(char** args) { + if (!g_verbose) { + return; + } + + int counter = 0; + printf("\nArguments:\n"); + counter = 0; + while (1) { + if (!args[counter]) { + break; + } + printf(" %2d: %s\n", counter, args[counter]); + counter++; + } + + printf("\n"); + fflush(stdout); +} + +char* absolute_raw(const char* base, const char* relpath) { + const size_t absLen = strlen(base) + strlen(relpath) + 2; + char* abs = malloc(sizeof(char) * absLen); + snprintf(abs, absLen, "%s/%s", base, relpath); + return abs; +} + +char* absolute(AppRunInfo_t* info, const char* relpath) { + return absolute_raw(info->appdir, relpath); +} + +void envPrepend(const char* var, const char* val) { + char* curr = getenv(var); + if (!curr) { + setenv(var, val, 1); + return; + } + + const size_t len = strlen(var) + strlen(val) + strlen(curr) + 3; + char* res = malloc(sizeof(char) * len); + snprintf(res, len, "%s=%s:%s", var, val, curr); + putenv(res); +} diff --git a/packaging/appimage/wrappers/common.h b/packaging/appimage/wrappers/common.h new file mode 100644 index 000000000000..e010c637c6e4 --- /dev/null +++ b/packaging/appimage/wrappers/common.h @@ -0,0 +1,44 @@ +#pragma once + +// Some global defines to enable functions +#define _XOPEN_SOURCE 500 +#define _DEFAULT_SOURCE +#define _BSD_SOURCE +#define _GNU_SOURCE + +extern int g_verbose; + +#define LOG(fmt, ...) \ + if (g_verbose) { \ + printf(fmt "\n", ##__VA_ARGS__); \ + fflush(stdout); \ + } + +#define DIE(fmt, ...) \ + { \ + fprintf(stderr, "\x1b[31;1mFATAL ERROR:\x1b[0;1m " fmt "\x1b[0m\n", \ + ##__VA_ARGS__); \ + fflush(stderr); \ + exit(1); \ + } + +typedef struct AppRunInfo { + char* appdir; + char* appimage_path; + char* meson_bin; + char* python_bin; + + // basic path inf + char* exe_path; + char* path; + char* ld_linux; + char* pythonhome; +} AppRunInfo_t; + +void info_autofill_paths(AppRunInfo_t* inf, const char* exe_name); + +char* absolute_raw(const char* base, const char* relpath); +char* absolute(AppRunInfo_t* inf, const char* relpath); +void envPrepend(const char* var, const char* val); + +void logArgs(char** args); diff --git a/packaging/appimage/wrappers/meson.build b/packaging/appimage/wrappers/meson.build new file mode 100644 index 000000000000..8d53adaac506 --- /dev/null +++ b/packaging/appimage/wrappers/meson.build @@ -0,0 +1,44 @@ +project('apprun', ['c'], + default_options: [ + 'warning_level=3', + 'optimization=s', + 'werror=true', + 'debug=false', + 'strip=true', + 'b_lto=true', + 'prefix=/', + ], + version: '1.0', +) + +fakebin_dir = 'fakebin' +c_flags = [ + '-Wno-pedantic', + '-DAPPRUN_VERSION="@0@"'.format(meson.project_version()), + '-DFAKEBIN="@0@"'.format(fakebin_dir), +] +link_flags = ['-static'] + +common = static_library('common', ['common.c'], c_args : c_flags) + +wrappers = { + 'meson': ['-DIS_PYTHON_SCRIPT=1'], + 'ninja': [], + #'cmake': [], + 'pkgconf': [], + 'python': [], + 'python3': [], + 'python3.9': [], +} + +foreach name, extra_flags : wrappers + base_flags = ['-DREAL_EXE="' + name + '"'] + + executable(name, ['wrapper.c'], + link_with: [common], + c_args: c_flags + base_flags + extra_flags, + link_args: link_flags, + install: true, + install_dir: fakebin_dir + ) +endforeach diff --git a/packaging/appimage/wrappers/wrapper.c b/packaging/appimage/wrappers/wrapper.c new file mode 100644 index 000000000000..630bf45a55c9 --- /dev/null +++ b/packaging/appimage/wrappers/wrapper.c @@ -0,0 +1,79 @@ +#include "common.h" + +#include +#include +#include +#include + +// Assume that IS_PYTHON_SCRIPT is either 0 or 1 +#ifndef IS_PYTHON_SCRIPT +#define IS_PYTHON_SCRIPT 0 +#endif + +#ifndef STATICALLY_LINKED +#define STATICALLY_LINKED 0 +#endif + +int main(int argc, char* argv[]) { + AppRunInfo_t info; + char* args[argc + 3]; + int counter; + + g_verbose = 0; + + memset(&info, 0, sizeof(info)); + memset(&args, 0, sizeof(args)); + + // Check for verbose output + char* verbose = getenv("VERBOSE"); + if (verbose && verbose[0] != '0') { + g_verbose = 1; + } + + info_autofill_paths(&info, REAL_EXE); + + LOG("Meson exe wrapper " APPRUN_VERSION); + LOG("Running " REAL_EXE); + LOG("Extracted AppDir: %s", info.appdir); + LOG("Real exe location: %s", info.exe_path); + LOG("PATH fragment: %s", info.path); + LOG("Is Python script: %d", IS_PYTHON_SCRIPT); + LOG("Statically linked: %d", STATICALLY_LINKED); + + // Set the commandline arguments for exeve + // Again, IS_PYTHON_SCRIPT and STATICALLY_LINKED must be either 0 or 1 + counter = 2 + IS_PYTHON_SCRIPT - STATICALLY_LINKED; + for (int i = 1; i < argc; i++) { + args[counter++] = argv[i]; + } + + args[counter++] = NULL; + +#if IS_PYTHON_SCRIPT + args[0] = info.ld_linux; + args[1] = absolute(&info, "usr/bin/python3"); + args[2] = info.exe_path; +#elif STATICALLY_LINKED + args[0] = info.exe_path; +#else + args[0] = info.ld_linux; + args[1] = info.exe_path; +#endif + + // Set the env vars + envPrepend("PATH", info.path); + setenv("PYTHONHOME", info.pythonhome, 1); + setenv("MESON_COMMAND", info.meson_bin, 1); + setenv("MESON_PYTHON_BIN", info.python_bin, 1); + putenv("PYTHONDONTWRITEBYTECODE=1"); + + logArgs(args); + + if (execv(args[0], args) != 0) { + DIE("execv failed"); + } + + // We are technically leaking memory here, but all memory is freed once the + // program exits anyway... + return 0; +}