Files
LiHaohua 39158de976 Migrate all files from M5Stack-AppBuilder
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 14:30:34 +08:00

161 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""
Generic DEB packaging script for M5CardputerZero APPLaunch apps.
Follows the packaging conventions from:
M5CardputerZero-UserDemo/doc/APPLaunch-App-打包指南.md
"""
import argparse
import glob
import os
import shutil
import subprocess
from datetime import datetime
INSTALL_PREFIX = 'usr/share/APPLaunch'
BIN_PATH = f'{INSTALL_PREFIX}/bin'
LIB_PATH = f'{INSTALL_PREFIX}/lib'
SHARE_PATH = f'{INSTALL_PREFIX}/share'
APP_PATH = f'{INSTALL_PREFIX}/applications'
SERVICE_PATH = 'lib/systemd/system'
def build_deb(package_name, version, bin_name, app_name, src_folder, output_dir,
revision='m5stack1', with_service=True):
staging = os.path.join(output_dir, f'debian-{package_name}')
deb_file = os.path.join(output_dir,
f'{package_name}_{version}-{revision}_arm64.deb')
if os.path.exists(staging):
shutil.rmtree(staging)
os.makedirs(output_dir, exist_ok=True)
for d in [
os.path.join(staging, 'DEBIAN'),
os.path.join(staging, BIN_PATH),
os.path.join(staging, LIB_PATH),
os.path.join(staging, SHARE_PATH, 'font'),
os.path.join(staging, SHARE_PATH, 'images'),
os.path.join(staging, APP_PATH),
os.path.join(staging, SERVICE_PATH),
]:
os.makedirs(d, exist_ok=True)
# --- binary ---
bin_src = os.path.join(src_folder, bin_name)
if not os.path.exists(bin_src):
bin_src = os.path.join(src_folder, 'bin', bin_name)
if not os.path.exists(bin_src):
raise FileNotFoundError(f'Binary {bin_name} not found in {src_folder}')
shutil.copy2(bin_src, os.path.join(staging, BIN_PATH, bin_name))
# --- fonts ---
for f in glob.glob(os.path.join(src_folder, '**', '*.ttf'), recursive=True):
shutil.copy2(f, os.path.join(staging, SHARE_PATH, 'font'))
# --- images ---
for f in glob.glob(os.path.join(src_folder, '**', '*.png'), recursive=True):
shutil.copy2(f, os.path.join(staging, SHARE_PATH, 'images'))
# --- .desktop entry ---
icon = 'share/images/email.png'
pngs = glob.glob(os.path.join(staging, SHARE_PATH, 'images', '*.png'))
if pngs:
icon = f'share/images/{os.path.basename(pngs[0])}'
desktop = os.path.join(staging, APP_PATH, f'{package_name}.desktop')
with open(desktop, 'w') as f:
f.write('[Desktop Entry]\n')
f.write(f'Name={app_name}\n')
f.write(f'Exec=/{BIN_PATH}/{bin_name}\n')
f.write('Terminal=false\n')
f.write(f'Icon={icon}\n')
f.write('Type=Application\n')
# --- DEBIAN/control ---
with open(os.path.join(staging, 'DEBIAN', 'control'), 'w') as f:
f.write(f'Package: {package_name}\n')
f.write(f'Version: {version}\n')
f.write('Architecture: arm64\n')
f.write('Maintainer: m5stack <m5stack@m5stack.com>\n')
f.write(f'Section: APPLaunch\n')
f.write('Priority: optional\n')
f.write('Homepage: https://www.m5stack.com\n')
f.write(f'Packaged-Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
f.write(f'Description: M5CardputerZero {app_name}\n')
# --- DEBIAN/postinst ---
service_name = f'{package_name}.service'
with open(os.path.join(staging, 'DEBIAN', 'postinst'), 'w') as f:
f.write('#!/bin/sh\n')
if with_service:
f.write(f'[ -f "/lib/systemd/system/{service_name}" ] && systemctl enable {service_name}\n')
f.write(f'[ -f "/lib/systemd/system/{service_name}" ] && systemctl start {service_name}\n')
f.write('exit 0\n')
# --- DEBIAN/prerm ---
with open(os.path.join(staging, 'DEBIAN', 'prerm'), 'w') as f:
f.write('#!/bin/sh\n')
if with_service:
f.write(f'[ -f "/lib/systemd/system/{service_name}" ] && systemctl stop {service_name}\n')
f.write(f'[ -f "/lib/systemd/system/{service_name}" ] && systemctl disable {service_name}\n')
f.write('exit 0\n')
# --- systemd service ---
if with_service:
with open(os.path.join(staging, SERVICE_PATH, service_name), 'w') as f:
f.write('[Unit]\n')
f.write(f'Description={app_name} Service\n\n')
f.write('[Service]\n')
f.write(f'ExecStart=/{BIN_PATH}/{bin_name}\n')
f.write(f'WorkingDirectory=/{INSTALL_PREFIX}\n')
f.write('Restart=always\n')
f.write('RestartSec=1\n')
f.write('StartLimitInterval=0\n\n')
f.write('[Install]\n')
f.write('WantedBy=multi-user.target\n')
# --- permissions ---
os.chmod(os.path.join(staging, 'DEBIAN', 'postinst'), 0o755)
os.chmod(os.path.join(staging, 'DEBIAN', 'prerm'), 0o755)
os.chmod(os.path.join(staging, BIN_PATH, bin_name), 0o755)
# --- build .deb ---
subprocess.run(['dpkg-deb', '-b', staging, deb_file], check=True)
print(f'Created: {deb_file}')
shutil.rmtree(staging)
return deb_file
def main():
parser = argparse.ArgumentParser(description='Build DEB for M5CardputerZero APPLaunch')
parser.add_argument('--package-name', required=True, help='Package name (lowercase)')
parser.add_argument('--version', default='0.1', help='Package version')
parser.add_argument('--bin-name', required=True, help='Executable file name')
parser.add_argument('--app-name', default='', help='Display name in launcher')
parser.add_argument('--src-folder', required=True, help='Path to dist/ folder')
parser.add_argument('--output-dir', default='.', help='Output directory for .deb')
parser.add_argument('--revision', default='m5stack1', help='Package revision')
parser.add_argument('--no-service', action='store_true', help='Skip systemd service generation')
args = parser.parse_args()
if not args.app_name:
args.app_name = args.package_name
build_deb(
package_name=args.package_name,
version=args.version,
bin_name=args.bin_name,
app_name=args.app_name,
src_folder=args.src_folder,
output_dir=args.output_dir,
revision=args.revision,
with_service=not args.no_service,
)
if __name__ == '__main__':
main()