279 lines
8.4 KiB
RPMSpec
279 lines
8.4 KiB
RPMSpec
|
|
# -*- mode: python ; coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
PyInstaller spec file for PineAgents (Tauri sidecar).
|
||
|
|
|
||
|
|
Shared spec for both macOS and Windows. Builds an onedir backend bundle so the
|
||
|
|
desktop startup can load Python directly without onefile extraction. The same
|
||
|
|
bundle also includes a pineagents CLI executable for the Windows installer PATH
|
||
|
|
option.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from PyInstaller.utils.hooks import (
|
||
|
|
collect_data_files,
|
||
|
|
collect_submodules,
|
||
|
|
copy_metadata,
|
||
|
|
get_package_paths,
|
||
|
|
)
|
||
|
|
|
||
|
|
REPO_ROOT = Path(SPECPATH).parent.parent
|
||
|
|
|
||
|
|
SRC = REPO_ROOT / "src" / "pineagents"
|
||
|
|
if sys.platform == "darwin":
|
||
|
|
codesign_identity = os.environ.get(
|
||
|
|
"PYINSTALLER_CODESIGN_IDENTITY"
|
||
|
|
) or os.environ.get("APPLE_SIGNING_IDENTITY")
|
||
|
|
if not codesign_identity:
|
||
|
|
codesign_identity = None
|
||
|
|
else:
|
||
|
|
codesign_identity = None
|
||
|
|
|
||
|
|
def collect_tree(source_dir, target_dir):
|
||
|
|
return [
|
||
|
|
(str(path), str(Path(target_dir) / path.relative_to(source_dir).parent))
|
||
|
|
for path in source_dir.rglob("*")
|
||
|
|
if path.is_file()
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
# Match the legacy desktop package: the FastAPI backend serves the web console
|
||
|
|
# from pineagents/console, so Tauri can navigate to the backend-hosted same-origin
|
||
|
|
# console after the sidecar is ready.
|
||
|
|
CONSOLE_DIST = REPO_ROOT / "console" / "dist"
|
||
|
|
if not (CONSOLE_DIST / "index.html").is_file():
|
||
|
|
raise SystemExit(
|
||
|
|
f"console dist not found at {CONSOLE_DIST}; "
|
||
|
|
"run npm run build:prod in console/ before PyInstaller"
|
||
|
|
)
|
||
|
|
|
||
|
|
_data_dirs = [
|
||
|
|
("agents/skills", "pineagents/agents/skills"),
|
||
|
|
("agents/md_files", "pineagents/agents/md_files"),
|
||
|
|
("tokenizer", "pineagents/tokenizer"),
|
||
|
|
("security/tool_guard/rules", "pineagents/security/tool_guard/rules"),
|
||
|
|
("security/skill_scanner/rules", "pineagents/security/skill_scanner/rules"),
|
||
|
|
("security/skill_scanner/data", "pineagents/security/skill_scanner/data"),
|
||
|
|
("app/channels/yuanbao/proto", "pineagents/app/channels/yuanbao/proto"),
|
||
|
|
]
|
||
|
|
datas = [
|
||
|
|
(str(SRC / src), dst) for src, dst in _data_dirs if (SRC / src).is_dir()
|
||
|
|
]
|
||
|
|
datas += collect_tree(CONSOLE_DIST, "pineagents/console")
|
||
|
|
datas.append(
|
||
|
|
(
|
||
|
|
str(SRC / "browser/control_link/injected/engine.js"),
|
||
|
|
"pineagents/browser/control_link/injected",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
# Include reme package data files (configs, tool yamls, etc.)
|
||
|
|
datas += collect_data_files("reme")
|
||
|
|
datas += collect_data_files("whisper")
|
||
|
|
datas += collect_data_files("agentscope")
|
||
|
|
datas += collect_data_files(
|
||
|
|
"agentscope.tool._builtin._scripts",
|
||
|
|
include_py_files=True,
|
||
|
|
)
|
||
|
|
datas += collect_data_files(
|
||
|
|
"agentscope.workspace._mcp_gateway",
|
||
|
|
include_py_files=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
# The Qoder SDK ships a platform-specific qodercli executable. Classify it as
|
||
|
|
# a binary so PyInstaller preserves executable permissions and signs it with
|
||
|
|
# the rest of the macOS bundle.
|
||
|
|
_, _qoder_sdk_dir = get_package_paths("qoder_agent_sdk")
|
||
|
|
_qoder_cli_name = "qodercli.exe" if sys.platform == "win32" else "qodercli"
|
||
|
|
_qoder_cli = Path(_qoder_sdk_dir) / "_bundled" / _qoder_cli_name
|
||
|
|
if not _qoder_cli.is_file():
|
||
|
|
raise SystemExit(
|
||
|
|
f"Qoder SDK CLI not found at {_qoder_cli}; reinstall qoder-agent-sdk"
|
||
|
|
)
|
||
|
|
qoder_binaries = [
|
||
|
|
(str(_qoder_cli), "qoder_agent_sdk/_bundled"),
|
||
|
|
]
|
||
|
|
|
||
|
|
# The official Codex Python SDK depends on a platform wheel that exposes a
|
||
|
|
# stable bundled_codex_path() API. Preserve its runtime layout because Codex
|
||
|
|
# resolves sibling hosts and resources relative to the main executable.
|
||
|
|
_, _codex_bin_dir = get_package_paths("codex_cli_bin")
|
||
|
|
_codex_bin_dir = Path(_codex_bin_dir)
|
||
|
|
_codex_executable = (
|
||
|
|
"codex.exe" if sys.platform == "win32" else "codex"
|
||
|
|
)
|
||
|
|
_codex_cli = _codex_bin_dir / "bin" / _codex_executable
|
||
|
|
if not _codex_cli.is_file():
|
||
|
|
raise SystemExit(
|
||
|
|
f"Codex SDK CLI not found at {_codex_cli}; reinstall openai-codex"
|
||
|
|
)
|
||
|
|
codex_binaries = [
|
||
|
|
(
|
||
|
|
str(path),
|
||
|
|
str(Path("codex_cli_bin") / path.relative_to(_codex_bin_dir).parent),
|
||
|
|
)
|
||
|
|
for directory_name in ("bin", "codex-path", "codex-resources")
|
||
|
|
for path in (_codex_bin_dir / directory_name).rglob("*")
|
||
|
|
if path.is_file()
|
||
|
|
]
|
||
|
|
datas.append(
|
||
|
|
(
|
||
|
|
str(_codex_bin_dir / "codex-package.json"),
|
||
|
|
"codex_cli_bin",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
# Collect package metadata for packages that use importlib.metadata at runtime.
|
||
|
|
# Keep this allowlist in sync when adding runtime dependencies that query
|
||
|
|
# importlib.metadata, otherwise packaged sidecars may fail only after install.
|
||
|
|
_metadata_pkgs = [
|
||
|
|
"pineagents",
|
||
|
|
"fastmcp",
|
||
|
|
"mcp",
|
||
|
|
"httpx",
|
||
|
|
"httpcore",
|
||
|
|
"anyio",
|
||
|
|
"sniffio",
|
||
|
|
"starlette",
|
||
|
|
"pydantic",
|
||
|
|
"pydantic-core",
|
||
|
|
"pydantic-settings",
|
||
|
|
"uvicorn",
|
||
|
|
"openai",
|
||
|
|
"anthropic",
|
||
|
|
"tiktoken",
|
||
|
|
"agentscope",
|
||
|
|
"agentscope-runtime",
|
||
|
|
"huggingface_hub",
|
||
|
|
"modelscope",
|
||
|
|
"openai-whisper",
|
||
|
|
"openai-codex",
|
||
|
|
"openai-codex-cli-bin",
|
||
|
|
"qoder-agent-sdk",
|
||
|
|
]
|
||
|
|
for _pkg in _metadata_pkgs:
|
||
|
|
try:
|
||
|
|
datas += copy_metadata(_pkg)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
a = Analysis(
|
||
|
|
[
|
||
|
|
str(SRC / "tauri" / "entry.py"),
|
||
|
|
str(SRC / "tauri" / "cli_entry.py"),
|
||
|
|
],
|
||
|
|
pathex=[str(REPO_ROOT), str(REPO_ROOT / "src")],
|
||
|
|
binaries=[*qoder_binaries, *codex_binaries],
|
||
|
|
datas=datas,
|
||
|
|
hiddenimports=[
|
||
|
|
"codex_cli_bin",
|
||
|
|
# uvicorn internals (not auto-discovered by PyInstaller)
|
||
|
|
"uvicorn.logging",
|
||
|
|
"uvicorn.loops",
|
||
|
|
"uvicorn.loops.auto",
|
||
|
|
"uvicorn.protocols",
|
||
|
|
"uvicorn.protocols.http",
|
||
|
|
"uvicorn.protocols.http.auto",
|
||
|
|
"uvicorn.protocols.websockets",
|
||
|
|
"uvicorn.protocols.websockets.auto",
|
||
|
|
"uvicorn.lifespan",
|
||
|
|
"uvicorn.lifespan.on",
|
||
|
|
# All CLI sub-commands (dynamically loaded by Click)
|
||
|
|
*collect_submodules("pineagents.cli"),
|
||
|
|
# All channel adapters (imported on-demand at runtime)
|
||
|
|
*collect_submodules("pineagents.app.channels"),
|
||
|
|
# ACP runner support is lazily imported by delegate_external_agent.
|
||
|
|
*collect_submodules("pineagents.agents.acp"),
|
||
|
|
# PawApp SDK modules are imported by installed app plugins at runtime.
|
||
|
|
*collect_submodules("pineagents.pawapp"),
|
||
|
|
# ASGI app entry points
|
||
|
|
"pineagents.app._app",
|
||
|
|
"pineagents.app.multi_agent_manager",
|
||
|
|
"pineagents.app.chats",
|
||
|
|
"pineagents.app.task_tracker",
|
||
|
|
"pineagents.runtime.commands",
|
||
|
|
# Backup modules are exposed through pineagents.backup.__getattr__, which
|
||
|
|
# PyInstaller cannot discover from static imports.
|
||
|
|
*collect_submodules("pineagents.backup"),
|
||
|
|
# Third-party packages that use dynamic imports. Use
|
||
|
|
# collect_submodules() for packages that load many submodules by name;
|
||
|
|
# keep the bare package string when runtime code imports only the
|
||
|
|
# package root or when PyInstaller needs the top-level module anchor.
|
||
|
|
*collect_submodules("dotenv"),
|
||
|
|
"dotenv",
|
||
|
|
*collect_submodules("acp"),
|
||
|
|
"acp",
|
||
|
|
"psutil",
|
||
|
|
"multipart",
|
||
|
|
"websockets",
|
||
|
|
"modelscope",
|
||
|
|
"modelscope.hub.api",
|
||
|
|
"modelscope.hub.snapshot_download",
|
||
|
|
*collect_submodules("agentscope.tool._builtin._scripts"),
|
||
|
|
*collect_submodules("agentscope.workspace._mcp_gateway"),
|
||
|
|
*collect_submodules("whisper"),
|
||
|
|
*collect_submodules("chromadb"),
|
||
|
|
],
|
||
|
|
hookspath=[],
|
||
|
|
hooksconfig={},
|
||
|
|
runtime_hooks=[],
|
||
|
|
excludes=[],
|
||
|
|
noarchive=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
pyz = PYZ(a.pure)
|
||
|
|
|
||
|
|
def script_entry(file_name):
|
||
|
|
for item in a.scripts:
|
||
|
|
if Path(item[1]).name == file_name:
|
||
|
|
return [item]
|
||
|
|
raise SystemExit(f"script entry not found: {file_name}")
|
||
|
|
|
||
|
|
|
||
|
|
backend_exe = EXE(
|
||
|
|
pyz,
|
||
|
|
script_entry("entry.py"),
|
||
|
|
[],
|
||
|
|
name="qwenpaw-backend",
|
||
|
|
debug=False,
|
||
|
|
bootloader_ignore_signals=False,
|
||
|
|
strip=False,
|
||
|
|
# UPX triggers antivirus false positives and can corrupt binaries.
|
||
|
|
upx=False,
|
||
|
|
console=False,
|
||
|
|
disable_windowed_traceback=True,
|
||
|
|
argv_emulation=False,
|
||
|
|
target_arch=None,
|
||
|
|
codesign_identity=codesign_identity,
|
||
|
|
exclude_binaries=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
cli_exe = EXE(
|
||
|
|
pyz,
|
||
|
|
script_entry("cli_entry.py"),
|
||
|
|
[],
|
||
|
|
name="qwenpaw",
|
||
|
|
debug=False,
|
||
|
|
bootloader_ignore_signals=False,
|
||
|
|
strip=False,
|
||
|
|
upx=False,
|
||
|
|
console=True,
|
||
|
|
disable_windowed_traceback=False,
|
||
|
|
argv_emulation=False,
|
||
|
|
target_arch=None,
|
||
|
|
codesign_identity=codesign_identity,
|
||
|
|
exclude_binaries=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
coll = COLLECT(
|
||
|
|
backend_exe,
|
||
|
|
cli_exe,
|
||
|
|
a.binaries,
|
||
|
|
a.datas,
|
||
|
|
strip=False,
|
||
|
|
upx=False,
|
||
|
|
name="qwenpaw-backend",
|
||
|
|
)
|