# Copyright (C) 2024–present Loren Eteval & contributors # # This file is part of Furious. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import annotations from Furious.Frozenlib import * import os import re import sys import uuid import glob import shutil import logging import tarfile import pathlib import argparse import datetime import subprocess import urllib.request from typing import AnyStr from xml.sax.saxutils import escape as xml_escape logging.basicConfig( format='[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s', level=logging.INFO, ) logging.raiseExceptions = False logger = logging.getLogger('Deploy') # Exit status EXIT_SUCCESS = 0 EXIT_FAILURE = 1 USER_HOME = pathlib.Path.home() DEPLOY_DIR_NAME = f'{APPLICATION_NAME}-Deploy' HYSTERIA_DATA_DIR = DATA_DIR / 'hysteria' WINDOWS_7_COMPATIBILITY_DLL_NAME = 'api-ms-win-core-path-l1-1-0.dll' NUITKA_BINARY_VERSION_OPTION = ( f'--company-name={APPLICATION_NAME} ' f'--product-name={APPLICATION_NAME} ' f'--file-version={APPLICATION_VERSION} ' f'--product-version={APPLICATION_VERSION} ' ) NUITKA_PACKAGE_DATA_OPTIONS = ( f'--include-package-data=Furious:Data/** ' f'--noinclude-data-files=Furious/Data/*.md ' f'--noinclude-data-files=Furious/Data/**/*.md ' ) PLATFORM_MACHINE_LOWER = PLATFORM_MACHINE.casefold() if PLATFORM == 'Windows': windowsArchitectures = { 'amd64': ('amd64', 'x64'), 'x86_64': ('amd64', 'x64'), 'arm64': ('arm64', 'arm64'), 'aarch64': ('arm64', 'arm64'), } try: WINDOWS_ARTIFACT_ARCHITECTURE, WINDOWS_WIX_ARCHITECTURE = windowsArchitectures[ PLATFORM_MACHINE_LOWER ] except KeyError: raise RuntimeError(f'unsupported Windows architecture: {PLATFORM_MACHINE}') if PLATFORM == 'Windows': NUITKA_BINARY_VERSION_OPTION += f'--file-description=\"{APPLICATION_DESCRIPTION}\" ' if PLATFORM == 'Windows' or PLATFORM == 'Darwin': NUITKA_BINARY_VERSION_OPTION += f'--copyright=\"Copyright (C) 2024–present {APPLICATION_AUTHOR_NAME} & contributors <{APPLICATION_AUTHOR_EMAIL}>\" ' if PLATFORM == 'Windows': NUITKA_BUILD = ( f'python -m nuitka ' f'--standalone --plugin-enable=pyside6 ' f'--disable-console ' f'--assume-yes-for-downloads ' f'{NUITKA_PACKAGE_DATA_OPTIONS}' f'--nofollow-import-to=numpy ' f'{NUITKA_BINARY_VERSION_OPTION}' f'--windows-icon-from-ico=\"Icons/png/rocket-takeoff-window.png\" ' f'--force-stdout-spec=^%TEMP^%/_Furious_Enable_Stdout ' f'--force-stderr-spec=^%TEMP^%/_Furious_Enable_Stderr ' f'Furious ' f'--output-dir=\"{ROOT_DIR / DEPLOY_DIR_NAME}\"' ) elif PLATFORM == 'Darwin': NUITKA_BUILD = ( f'python -m nuitka ' f'--include-module=objc ' f'--include-module=AppKit ' f'--include-module=Cocoa ' f'--standalone --plugin-enable=pyside6 ' f'--disable-console ' f'--assume-yes-for-downloads ' f'{NUITKA_PACKAGE_DATA_OPTIONS}' f'--nofollow-import-to=numpy ' f'{NUITKA_BINARY_VERSION_OPTION}' f'--macos-create-app-bundle ' f'--macos-app-icon=\"Icons/png/rocket-takeoff-window.png\" ' f'--macos-app-name=\"Furious\" ' f'Furious-GUI.py ' f'--output-dir=\"{ROOT_DIR / DEPLOY_DIR_NAME}\"' ) elif PLATFORM == 'Linux': NUITKA_BUILD = ( f'python -m nuitka ' f'--standalone --plugin-enable=pyside6 ' f'--disable-console ' f'--assume-yes-for-downloads ' f'{NUITKA_PACKAGE_DATA_OPTIONS}' f'--nofollow-import-to=numpy ' # The Essentials-only Linux build does not ship QtPdf, and Furious does # not use the optional TIFF/WebP codecs or the embedded EGLFS backend. # Excluding them at collection time prevents unusable plugins from # acquiring host-specific dependencies in the portable artifacts. f'--noinclude-qt-plugins=egldeviceintegrations ' f'--noinclude-dlls=*/libqeglfs*.so ' f'--noinclude-dlls=*/libqpdf.so ' f'--noinclude-dlls=*/libqtiff.so ' f'--noinclude-dlls=*/libqwebp.so ' f'{NUITKA_BINARY_VERSION_OPTION}' f'Furious ' f'--output-dir=\"{ROOT_DIR / DEPLOY_DIR_NAME}\"' ) else: # Deploy: Not implemented NUITKA_BUILD = '' if PLATFORM == 'Windows': if PLATFORM_RELEASE.endswith('Server'): # Windows server. Fixed to windows10 winVerCompatible = f'{PLATFORM.casefold()}10' else: winVerCompatible = f'{PLATFORM.casefold()}{PLATFORM_RELEASE}' if os.environ.get('WIN_VER_COMPATIBLE', ''): winVerCompatible = os.environ['WIN_VER_COMPATIBLE'] ARTIFACT_NAME = ( f'{APPLICATION_NAME}-{APPLICATION_VERSION}-' f'{winVerCompatible}-{WINDOWS_ARTIFACT_ARCHITECTURE}' ) WIN_UNZIPPED = ARTIFACT_NAME elif PLATFORM == 'Darwin': value = versionToValue(PYSIDE6_VERSION) # https://doc.qt.io/qt-6/supported-platforms.html if value <= versionToValue('6.4.3'): macVerCompatible = 'macOS-10.9' elif value <= versionToValue('6.7.3'): macVerCompatible = 'macos-11.0' else: macVerCompatible = 'macOS-12.0' ARTIFACT_NAME = ( f'{APPLICATION_NAME}-{APPLICATION_VERSION}-' f'{macVerCompatible}-{PLATFORM_MACHINE_LOWER}' ) MAC_APP_DIR = ROOT_DIR / 'app' MAC_DMG_FILENAME = f'{ARTIFACT_NAME}.dmg' MAC_CREATE_DMG_CMD = ( f'create-dmg ' f'--volname \"Furious\" ' f'--volicon \"Icons/png/rocket-takeoff-window.png\" ' f'--window-pos 200 120 ' f'--window-size 600 300 ' f'--icon-size 100 ' f'--icon \"Furious-GUI.app\" 175 120 ' f'--hide-extension \"Furious-GUI.app\" ' f'--app-drop-link 425 120 ' f'\"{ROOT_DIR / MAC_DMG_FILENAME}\" ' f'\"{MAC_APP_DIR}\"' ) elif PLATFORM == 'Linux': LINUX_APP_DIR = ROOT_DIR / 'AppDir' LINUX_ICON_FILE = 'rocket-takeoff-window-512x512.png' LINUX_DESKTOP_FILE = f'{APPLICATION_NAME}.desktop' ARTIFACT_NAME = ( f'{APPLICATION_NAME}-{APPLICATION_VERSION}-linux-{PLATFORM_MACHINE_LOWER}' ) LINUX_APPIMAGE_FILENAME = f'{ARTIFACT_NAME}.AppImage' LINUX_APPIMAGE_BUILDER = os.environ.get( 'LINUX_APPIMAGE_BUILDER', 'appimagetool' ).casefold() if LINUX_APPIMAGE_BUILDER == 'linuxdeploy': LINUX_CREATE_APPIMAGE_CMD = [ f'linuxdeploy-{PLATFORM_MACHINE}.AppImage', '--appdir', LINUX_APP_DIR, '-d', LINUX_APP_DIR / LINUX_DESKTOP_FILE, '-i', LINUX_APP_DIR / LINUX_ICON_FILE, '--plugin', 'qt', '--output', 'appimage', ] elif LINUX_APPIMAGE_BUILDER == 'appimagetool': LINUX_CREATE_APPIMAGE_CMD = [ f'appimagetool-{PLATFORM_MACHINE}.AppImage', LINUX_APP_DIR, ROOT_DIR / LINUX_APPIMAGE_FILENAME, ] else: raise RuntimeError( f'unsupported Linux AppImage builder: {LINUX_APPIMAGE_BUILDER!r}' ) LINUX_DEBIAN_DIR = ROOT_DIR / 'debian' LINUX_DEB_FILENAME = f'{ARTIFACT_NAME}.deb' LINUX_FLATPAK_DIR = ROOT_DIR / 'flatpak' LINUX_FLATPAK_FILENAME = f'{ARTIFACT_NAME}.flatpak' LINUX_RPM_DIR = ROOT_DIR / 'rpmbuild' LINUX_RPM_FILENAME = f'{ARTIFACT_NAME}.rpm' else: # Deploy: Not implemented ARTIFACT_NAME = '' def downloadFromURL(url, filepath): try: import requests except ImportError: raise ModuleNotFoundError('missing requests module') # Send an HTTP GET request to the URL response = requests.get(url) try: response.raise_for_status() # Check if the request was successful # Open the save_path in write-binary mode and write the content of the response with open(filepath, 'wb') as file: file.write(response.content) except Exception: # Any non-exit exceptions return response.status_code, False else: return response.status_code, True def downloadXrayAssets(url, filepath): # Make sure the save directory exists if not os.path.exists(XRAY_ASSET_DIR): os.makedirs(XRAY_ASSET_DIR) code, success = downloadFromURL(url, filepath) if success: logger.info(f'xray asset downloaded successfully and saved to {filepath}') else: logger.error(f'failed to download xray asset from {url}. Status code: {code}') return success def downloadHy1Asset(): def writeHy1Rules(response, pattern_, action_, file_): count_ = 0 for domain in response: m = re.search(pattern_, domain.decode('utf-8').strip()).group() if m != 'payload': count_ += 1 file_.write(f'{action_} domain-suffix {m}\n') return count_ try: urllib.request.urlretrieve( 'https://github.com/Loyalsoldier/geoip/releases/latest/download//Country.mmdb', HYSTERIA_DATA_DIR / 'country.mmdb', ) today = datetime.date.today().strftime('%B %d, %Y') pattern = re.compile(r'([a-z]|[0-9]|[A-Z])(.*[a-z]|[A-Z])') with open( HYSTERIA_DATA_DIR / 'bypass-mainland-China.acl', 'w', encoding='utf-8', ) as file: file.write( f'# Author:github.com/A1-hub\n' f'# Author:github.com/{APPLICATION_REPO_OWNER_NAME}\n' f'# hysteria acl rules\n' f'# Generated on {today}\n\n' ) for action, url in zip( [ 'block', 'direct', 'proxy', ], [ 'https://github.com/Loyalsoldier/clash-rules/releases/latest/download/reject.txt', 'https://github.com/Loyalsoldier/clash-rules/releases/latest/download/direct.txt', 'https://github.com/Loyalsoldier/clash-rules/releases/latest/download/proxy.txt', ], ): count = writeHy1Rules( urllib.request.urlopen(url), pattern, action, file ) logger.debug(f'genereated {count} {action} rules') file.write('direct country cn\nproxy all\n') except Exception as ex: # Any non-exit exceptions logger.error(f'download hy1 asset failed. {ex}') return False else: logger.info(f'download hy1 asset success') return True def cleanup(): def rmtree(path, action: str = ''): try: shutil.rmtree(path) except Exception as ex: # Any non-exit exceptions if action: logger.error(f'{action} failed: {ex}') else: if action: logger.info(f'{action} success') def remove(path, action: str = ''): try: os.remove(path) except Exception as ex: # Any non-exit exceptions if action: logger.error(f'{action} failed: {ex}') else: if action: logger.info(f'{action} success') rmtree(ROOT_DIR / DEPLOY_DIR_NAME, 'remove deployment dir') if PLATFORM == 'Windows': # More cleanup on Windows # Remove artifact remove(ROOT_DIR / f'{ARTIFACT_NAME}.zip', 'remove artifact') # Remove artifact remove(ROOT_DIR / f'{ARTIFACT_NAME}.msi', 'remove artifact') # Remove artifact remove(ROOT_DIR / f'{ARTIFACT_NAME}.wixpdb', 'remove artifact') # Remove unzipped folder rmtree(ROOT_DIR / WIN_UNZIPPED, 'remove unzipped dir') elif PLATFORM == 'Darwin': # More cleanup on Darwin # Remove artifact remove(ROOT_DIR / MAC_DMG_FILENAME, 'remove artifact') # Remove app folder rmtree(MAC_APP_DIR, 'remove app dir') elif PLATFORM == 'Linux': # More cleanup on Linux # Cleanup AppImage environment # Remove artifact remove(ROOT_DIR / LINUX_APPIMAGE_FILENAME, 'remove artifact') # Remove app folder rmtree(LINUX_APP_DIR, 'remove app dir') # Cleanup deb environment # Remove artifact remove(ROOT_DIR / LINUX_DEB_FILENAME, 'remove artifact') # Remove debian folder rmtree(LINUX_DEBIAN_DIR, 'remove debian dir') # Cleanup flatpak environment # Remove artifact remove(ROOT_DIR / LINUX_FLATPAK_FILENAME, 'remove artifact') # Remove flatpak folder rmtree(LINUX_FLATPAK_DIR, 'remove flatpak dir') # Cleanup rpm environment # Remove artifact remove(ROOT_DIR / LINUX_RPM_FILENAME, 'remove artifact') # Remove rpm folder rmtree(LINUX_RPM_DIR, 'remove rpm dir') # Remove home rpmbuild folder rmtree(USER_HOME / 'rpmbuild', 'remove home rpmbuild') else: # Deploy: Not implemented pass def download(): return all( [ downloadXrayAssets(URL_GEOSITE, XRAY_ASSET_PATH_GEOSITE), downloadXrayAssets(URL_GEOIP, XRAY_ASSET_PATH_GEOIP), downloadHy1Asset(), ] ) def printStandardStream(stdout: AnyStr, stderr: AnyStr): if isinstance(stdout, str): encoded_stdout = stdout.encode() else: encoded_stdout = stdout if isinstance(stderr, str): encoded_stderr = stderr.encode() else: encoded_stderr = stderr encoded_output = b'stdout:\n%sstderr:\n%s' % (encoded_stdout, encoded_stderr) print(encoded_output.decode('utf-8', 'ignore'), flush=True) def main(): parser = argparse.ArgumentParser() parser.add_argument( '-d', '--download', action='store_true', help='Download latest asset files', ) parser.add_argument( '-c', '--cleanup', action='store_true', help='Cleanup deployment files', ) args = parser.parse_args() if args.cleanup: cleanup() logger.info('cleanup done') sys.exit(EXIT_SUCCESS) if args.download: success = EXIT_SUCCESS if download() else EXIT_FAILURE sys.exit(success) try: import nuitka except ImportError: raise ModuleNotFoundError('please install nuitka to run this script') logger.info('building') try: result = runExternalCommand( NUITKA_BUILD, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, check=True, ) except subprocess.CalledProcessError as err: logger.error(f'build failed with returncode {err.returncode}') printStandardStream(err.stdout, err.stderr) sys.exit(EXIT_FAILURE) else: logger.info(f'build success') printStandardStream(result.stdout, result.stderr) if PLATFORM == 'Windows': distributionPath = ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist' if winVerCompatible.casefold() == 'windows7': compatibilityDll = pathlib.Path(sys.executable).with_name( WINDOWS_7_COMPATIBILITY_DLL_NAME ) if not compatibilityDll.is_file(): raise FileNotFoundError( f'Windows 7 compatibility DLL not found: {compatibilityDll}' ) shutil.copy2(compatibilityDll, distributionPath) logger.info(f'included Windows 7 compatibility DLL: {compatibilityDll}') try: shutil.rmtree(ROOT_DIR / DEPLOY_DIR_NAME / WIN_UNZIPPED) except FileNotFoundError: pass except Exception: # Any non-exit exceptions raise # Copy LICENSE to distribution folder shutil.copy( ROOT_DIR / 'LICENSE', distributionPath, ) shutil.copy( ROOT_DIR / 'LICENSE.rtf', ROOT_DIR / DEPLOY_DIR_NAME, ) shutil.copytree( distributionPath, ROOT_DIR / DEPLOY_DIR_NAME / WIN_UNZIPPED, ) shutil.make_archive( ARTIFACT_NAME, 'zip', ROOT_DIR / DEPLOY_DIR_NAME, WIN_UNZIPPED, logger=logger, ) shutil.copytree( ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist', ROOT_DIR / DEPLOY_DIR_NAME / 'SourceDir', ) msifolder = ROOT_DIR / DEPLOY_DIR_NAME / 'msi' os.makedirs(msifolder, exist_ok=True) # Create Product.wxs file try: with open( msifolder / 'Product.wxs', 'w', encoding='utf-8', ) as file: file.write( f'\n' f'\n' f'\n' f' \n' f'\n' f' \n' f' \n' f' \n' f'\n' f' \n' f' \n' f' \n' f'\n' f' \n' f' \n' f'\n' f' \n' f' \n' f' \n' f' \n' f'\n' f' \n' f' \n' f' \n' f' \n' f'\n' f' \n' f' \n' f' \n' f' \n' f' \n' f'\n' f' \n' f' \n' f'\n' f' \n' f' \n' f'\n' f' \n' f'\n' f'\n' ) except Exception: # Any non-exit exceptions raise wixui = 'WixUI_InstallDir.wxs' code, success = downloadFromURL( f'https://raw.githubusercontent.com/LorenEteval/wix/' f'refs/heads/main/src/ext/UI/wixlib/{wixui}', msifolder / wixui, ) if success: logger.info(f'download \'{wixui}\' success\'') else: logger.error(f'download \'{wixui}\' failed\'. Status code: {code}') raise cwd = os.getcwd() try: os.chdir(ROOT_DIR / DEPLOY_DIR_NAME) # Requires "wix extension add WixToolset.UI.wixext" result = runExternalCommand( [ 'wix', 'build', msifolder / 'Product.wxs', '-arch', WINDOWS_WIX_ARCHITECTURE, '-o', ROOT_DIR / f'{ARTIFACT_NAME}.msi', '-ext', 'WixToolset.UI.wixext', ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) except subprocess.CalledProcessError as err: logger.error(f'wix build failed with returncode {err.returncode}') printStandardStream(err.stdout, err.stderr) sys.exit(EXIT_FAILURE) else: logger.info(f'wix build success: {ARTIFACT_NAME}.msi') printStandardStream(result.stdout, result.stderr) finally: os.chdir(cwd) elif PLATFORM == 'Darwin': try: shutil.rmtree(MAC_APP_DIR) except FileNotFoundError: pass except Exception: # Any non-exit exceptions raise try: os.mkdir(MAC_APP_DIR) except Exception: # Any non-exit exceptions raise sourceApp = ROOT_DIR / DEPLOY_DIR_NAME / 'Furious-GUI.app' stagedApp = MAC_APP_DIR / 'Furious-GUI.app' try: result = runExternalCommand( ( 'ditto', '--rsrc', '--extattr', str(sourceApp), str(stagedApp), ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) except subprocess.CalledProcessError as err: logger.error(f'stage macOS app failed with returncode {err.returncode}') printStandardStream(err.stdout, err.stderr) sys.exit(EXIT_FAILURE) else: logger.info('stage macOS app success') printStandardStream(result.stdout, result.stderr) logger.info('generating dmg') try: result = runExternalCommand( MAC_CREATE_DMG_CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, check=True, ) except subprocess.CalledProcessError as err: logger.error(f'generate dmg failed with returncode {err.returncode}') printStandardStream(err.stdout, err.stderr) sys.exit(EXIT_FAILURE) else: logger.info(f'generate dmg success: {MAC_DMG_FILENAME}') printStandardStream(result.stdout, result.stderr) elif PLATFORM == 'Linux': try: shutil.rmtree(LINUX_APP_DIR) except FileNotFoundError: pass except Exception: # Any non-exit exceptions raise try: os.makedirs(LINUX_APP_DIR / 'usr' / 'bin', exist_ok=True) except Exception: # Any non-exit exceptions raise iconbase = 'rocket-takeoff-window-512x512' shutil.copytree( ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist', LINUX_APP_DIR / 'usr' / 'bin', dirs_exist_ok=True, ) shutil.copy( ROOT_DIR / 'Icons' / 'png' / f'{iconbase}.png', LINUX_APP_DIR, ) DESKTOP_FILE_CONTENT = ( f'[Desktop Entry]\n' f'Type=Application\n' f'Name={APPLICATION_NAME}\n' f'Exec={APPLICATION_NAME}.bin\n' f'Icon={iconbase}\n' f'Terminal=false\n' f'Categories=Utility\n' ) try: with open( LINUX_APP_DIR / LINUX_DESKTOP_FILE, 'w', encoding='utf-8', ) as file: file.write(DESKTOP_FILE_CONTENT) except Exception: # Any non-exit exceptions raise appRun = LINUX_APP_DIR / 'AppRun' with open(appRun, 'w', encoding='utf-8') as file: file.write( '#!/bin/sh\n' 'APPDIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"\n' f'exec "$APPDIR/usr/bin/{APPLICATION_NAME}.bin" "$@"\n' ) appRun.chmod(0o755) logger.info('generating AppImage') if LINUX_APPIMAGE_BUILDER == 'linuxdeploy': os.environ['LDAI_OUTPUT'] = LINUX_APPIMAGE_FILENAME os.environ['LINUXDEPLOY_OUTPUT_VERSION'] = APPLICATION_VERSION else: # Nuitka's standalone directory already owns its complete Qt runtime. # appimagetool packages it as-is instead of harvesting Qt a second time. os.environ['ARCH'] = PLATFORM_MACHINE try: result = runExternalCommand( LINUX_CREATE_APPIMAGE_CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) except subprocess.CalledProcessError as err: logger.error(f'generate AppImage failed with returncode {err.returncode}') printStandardStream(err.stdout, err.stderr) sys.exit(EXIT_FAILURE) else: logger.info(f'generate AppImage success: {LINUX_APPIMAGE_FILENAME}') printStandardStream(result.stdout, result.stderr) try: result = runExternalCommand( ['chmod', '+x', LINUX_APPIMAGE_FILENAME], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) except subprocess.CalledProcessError as err: logger.error(f'chmod +x failed with returncode {err.returncode}') printStandardStream(err.stdout, err.stderr) sys.exit(EXIT_FAILURE) else: logger.info(f'chmod +x success: {LINUX_APPIMAGE_FILENAME}') printStandardStream(result.stdout, result.stderr) # Create .deb package logger.info('generating deb') # Prepare package layout try: os.makedirs(LINUX_DEBIAN_DIR / 'DEBIAN', exist_ok=True) os.makedirs(LINUX_DEBIAN_DIR / 'usr' / 'bin', exist_ok=True) os.makedirs( LINUX_DEBIAN_DIR / 'usr' / 'share' / APPLICATION_NAME, exist_ok=True, ) os.makedirs( LINUX_DEBIAN_DIR / 'usr' / 'share' / 'applications', exist_ok=True, ) os.makedirs( LINUX_DEBIAN_DIR / 'usr' / 'share' / 'icons' / 'hicolor' / '512x512' / 'apps', exist_ok=True, ) except Exception: # Any non-exit exceptions raise # Copy output shutil.copytree( ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist', LINUX_DEBIAN_DIR / 'usr' / 'share' / APPLICATION_NAME, dirs_exist_ok=True, ) shutil.copy( ROOT_DIR / 'Icons' / 'png' / f'{iconbase}.png', LINUX_DEBIAN_DIR / 'usr' / 'share' / 'icons' / 'hicolor' / '512x512' / 'apps', ) # Create launcher script try: launcherScript = LINUX_DEBIAN_DIR / 'usr' / 'bin' / APPLICATION_NAME with open( launcherScript, 'w', encoding='utf-8', ) as file: file.write( f'#!/bin/bash\n' f'exec /usr/share/{APPLICATION_NAME}/{APPLICATION_NAME}.bin \"$@\"\n' ) runExternalCommand( ['chmod', '+x', launcherScript], check=True, ) except Exception: # Any non-exit exceptions raise # Create desktop entry file try: with open( LINUX_DEBIAN_DIR / 'usr' / 'share' / 'applications' / LINUX_DESKTOP_FILE, 'w', encoding='utf-8', ) as file: file.write(DESKTOP_FILE_CONTENT) except Exception: # Any non-exit exceptions raise # Create control file try: if PLATFORM_MACHINE_LOWER == 'x86_64': arch = 'amd64' elif PLATFORM_MACHINE_LOWER == 'aarch64': arch = 'arm64' else: raise with open( LINUX_DEBIAN_DIR / 'DEBIAN' / 'control', 'w', encoding='utf-8', ) as file: file.write( f'Package: {APPLICATION_NAME}\n' f'Version: {APPLICATION_VERSION}\n' f'Section: utils\n' f'Priority: optional\n' f'Architecture: {arch}\n' f'Depends: libc6, libstdc++6, libgl1, libxcb-cursor0, libxcb-xinerama0\n' f'Maintainer: {APPLICATION_AUTHOR_NAME} <{APPLICATION_AUTHOR_EMAIL}>\n' f'Description: {APPLICATION_DESCRIPTION}\n' f'Homepage: {APPLICATION_ABOUT_PAGE}\n' ) except Exception: # Any non-exit exceptions raise # Fix permissions try: runExternalCommand( f'find debian -type d -exec chmod 755 {{}} \\;', shell=True, check=True, ) runExternalCommand( f'find debian -type f -exec chmod 644 {{}} \\;', shell=True, check=True, ) runExternalCommand( ['chmod', '755', LINUX_DEBIAN_DIR / 'usr' / 'bin' / APPLICATION_NAME], check=True, ) runExternalCommand( [ 'chmod', '755', LINUX_DEBIAN_DIR / 'usr' / 'share' / APPLICATION_NAME / f'{APPLICATION_NAME}.bin', ], check=True, ) except Exception: # Any non-exit exceptions raise try: runExternalCommand( f'dpkg-deb --build debian {LINUX_DEB_FILENAME}', shell=True, check=True, ) except Exception: # Any non-exit exceptions raise # Create .flatpak package logger.info('generating flatpak') # Prepare package layout try: os.makedirs(LINUX_FLATPAK_DIR / APPLICATION_NAME, exist_ok=True) except Exception: # Any non-exit exceptions raise # Copy output shutil.copytree( ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist', LINUX_FLATPAK_DIR / APPLICATION_NAME, dirs_exist_ok=True, ) shutil.copy( ROOT_DIR / 'Icons' / 'png' / f'{iconbase}.png', LINUX_FLATPAK_DIR / f'{APPLICATION_FLATPAK_ID}.png', ) # Create flatpak manifest file try: with open( LINUX_FLATPAK_DIR / f'{APPLICATION_FLATPAK_ID}.yml', 'w', encoding='utf-8', ) as file: file.write( f'app-id: {APPLICATION_FLATPAK_ID}\n' f'runtime: org.kde.Platform\n' f'runtime-version: \"6.8\"\n' f'sdk: org.kde.Sdk\n' f'command: {APPLICATION_NAME}\n' f'\n' f'finish-args:\n' f' - --share=ipc\n' f' - --share=network\n' f' - --socket=wayland\n' f' - --socket=fallback-x11\n' f' - --device=dri\n' f' - --filesystem=home\n' f' - --talk-name=org.freedesktop.Flatpak\n' f'\n' f'modules:\n' f' - name: double-conversion\n' f' buildsystem: cmake-ninja\n' f' config-opts:\n' f' - -DBUILD_SHARED_LIBS=ON\n' f' - -DBUILD_TESTING=OFF\n' f' - -DCMAKE_INSTALL_LIBDIR=lib\n' f' sources:\n' f' - type: archive\n' f' url: https://github.com/google/double-conversion/archive/refs/tags/v3.3.1.tar.gz\n' f' sha256: fe54901055c71302dcdc5c3ccbe265a6c191978f3761ce1414d0895d6b0ea90e\n' f' - name: mit-krb5\n' f' buildsystem: simple\n' f' sources:\n' f' - type: archive\n' f' url: https://kerberos.org/dist/krb5/1.21/krb5-1.21.3.tar.gz\n' f' sha256: b7a4cd5ead67fb08b980b21abd150ff7217e85ea320c9ed0c6dadd304840ad35\n' f' build-commands:\n' f' - cd src && autoreconf -i # Ensure Autotools files are generated\n' f' - cd src && ./configure --prefix=/app --disable-static --enable-shared\n' f' - cd src && make -j$(nproc)\n' f' - cd src && make install\n' f' - name: {APPLICATION_NAME}\n' f' buildsystem: simple\n' f' build-commands:\n' f' # Script\n' f' - install -Dm755 {APPLICATION_NAME}.sh /app/bin/{APPLICATION_NAME}\n' f' # Whole application directory\n' f' - mkdir -p /app/lib/{APPLICATION_NAME}/\n' f' - cp -a . /app/lib/{APPLICATION_NAME}/\n' f' # Desktop + icon\n' f' - install -Dm644 {APPLICATION_FLATPAK_ID}.desktop /app/share/applications/{APPLICATION_FLATPAK_ID}.desktop\n' f' - install -Dm644 {APPLICATION_FLATPAK_ID}.png /app/share/icons/hicolor/512x512/apps/{APPLICATION_FLATPAK_ID}.png\n' f' sources:\n' f' - type: file\n' f' path: {APPLICATION_NAME}.sh\n' f' - type: dir\n' f' path: {APPLICATION_NAME}\n' f' - type: file\n' f' path: {APPLICATION_FLATPAK_ID}.desktop\n' f' - type: file\n' f' path: {APPLICATION_FLATPAK_ID}.png\n' ) except Exception: # Any non-exit exceptions raise # Create desktop entry file try: with open( LINUX_FLATPAK_DIR / f'{APPLICATION_FLATPAK_ID}.desktop', 'w', encoding='utf-8', ) as file: file.write( f'[Desktop Entry]\n' f'Type=Application\n' f'Name={APPLICATION_NAME}\n' f'Exec={APPLICATION_NAME}\n' f'Icon={APPLICATION_FLATPAK_ID}\n' f'Terminal=false\n' f'Categories=Utility\n' ) except Exception: # Any non-exit exceptions raise # Create launcher script try: with open( LINUX_FLATPAK_DIR / f'{APPLICATION_NAME}.sh', 'w', encoding='utf-8', ) as file: file.write( f'#!/usr/bin/env bash\n' f'exec /app/lib/{APPLICATION_NAME}/{APPLICATION_NAME}.bin \"$@\"\n' ) except Exception: # Any non-exit exceptions raise cwd = os.getcwd() try: os.chdir(LINUX_FLATPAK_DIR) runExternalCommand( f'flatpak-builder build-dir {APPLICATION_FLATPAK_ID}.yml --force-clean', shell=True, check=True, ) runExternalCommand( f'flatpak build-export repo build-dir', shell=True, check=True, ) runExternalCommand( f'flatpak build-bundle repo {LINUX_FLATPAK_FILENAME} {APPLICATION_FLATPAK_ID}', shell=True, check=True, ) shutil.copy( LINUX_FLATPAK_FILENAME, ROOT_DIR, ) except Exception: # Any non-exit exceptions raise finally: os.chdir(cwd) # Create .rpm package logger.info('generating rpm') # Prepare package layout try: os.makedirs( LINUX_RPM_DIR / f'{APPLICATION_NAME}-{APPLICATION_VERSION}', exist_ok=True, ) for name in ['BUILD', 'RPMS', 'SOURCES', 'SPECS', 'SRPMS']: os.makedirs( USER_HOME / 'rpmbuild' / name, exist_ok=True, ) except Exception: # Any non-exit exceptions raise # Copy output shutil.copytree( ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist', LINUX_RPM_DIR / f'{APPLICATION_NAME}-{APPLICATION_VERSION}', dirs_exist_ok=True, ) shutil.copy( ROOT_DIR / 'Icons' / 'png' / f'{iconbase}.png', LINUX_RPM_DIR / f'{APPLICATION_NAME}-{APPLICATION_VERSION}', ) with tarfile.open(LINUX_RPM_DIR / f'{ARTIFACT_NAME}.tar.gz', 'w:gz') as tar: tar.add( LINUX_RPM_DIR / f'{APPLICATION_NAME}-{APPLICATION_VERSION}', arcname=f'{APPLICATION_NAME}-{APPLICATION_VERSION}', ) shutil.move( LINUX_RPM_DIR / f'{ARTIFACT_NAME}.tar.gz', USER_HOME / 'rpmbuild' / 'SOURCES', ) # Create rpm manifest file try: with open( USER_HOME / 'rpmbuild' / 'SPECS' / f'{APPLICATION_NAME}.spec', 'w', encoding='utf-8', ) as file: file.write( f'Name: {APPLICATION_NAME}\n' f'Version: {APPLICATION_VERSION}\n' f'Release: 1%{{?dist}}\n' f'Summary: {APPLICATION_DESCRIPTION}\n' f'\n' f'License: GPLv3\n' f'URL: {APPLICATION_ABOUT_PAGE}\n' f'Source0: {ARTIFACT_NAME}.tar.gz\n' f'\n' f'BuildArch: {PLATFORM_MACHINE_LOWER}\n' f'\n' f'%description\n' f'{APPLICATION_DESCRIPTION}\n' f'\n' f'%prep\n' f'%autosetup\n' f'\n' f'%install\n' f'rm -rf %{{buildroot}}\n' f'\n' f'# App directory\n' f'mkdir -p %{{buildroot}}/opt/{APPLICATION_NAME}\n' f'cp -a * %{{buildroot}}/opt/{APPLICATION_NAME}/\n' f'\n' f'# Binary symlink\n' f'mkdir -p %{{buildroot}}/usr/bin\n' f'ln -s /opt/{APPLICATION_NAME}/{APPLICATION_NAME}.bin %{{buildroot}}/usr/bin/{APPLICATION_NAME}\n' f'\n' f'# Desktop entry\n' f'mkdir -p %{{buildroot}}/usr/share/applications\n' f'cat > %{{buildroot}}/usr/share/applications/{APPLICATION_NAME}.desktop <