mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
Include only Furious/Data resources in standalone builds so repository guidance and other non-runtime package files are not copied into distributions. Signed-off-by: Loren Eteval <loren.eteval@proton.me>
1286 lines
41 KiB
Python
1286 lines
41 KiB
Python
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||
#
|
||
# 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 <https://www.gnu.org/licenses/>.
|
||
|
||
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} '
|
||
)
|
||
|
||
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'--include-package-data=Furious:Data/** '
|
||
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'--include-package-data=Furious:Data/** '
|
||
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'--include-package-data=Furious:Data/** '
|
||
f'--nofollow-import-to=numpy '
|
||
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'<?xml version=\"1.0\" encoding=\"utf-8\"?>\n'
|
||
f'<Wix\n'
|
||
f' xmlns=\"http://wixtoolset.org/schemas/v4/wxs\"\n'
|
||
f' xmlns:ui=\"http://wixtoolset.org/schemas/v4/wxs/ui\">\n'
|
||
f'\n'
|
||
f' <Package\n'
|
||
f' Name=\"{APPLICATION_NAME}\"\n'
|
||
f' Manufacturer=\"{APPLICATION_NAME}\"\n'
|
||
f' Version=\"{APPLICATION_VERSION}\"\n'
|
||
f' UpgradeCode=\"{uuid.uuid4()}\"\n'
|
||
f' Compressed=\"yes\">\n'
|
||
f'\n'
|
||
f' <!-- Major upgrade behavior -->\n'
|
||
f' <MajorUpgrade DowngradeErrorMessage=\"A newer version is already installed.\" />\n'
|
||
f' <MediaTemplate EmbedCab=\"yes\" />\n'
|
||
f'\n'
|
||
f' <!-- Enable InstallDir UI -->\n'
|
||
f' <ui:WixUI Id=\"WixUI_InstallDir\" />\n'
|
||
f' <Property Id=\"WIXUI_INSTALLDIR\" Value=\"INSTALLFOLDER\" />\n'
|
||
f'\n'
|
||
f' <!-- LICENSE shown in installer UI (RTF required) -->\n'
|
||
f' <WixVariable Id=\"WixUILicenseRtf\" Value=\"LICENSE.rtf\" />\n'
|
||
f'\n'
|
||
f' <!-- Installation directory -->\n'
|
||
f' <StandardDirectory Id=\"ProgramFiles6432Folder\">\n'
|
||
f' <Directory Id=\"INSTALLFOLDER\" Name=\"{WIN_UNZIPPED}\" />\n'
|
||
f' </StandardDirectory>\n'
|
||
f'\n'
|
||
f' <!-- Files harvested by WiX for the selected architecture -->\n'
|
||
f' <ComponentGroup Id="FuriousComponents" Directory="INSTALLFOLDER">\n'
|
||
f' <Files Include="{xml_escape(str(ROOT_DIR / DEPLOY_DIR_NAME / "SourceDir" / "**"))}" />\n'
|
||
f' </ComponentGroup>\n'
|
||
f'\n'
|
||
f' <!-- Features -->\n'
|
||
f' <Feature Id=\"MainFeature\" Title=\"{APPLICATION_NAME}\" Level=\"1\">\n'
|
||
f' <ComponentGroupRef Id=\"FuriousComponents\" />\n'
|
||
f' <ComponentRef Id=\"FuriousShortcut\" />\n'
|
||
f' </Feature>\n'
|
||
f'\n'
|
||
f' <!-- Desktop shortcut -->\n'
|
||
f' <Component\n'
|
||
f' Id=\"FuriousShortcut\"\n'
|
||
f' Guid=\"{uuid.uuid4()}\"\n'
|
||
f' Directory=\"INSTALLFOLDER\">\n'
|
||
f'\n'
|
||
f' <Shortcut\n'
|
||
f' Id=\"DesktopShortcut\"\n'
|
||
f' Directory=\"DesktopFolder\"\n'
|
||
f' Name=\"{APPLICATION_NAME}\"\n'
|
||
f' Description=\"{xml_escape(APPLICATION_DESCRIPTION)}\"\n'
|
||
f' Target=\"[INSTALLFOLDER]{APPLICATION_NAME}.exe\"\n'
|
||
f' WorkingDirectory=\"INSTALLFOLDER\" />\n'
|
||
f' </Component>\n'
|
||
f'\n'
|
||
f' </Package>\n'
|
||
f'\n'
|
||
f'</Wix>\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: 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 <<EOF\n'
|
||
f'[Desktop Entry]\n'
|
||
f'Type=Application\n'
|
||
f'Name={APPLICATION_NAME}\n'
|
||
f'Exec={APPLICATION_NAME}\n'
|
||
f'Icon={iconbase}\n'
|
||
f'Categories=Utility\n'
|
||
f'EOF\n'
|
||
f'\n'
|
||
f'mkdir -p %{{buildroot}}/usr/share/icons/hicolor/512x512/apps\n'
|
||
f'cp {iconbase}.png %{{buildroot}}/usr/share/icons/hicolor/512x512/apps/{iconbase}.png\n'
|
||
f'\n'
|
||
f'%files\n'
|
||
f'/opt/{APPLICATION_NAME}\n'
|
||
f'/usr/bin/{APPLICATION_NAME}\n'
|
||
f'/usr/share/applications/{APPLICATION_NAME}.desktop\n'
|
||
f'/usr/share/icons/hicolor/512x512/apps/{iconbase}.png\n'
|
||
)
|
||
except Exception:
|
||
# Any non-exit exceptions
|
||
|
||
raise
|
||
|
||
runExternalCommand(
|
||
[
|
||
'rpmbuild',
|
||
'-ba',
|
||
USER_HOME / 'rpmbuild' / 'SPECS' / f'{APPLICATION_NAME}.spec',
|
||
],
|
||
check=True,
|
||
)
|
||
|
||
for file in glob.glob(
|
||
(
|
||
USER_HOME / 'rpmbuild' / 'RPMS' / PLATFORM_MACHINE_LOWER / '*.rpm'
|
||
).as_posix()
|
||
):
|
||
shutil.copy(
|
||
file,
|
||
ROOT_DIR / LINUX_RPM_FILENAME,
|
||
)
|
||
else:
|
||
# Deploy: Not implemented
|
||
pass
|
||
|
||
sys.exit(EXIT_SUCCESS)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|