mirror of
https://github.com/nyakang/nyaterm.git
synced 2026-09-22 08:01:31 +00:00
Ship nyaterm-mcp beside the application in native packages and verify its presence in packaging tests and AUR artifacts.
693 lines
24 KiB
Python
Executable File
693 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Build and package the native NyaTerm application for one Rust target."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import plistlib
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import textwrap
|
|
import tomllib
|
|
import zipfile
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parents[2]
|
|
DIST_DIR = ROOT_DIR / "dist"
|
|
WORK_DIR = ROOT_DIR / "target" / "native-package"
|
|
RESOURCE_DIR = ROOT_DIR / "crates" / "nyaterm-app" / "resources"
|
|
ICON_DIR = RESOURCE_DIR / "icons"
|
|
LICENSE_PATH = ROOT_DIR / "LICENSE"
|
|
APP_NAME = "NyaTerm"
|
|
APP_BIN = "nyaterm"
|
|
# Helper processes the application spawns at runtime. `resolve_helper_path()`
|
|
# in nyaterm-remote-desktop only looks beside the running executable, so each
|
|
# of these must be packaged next to the application binary in every format.
|
|
HELPER_BINS = ("nyaterm-rdp-helper", "nyaterm-vnc-helper", "nyaterm-mcp")
|
|
MACOS_IDENTIFIER = "com.kang.nyaterm"
|
|
LINUX_PACKAGE = "nyaterm"
|
|
URL_SCHEME = "nyaterm"
|
|
PORTABLE_MARKER = "nyaterm-portable"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TargetInfo:
|
|
target: str
|
|
os_name: str
|
|
arch: str
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
return f"{self.os_name}_{self.arch}"
|
|
|
|
|
|
TARGETS = {
|
|
"aarch64-apple-darwin": TargetInfo("aarch64-apple-darwin", "macos", "arm64"),
|
|
"x86_64-apple-darwin": TargetInfo("x86_64-apple-darwin", "macos", "x64"),
|
|
"aarch64-unknown-linux-gnu": TargetInfo(
|
|
"aarch64-unknown-linux-gnu", "linux", "arm64"
|
|
),
|
|
"x86_64-unknown-linux-gnu": TargetInfo(
|
|
"x86_64-unknown-linux-gnu", "linux", "x64"
|
|
),
|
|
"aarch64-pc-windows-msvc": TargetInfo(
|
|
"aarch64-pc-windows-msvc", "windows", "arm64"
|
|
),
|
|
"x86_64-pc-windows-msvc": TargetInfo(
|
|
"x86_64-pc-windows-msvc", "windows", "x64"
|
|
),
|
|
}
|
|
|
|
|
|
def run(
|
|
args: list[str],
|
|
*,
|
|
cwd: Path = ROOT_DIR,
|
|
env: dict[str, str] | None = None,
|
|
) -> None:
|
|
print("+", " ".join(args), flush=True)
|
|
subprocess.run(args, cwd=cwd, env=env, check=True)
|
|
|
|
|
|
def require_tool(name: str) -> str:
|
|
path = shutil.which(name)
|
|
if not path:
|
|
raise RuntimeError(f"required packaging tool not found: {name}")
|
|
return path
|
|
|
|
|
|
def workspace_version() -> str:
|
|
with (ROOT_DIR / "Cargo.toml").open("rb") as handle:
|
|
return tomllib.load(handle)["workspace"]["package"]["version"]
|
|
|
|
|
|
def normalize_version(raw: str) -> str:
|
|
return raw.strip().removeprefix("v").removeprefix("V")
|
|
|
|
|
|
def validate_version(raw: str, expected: str | None = None) -> str:
|
|
version = normalize_version(raw)
|
|
if not re.fullmatch(
|
|
r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?",
|
|
version,
|
|
):
|
|
raise ValueError(f"invalid release version: {raw}")
|
|
if expected is not None and version != expected:
|
|
raise ValueError(
|
|
f"release version {version} does not match Cargo workspace version {expected}"
|
|
)
|
|
return version
|
|
|
|
|
|
def validate_artifact_version(raw: str) -> str:
|
|
"""Validate a public filename label without changing package metadata."""
|
|
value = raw.strip()
|
|
if value == "main-snapshot":
|
|
return value
|
|
return validate_version(value)
|
|
|
|
|
|
def target_info(target: str) -> TargetInfo:
|
|
try:
|
|
return TARGETS[target]
|
|
except KeyError as error:
|
|
raise ValueError(f"unsupported release target: {target}") from error
|
|
|
|
|
|
def artifact_names(target: str, version: str) -> set[str]:
|
|
info = target_info(target)
|
|
prefix = f"{APP_NAME}_{validate_artifact_version(version)}_{info.label}"
|
|
if info.os_name == "macos":
|
|
return {f"{prefix}.dmg", f"{prefix}.app.tar.gz"}
|
|
if info.os_name == "linux":
|
|
return {f"{prefix}.AppImage", f"{prefix}.deb", f"{prefix}.rpm"}
|
|
return {f"{prefix}_portable.zip", f"{prefix}-setup.exe"}
|
|
|
|
|
|
def cargo_target_dir() -> Path:
|
|
configured = os.environ.get("CARGO_TARGET_DIR")
|
|
if not configured:
|
|
return ROOT_DIR / "target"
|
|
path = Path(configured)
|
|
return path if path.is_absolute() else ROOT_DIR / path
|
|
|
|
|
|
def release_binary_path(target: str, name: str = APP_BIN) -> Path:
|
|
suffix = ".exe" if "windows" in target else ""
|
|
return cargo_target_dir() / target / "release" / f"{name}{suffix}"
|
|
|
|
|
|
def helper_binary_paths(target: str) -> list[Path]:
|
|
return [release_binary_path(target, name) for name in HELPER_BINS]
|
|
|
|
|
|
def build_binary(package: str, name: str, target: str) -> Path:
|
|
run(
|
|
[
|
|
"cargo",
|
|
"build",
|
|
"-p",
|
|
package,
|
|
"--bin",
|
|
name,
|
|
"--release",
|
|
"--target",
|
|
target,
|
|
"--locked",
|
|
]
|
|
)
|
|
binary = release_binary_path(target, name)
|
|
if not binary.is_file():
|
|
raise FileNotFoundError(f"release executable not found: {binary}")
|
|
return binary
|
|
|
|
|
|
def build_application(target: str) -> Path:
|
|
binary = build_binary("nyaterm-app", APP_BIN, target)
|
|
for name in HELPER_BINS:
|
|
build_binary(name, name, target)
|
|
return binary
|
|
|
|
|
|
def make_executable(path: Path) -> None:
|
|
path.chmod(path.stat().st_mode | 0o111)
|
|
|
|
|
|
def copy_helpers(destination: Path, target: str) -> list[Path]:
|
|
"""Copy helper binaries beside the application and return the copies."""
|
|
copied = []
|
|
for source in helper_binary_paths(target):
|
|
binary = destination / source.name
|
|
shutil.copy2(source, binary)
|
|
if "windows" not in target:
|
|
make_executable(binary)
|
|
copied.append(binary)
|
|
return copied
|
|
|
|
|
|
def copy_release_documents(destination: Path, version: str) -> None:
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(LICENSE_PATH, destination / "LICENSE")
|
|
(destination / "VERSION").write_text(f"{version}\n", encoding="utf-8")
|
|
|
|
|
|
def reset_output() -> None:
|
|
if DIST_DIR.exists():
|
|
shutil.rmtree(DIST_DIR)
|
|
DIST_DIR.mkdir()
|
|
if WORK_DIR.exists():
|
|
shutil.rmtree(WORK_DIR)
|
|
WORK_DIR.mkdir(parents=True)
|
|
|
|
|
|
def archive_zip(source: Path, destination: Path) -> None:
|
|
with zipfile.ZipFile(
|
|
destination, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
|
|
) as archive:
|
|
for path in sorted(source.rglob("*")):
|
|
archive.write(path, path.relative_to(source.parent))
|
|
|
|
|
|
def windows_numeric_version(version: str) -> str:
|
|
components = version.split("-", 1)[0].split(".")
|
|
if len(components) != 3 or any(not part.isdigit() for part in components):
|
|
raise ValueError(f"Windows package version is not SemVer: {version}")
|
|
numeric = [int(part) for part in components] + [0]
|
|
if any(part > 65535 for part in numeric):
|
|
raise ValueError(f"Windows version component exceeds 65535: {version}")
|
|
return ".".join(str(part) for part in numeric)
|
|
|
|
|
|
def nsis_path(path: Path) -> str:
|
|
return str(path.resolve()).replace("/", "\\")
|
|
|
|
|
|
def find_makensis() -> str:
|
|
found = shutil.which("makensis") or shutil.which("makensis.exe")
|
|
if found:
|
|
return found
|
|
raise RuntimeError("makensis not found; install NSIS before packaging Windows")
|
|
|
|
|
|
def create_windows_packages(
|
|
binary: Path, info: TargetInfo, version: str, artifact_version: str
|
|
) -> None:
|
|
portable_root = WORK_DIR / "NyaTerm-portable"
|
|
portable_root.mkdir()
|
|
shutil.copy2(binary, portable_root / "NyaTerm.exe")
|
|
copy_helpers(portable_root, info.target)
|
|
(portable_root / PORTABLE_MARKER).touch()
|
|
(portable_root / "data").mkdir()
|
|
(portable_root / "data" / ".keep").touch()
|
|
copy_release_documents(portable_root, version)
|
|
portable_output = (
|
|
DIST_DIR / f"{APP_NAME}_{artifact_version}_{info.label}_portable.zip"
|
|
)
|
|
archive_zip(portable_root, portable_output)
|
|
|
|
installer_root = WORK_DIR / "windows-installer"
|
|
installer_root.mkdir()
|
|
shutil.copy2(binary, installer_root / "NyaTerm.exe")
|
|
installer_helpers = copy_helpers(installer_root, info.target)
|
|
copy_release_documents(installer_root, version)
|
|
shutil.copy2(ICON_DIR / "icon.ico", installer_root / "icon.ico")
|
|
|
|
nsis_indent = "\n" + " " * 14
|
|
helper_install = nsis_indent.join(
|
|
f'File "{nsis_path(path)}"' for path in installer_helpers
|
|
)
|
|
helper_uninstall = nsis_indent.join(
|
|
f'Delete "$INSTDIR\\{path.name}"' for path in installer_helpers
|
|
)
|
|
|
|
output = DIST_DIR / f"{APP_NAME}_{artifact_version}_{info.label}-setup.exe"
|
|
script = WORK_DIR / "nyaterm-installer.nsi"
|
|
script.write_text(
|
|
textwrap.dedent(
|
|
rf"""
|
|
Unicode true
|
|
RequestExecutionLevel user
|
|
SetCompressor /SOLID lzma
|
|
|
|
!include "MUI2.nsh"
|
|
!define MUI_ICON "{nsis_path(ICON_DIR / 'icon.ico')}"
|
|
!define MUI_UNICON "{nsis_path(ICON_DIR / 'icon.ico')}"
|
|
|
|
Name "NyaTerm"
|
|
OutFile "{nsis_path(output)}"
|
|
InstallDir "$LOCALAPPDATA\Programs\NyaTerm"
|
|
InstallDirRegKey HKCU "Software\NyaTerm" "InstallDir"
|
|
VIProductVersion "{windows_numeric_version(version)}"
|
|
VIAddVersionKey "ProductName" "NyaTerm"
|
|
VIAddVersionKey "ProductVersion" "{version}"
|
|
VIAddVersionKey "FileDescription" "NyaTerm native GPUI terminal"
|
|
VIAddVersionKey "LegalCopyright" "Copyright Kang"
|
|
|
|
!insertmacro MUI_PAGE_WELCOME
|
|
!insertmacro MUI_PAGE_DIRECTORY
|
|
!insertmacro MUI_PAGE_INSTFILES
|
|
!insertmacro MUI_PAGE_FINISH
|
|
!insertmacro MUI_UNPAGE_CONFIRM
|
|
!insertmacro MUI_UNPAGE_INSTFILES
|
|
!insertmacro MUI_LANGUAGE "English"
|
|
|
|
Section "NyaTerm" SecMain
|
|
SetOutPath "$INSTDIR"
|
|
File "{nsis_path(installer_root / 'NyaTerm.exe')}"
|
|
{helper_install}
|
|
File "{nsis_path(installer_root / 'LICENSE')}"
|
|
File "{nsis_path(installer_root / 'VERSION')}"
|
|
File "{nsis_path(installer_root / 'icon.ico')}"
|
|
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
|
WriteRegStr HKCU "Software\NyaTerm" "InstallDir" "$INSTDIR"
|
|
WriteRegStr HKCU "Software\Classes\{URL_SCHEME}" "" "URL:NyaTerm Protocol"
|
|
WriteRegStr HKCU "Software\Classes\{URL_SCHEME}" "URL Protocol" ""
|
|
WriteRegStr HKCU "Software\Classes\{URL_SCHEME}\DefaultIcon" "" "$INSTDIR\NyaTerm.exe,0"
|
|
WriteRegStr HKCU "Software\Classes\{URL_SCHEME}\shell\open\command" "" "$\"$INSTDIR\NyaTerm.exe$\" $\"%1$\""
|
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\NyaTerm" "DisplayName" "NyaTerm"
|
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\NyaTerm" "DisplayVersion" "{version}"
|
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\NyaTerm" "DisplayIcon" "$INSTDIR\NyaTerm.exe"
|
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\NyaTerm" "UninstallString" "$\"$INSTDIR\Uninstall.exe$\""
|
|
CreateDirectory "$SMPROGRAMS\NyaTerm"
|
|
CreateShortcut "$SMPROGRAMS\NyaTerm\NyaTerm.lnk" "$INSTDIR\NyaTerm.exe" "" "$INSTDIR\icon.ico"
|
|
CreateShortcut "$DESKTOP\NyaTerm.lnk" "$INSTDIR\NyaTerm.exe" "" "$INSTDIR\icon.ico"
|
|
SectionEnd
|
|
|
|
Section "Uninstall"
|
|
Delete "$DESKTOP\NyaTerm.lnk"
|
|
Delete "$SMPROGRAMS\NyaTerm\NyaTerm.lnk"
|
|
RMDir "$SMPROGRAMS\NyaTerm"
|
|
Delete "$INSTDIR\NyaTerm.exe"
|
|
{helper_uninstall}
|
|
Delete "$INSTDIR\LICENSE"
|
|
Delete "$INSTDIR\VERSION"
|
|
Delete "$INSTDIR\icon.ico"
|
|
Delete "$INSTDIR\Uninstall.exe"
|
|
RMDir "$INSTDIR"
|
|
DeleteRegKey HKCU "Software\Classes\{URL_SCHEME}"
|
|
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\NyaTerm"
|
|
DeleteRegKey HKCU "Software\NyaTerm"
|
|
SectionEnd
|
|
"""
|
|
).lstrip(),
|
|
encoding="utf-8",
|
|
)
|
|
run([find_makensis(), str(script)])
|
|
|
|
|
|
def create_macos_packages(
|
|
binary: Path, info: TargetInfo, version: str, artifact_version: str
|
|
) -> None:
|
|
bundle = WORK_DIR / "NyaTerm.app"
|
|
macos_dir = bundle / "Contents" / "MacOS"
|
|
resources_dir = bundle / "Contents" / "Resources"
|
|
macos_dir.mkdir(parents=True)
|
|
resources_dir.mkdir(parents=True)
|
|
app_binary = macos_dir / "NyaTerm"
|
|
shutil.copy2(binary, app_binary)
|
|
make_executable(app_binary)
|
|
helper_binaries = copy_helpers(macos_dir, info.target)
|
|
shutil.copy2(ICON_DIR / "icon.icns", resources_dir / "icon.icns")
|
|
copy_release_documents(resources_dir, version)
|
|
|
|
plist = {
|
|
"CFBundleDevelopmentRegion": "en",
|
|
"CFBundleDisplayName": APP_NAME,
|
|
"CFBundleExecutable": "NyaTerm",
|
|
"CFBundleIconFile": "icon.icns",
|
|
"CFBundleIdentifier": MACOS_IDENTIFIER,
|
|
"CFBundleInfoDictionaryVersion": "6.0",
|
|
"CFBundleName": APP_NAME,
|
|
"CFBundlePackageType": "APPL",
|
|
"CFBundleShortVersionString": version,
|
|
"CFBundleURLTypes": [
|
|
{
|
|
"CFBundleTypeRole": "Viewer",
|
|
"CFBundleURLName": MACOS_IDENTIFIER,
|
|
"CFBundleURLSchemes": [URL_SCHEME],
|
|
}
|
|
],
|
|
"CFBundleVersion": version,
|
|
"LSMinimumSystemVersion": "12.0",
|
|
"NSHighResolutionCapable": True,
|
|
"NSHumanReadableCopyright": "Copyright Kang",
|
|
}
|
|
with (bundle / "Contents" / "Info.plist").open("wb") as handle:
|
|
plistlib.dump(plist, handle, sort_keys=True)
|
|
|
|
codesign = require_tool("codesign")
|
|
# Sign inside out. A bundle-only signature seals extra Mach-O files in
|
|
# Contents/MacOS as resources instead of signing them as code, which makes
|
|
# the helper fail to launch under Gatekeeper.
|
|
for helper in helper_binaries:
|
|
run([codesign, "--force", "--sign", "-", "--timestamp=none", str(helper)])
|
|
run([codesign, "--force", "--deep", "--sign", "-", "--timestamp=none", str(bundle)])
|
|
|
|
tar_output = DIST_DIR / f"{APP_NAME}_{artifact_version}_{info.label}.app.tar.gz"
|
|
with tarfile.open(tar_output, "w:gz", compresslevel=9) as archive:
|
|
archive.add(bundle, arcname="NyaTerm.app")
|
|
|
|
dmg_root = WORK_DIR / "dmg"
|
|
dmg_root.mkdir()
|
|
shutil.copytree(bundle, dmg_root / "NyaTerm.app", symlinks=True)
|
|
(dmg_root / "Applications").symlink_to("/Applications")
|
|
dmg_output = DIST_DIR / f"{APP_NAME}_{artifact_version}_{info.label}.dmg"
|
|
run(
|
|
[
|
|
require_tool("hdiutil"),
|
|
"create",
|
|
"-volname",
|
|
APP_NAME,
|
|
"-srcfolder",
|
|
str(dmg_root),
|
|
"-ov",
|
|
"-format",
|
|
"UDZO",
|
|
str(dmg_output),
|
|
]
|
|
)
|
|
|
|
|
|
def linux_deb_arch(target: str) -> str:
|
|
return {
|
|
"x86_64-unknown-linux-gnu": "amd64",
|
|
"aarch64-unknown-linux-gnu": "arm64",
|
|
}[target]
|
|
|
|
|
|
def linux_rpm_arch(target: str) -> str:
|
|
return {
|
|
"x86_64-unknown-linux-gnu": "x86_64",
|
|
"aarch64-unknown-linux-gnu": "aarch64",
|
|
}[target]
|
|
|
|
|
|
def linux_appimage_arch(target: str) -> str:
|
|
return {
|
|
"x86_64-unknown-linux-gnu": "x86_64",
|
|
"aarch64-unknown-linux-gnu": "aarch64",
|
|
}[target]
|
|
|
|
|
|
def linux_rpm_version(version: str) -> tuple[str, str]:
|
|
if "-" not in version:
|
|
return version, "1"
|
|
upstream, prerelease = version.split("-", 1)
|
|
normalized = re.sub(r"[^0-9A-Za-z]+", ".", prerelease).strip(".")
|
|
return upstream, f"0.{normalized or 'preview'}"
|
|
|
|
|
|
def write_desktop_file(path: Path, executable: str) -> None:
|
|
path.write_text(
|
|
textwrap.dedent(
|
|
f"""
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=NyaTerm
|
|
Comment=Native GPUI terminal and SSH client
|
|
Exec={executable} %U
|
|
Icon=nyaterm
|
|
StartupWMClass=nyaterm
|
|
Terminal=false
|
|
Categories=Development;TerminalEmulator;Network;
|
|
MimeType=x-scheme-handler/{URL_SCHEME};
|
|
StartupNotify=true
|
|
"""
|
|
).lstrip(),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def copy_linux_icons(root: Path) -> None:
|
|
icons = {
|
|
"32x32": "32x32.png",
|
|
"64x64": "64x64.png",
|
|
"128x128": "128x128.png",
|
|
"256x256": "256x256.png",
|
|
}
|
|
for size, source in icons.items():
|
|
destination = root / "usr" / "share" / "icons" / "hicolor" / size / "apps"
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(ICON_DIR / source, destination / "nyaterm.png")
|
|
|
|
|
|
def parse_dpkg_dependencies(output: str) -> str:
|
|
prefix = "shlibs:Depends="
|
|
for line in output.splitlines():
|
|
if line.startswith(prefix) and line[len(prefix) :].strip():
|
|
return line[len(prefix) :].strip()
|
|
raise RuntimeError("dpkg-shlibdeps did not report shlibs:Depends")
|
|
|
|
|
|
def linux_deb_dependencies(binaries: list[Path]) -> str:
|
|
scratch = WORK_DIR / "shlibdeps"
|
|
debian = scratch / "debian"
|
|
debian.mkdir(parents=True)
|
|
(debian / "control").write_text(
|
|
"Source: nyaterm\nPackage: nyaterm\nArchitecture: any\nDescription: NyaTerm\n",
|
|
encoding="utf-8",
|
|
)
|
|
# Helpers can pull shared libraries the application itself does not need.
|
|
command = [require_tool("dpkg-shlibdeps"), "-O"]
|
|
for binary in binaries:
|
|
command += ["-e", str(binary)]
|
|
output = subprocess.check_output(command, cwd=scratch, text=True)
|
|
return parse_dpkg_dependencies(output)
|
|
|
|
|
|
def create_linux_appimage(
|
|
binary: Path, info: TargetInfo, version: str, artifact_version: str
|
|
) -> None:
|
|
appdir = WORK_DIR / "NyaTerm.AppDir"
|
|
usr_bin = appdir / "usr" / "bin"
|
|
usr_bin.mkdir(parents=True)
|
|
app_binary = usr_bin / APP_BIN
|
|
shutil.copy2(binary, app_binary)
|
|
make_executable(app_binary)
|
|
copy_helpers(usr_bin, info.target)
|
|
copy_release_documents(appdir / "usr" / "share" / "doc" / LINUX_PACKAGE, version)
|
|
|
|
applications = appdir / "usr" / "share" / "applications"
|
|
applications.mkdir(parents=True)
|
|
write_desktop_file(applications / "nyaterm.desktop", APP_BIN)
|
|
shutil.copy2(applications / "nyaterm.desktop", appdir / "nyaterm.desktop")
|
|
shutil.copy2(ICON_DIR / "128x128.png", appdir / "nyaterm.png")
|
|
copy_linux_icons(appdir)
|
|
|
|
apprun = appdir / "AppRun"
|
|
apprun.write_text(
|
|
'#!/bin/sh\nAPPDIR="${APPDIR:-$(dirname "$(readlink -f "$0")")}"\n'
|
|
'exec "$APPDIR/usr/bin/nyaterm" "$@"\n',
|
|
encoding="utf-8",
|
|
)
|
|
make_executable(apprun)
|
|
|
|
output = DIST_DIR / f"{APP_NAME}_{artifact_version}_{info.label}.AppImage"
|
|
env = os.environ.copy()
|
|
env["ARCH"] = linux_appimage_arch(info.target)
|
|
env.setdefault("APPIMAGE_EXTRACT_AND_RUN", "1")
|
|
run([require_tool("appimagetool"), str(appdir), str(output)], env=env)
|
|
|
|
|
|
def create_linux_deb(
|
|
binary: Path, info: TargetInfo, version: str, artifact_version: str
|
|
) -> None:
|
|
root = WORK_DIR / "deb"
|
|
app_root = root / "opt" / LINUX_PACKAGE
|
|
app_root.mkdir(parents=True)
|
|
app_binary = app_root / APP_BIN
|
|
shutil.copy2(binary, app_binary)
|
|
make_executable(app_binary)
|
|
helper_binaries = copy_helpers(app_root, info.target)
|
|
copy_release_documents(app_root, version)
|
|
copy_release_documents(root / "usr" / "share" / "doc" / LINUX_PACKAGE, version)
|
|
|
|
applications = root / "usr" / "share" / "applications"
|
|
applications.mkdir(parents=True)
|
|
write_desktop_file(applications / "nyaterm.desktop", "/opt/nyaterm/nyaterm")
|
|
copy_linux_icons(root)
|
|
|
|
control_dir = root / "DEBIAN"
|
|
control_dir.mkdir()
|
|
dependencies = linux_deb_dependencies([app_binary, *helper_binaries])
|
|
(control_dir / "control").write_text(
|
|
textwrap.dedent(
|
|
f"""
|
|
Package: nyaterm
|
|
Version: {version.replace('-', '~')}
|
|
Section: utils
|
|
Priority: optional
|
|
Architecture: {linux_deb_arch(info.target)}
|
|
Maintainer: Kang <noreply@nyaterm.app>
|
|
Depends: {dependencies}
|
|
Recommends: libvulkan1 | mesa-vulkan-drivers
|
|
Description: NyaTerm native GPUI terminal and SSH client
|
|
Native terminal workspace with SSH, SFTP and remote operations.
|
|
"""
|
|
).lstrip(),
|
|
encoding="utf-8",
|
|
)
|
|
output = DIST_DIR / f"{APP_NAME}_{artifact_version}_{info.label}.deb"
|
|
run([require_tool("dpkg-deb"), "--build", "--root-owner-group", str(root), str(output)])
|
|
|
|
|
|
def create_linux_rpm(
|
|
binary: Path, info: TargetInfo, version: str, artifact_version: str
|
|
) -> None:
|
|
rpm_root = WORK_DIR / "rpm"
|
|
top_dir = rpm_root / "rpmbuild"
|
|
payload = rpm_root / "payload"
|
|
for directory in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"):
|
|
(top_dir / directory).mkdir(parents=True)
|
|
|
|
app_root = payload / "opt" / LINUX_PACKAGE
|
|
app_root.mkdir(parents=True)
|
|
app_binary = app_root / APP_BIN
|
|
shutil.copy2(binary, app_binary)
|
|
make_executable(app_binary)
|
|
copy_helpers(app_root, info.target)
|
|
copy_release_documents(app_root, version)
|
|
copy_release_documents(payload / "usr" / "share" / "doc" / LINUX_PACKAGE, version)
|
|
applications = payload / "usr" / "share" / "applications"
|
|
applications.mkdir(parents=True)
|
|
write_desktop_file(applications / "nyaterm.desktop", "/opt/nyaterm/nyaterm")
|
|
copy_linux_icons(payload)
|
|
|
|
rpm_version, rpm_release = linux_rpm_version(version)
|
|
payload_path = str(payload.resolve()).replace("%", "%%")
|
|
spec = textwrap.dedent(
|
|
f"""
|
|
Name: nyaterm
|
|
Version: {rpm_version}
|
|
Release: {rpm_release}
|
|
Summary: NyaTerm native GPUI terminal and SSH client
|
|
License: Apache-2.0
|
|
URL: https://nyaterm.app
|
|
BuildArch: {linux_rpm_arch(info.target)}
|
|
|
|
%description
|
|
Native terminal workspace with SSH, SFTP and remote operations.
|
|
|
|
%install
|
|
rm -rf %{{buildroot}}
|
|
mkdir -p %{{buildroot}}
|
|
cp -a "{payload_path}/." %{{buildroot}}/
|
|
|
|
%files
|
|
/opt/nyaterm
|
|
/usr/share/applications/nyaterm.desktop
|
|
/usr/share/icons/hicolor/*/apps/nyaterm.png
|
|
/usr/share/doc/nyaterm
|
|
"""
|
|
).lstrip()
|
|
spec_path = top_dir / "SPECS" / "nyaterm.spec"
|
|
spec_path.write_text(spec, encoding="utf-8")
|
|
run(
|
|
[
|
|
require_tool("rpmbuild"),
|
|
"--define",
|
|
f"_topdir {top_dir.resolve()}",
|
|
"--define",
|
|
"_build_id_links none",
|
|
"--define",
|
|
"debug_package %{nil}",
|
|
"-bb",
|
|
str(spec_path),
|
|
]
|
|
)
|
|
built = list((top_dir / "RPMS" / linux_rpm_arch(info.target)).glob("*.rpm"))
|
|
if len(built) != 1:
|
|
raise RuntimeError(f"expected one RPM artifact, found {len(built)}")
|
|
shutil.copy2(
|
|
built[0], DIST_DIR / f"{APP_NAME}_{artifact_version}_{info.label}.rpm"
|
|
)
|
|
|
|
|
|
def create_linux_packages(
|
|
binary: Path, info: TargetInfo, version: str, artifact_version: str
|
|
) -> None:
|
|
create_linux_appimage(binary, info, version, artifact_version)
|
|
create_linux_deb(binary, info, version, artifact_version)
|
|
create_linux_rpm(binary, info, version, artifact_version)
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) != 2:
|
|
raise SystemExit("usage: package_native.py <rust-target>")
|
|
info = target_info(sys.argv[1])
|
|
expected_version = workspace_version()
|
|
version = validate_version(
|
|
os.environ.get("NYATERM_VERSION", expected_version), expected_version
|
|
)
|
|
artifact_version = validate_artifact_version(
|
|
os.environ.get("NYATERM_ARTIFACT_VERSION", version)
|
|
)
|
|
reset_output()
|
|
print(f"==> Packaging {APP_NAME} {version} for {info.target}", flush=True)
|
|
binary = build_application(info.target)
|
|
|
|
if info.os_name == "macos":
|
|
create_macos_packages(binary, info, version, artifact_version)
|
|
elif info.os_name == "linux":
|
|
create_linux_packages(binary, info, version, artifact_version)
|
|
else:
|
|
create_windows_packages(binary, info, version, artifact_version)
|
|
|
|
actual = {path.name for path in DIST_DIR.iterdir() if path.is_file()}
|
|
expected = artifact_names(info.target, artifact_version)
|
|
if actual != expected:
|
|
raise RuntimeError(
|
|
f"package output mismatch: expected {sorted(expected)}, got {sorted(actual)}"
|
|
)
|
|
for path in sorted(DIST_DIR.iterdir()):
|
|
print(f"==> {path.name} ({path.stat().st_size} bytes)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|