Files
tutti/tools/scripts/build-desktop-package.sh
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

532 lines
16 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
APP_DIR="${ROOT_DIR}/apps/desktop"
VARIANT="${1:-unpack}"
BUILTIN_APPS_PREPARED=false
DAEMON_BUNDLE_DIR="${APP_DIR}/build/tuttid"
CLI_BUNDLE_DIR="${APP_DIR}/build/tutti"
DESKTOP_BUILD_VERSION="${TUTTI_DESKTOP_BUILD_VERSION:-}"
MAC_ARCH="${TUTTI_DESKTOP_MAC_ARCH:-all}"
MAC_ARCH_ARGS=()
if [[ "$#" -gt 0 ]]; then
shift
fi
while [[ "$#" -gt 0 ]]; do
case "$1" in
--prepared-builtin-apps)
BUILTIN_APPS_PREPARED=true
;;
*)
echo "Unsupported desktop package argument: $1" >&2
exit 1
;;
esac
shift
done
release_timing_log() {
echo "[release-timing] $*"
}
run_timed_phase() {
local phase="$1"
shift
local start_seconds="${SECONDS}"
local exit_code=0
local status="done"
release_timing_log "phase=${phase} status=start"
set +e
"$@"
exit_code=$?
set -e
if [[ "${exit_code}" -ne 0 ]]; then
status="failed"
fi
release_timing_log "phase=${phase} status=${status} elapsed=$((SECONDS - start_seconds))s"
return "${exit_code}"
}
has_env() {
local name="$1"
[[ -n "${!name:-}" ]]
}
has_notarization_credentials() {
(
has_env APPLE_API_KEY &&
has_env APPLE_API_KEY_ID &&
has_env APPLE_API_ISSUER
) || (
has_env APPLE_ID &&
has_env APPLE_APP_SPECIFIC_PASSWORD &&
has_env APPLE_TEAM_ID
) || (
has_env APPLE_KEYCHAIN_PROFILE
)
}
has_signing_identity() {
has_env CSC_LINK ||
has_env CSC_NAME ||
security find-identity -p codesigning -v 2>/dev/null | grep -q "Developer ID Application"
}
is_windows_package_variant() {
[[ "${VARIANT}" == "win" || "${VARIANT}" == "win-unpacked" || "${VARIANT}" == "win-store" ]]
}
is_macos_package_variant() {
[[ "${VARIANT}" == "mac" || "${VARIANT}" == "mac-unsigned" || "${VARIANT}" == "mac-signed" ]]
}
require_macos_packaging_tools() {
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "macOS release packaging requires macOS." >&2
exit 1
fi
if ! command -v lipo >/dev/null 2>&1; then
echo "macOS release packaging requires lipo." >&2
exit 1
fi
}
require_signed_macos_release_environment() {
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "Signed macOS release packaging requires macOS." >&2
exit 1
fi
if ! has_signing_identity; then
cat >&2 <<'EOF'
Signed macOS release packaging requires a Developer ID Application signing identity.
Install the certificate in this keychain, or provide CSC_LINK plus CSC_KEY_PASSWORD.
Use "pnpm --filter @tutti-os/desktop build:mac:unsigned" for local packages that do not need Gatekeeper trust.
EOF
exit 1
fi
if ! has_notarization_credentials; then
cat >&2 <<'EOF'
Signed macOS release packaging requires Apple notarization credentials.
Recommended CI variables: APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER.
Use "pnpm --filter @tutti-os/desktop build:mac:unsigned" for local packages that do not need notarization.
EOF
exit 1
fi
}
build_macos_universal_go_binary() {
local package_dir="$1"
local package_path="$2"
local output_path="$3"
local output_name="$4"
local staging_dir
staging_dir="$(mktemp -d)"
(
cd "${package_dir}"
GOOS=darwin GOARCH=arm64 go build -o "${staging_dir}/${output_name}-arm64" "${package_path}" &&
GOOS=darwin GOARCH=amd64 go build -o "${staging_dir}/${output_name}-amd64" "${package_path}"
) || {
rm -rf "${staging_dir}"
return 1
}
lipo -create \
"${staging_dir}/${output_name}-arm64" \
"${staging_dir}/${output_name}-amd64" \
-output "${output_path}" || {
rm -rf "${staging_dir}"
return 1
}
lipo "${output_path}" -verify_arch arm64 x86_64 || {
rm -rf "${staging_dir}"
return 1
}
chmod 755 "${output_path}"
rm -rf "${staging_dir}"
}
prepare_packaged_daemon() {
rm -rf "${DAEMON_BUNDLE_DIR}" "${CLI_BUNDLE_DIR}"
mkdir -p "${DAEMON_BUNDLE_DIR}" "${CLI_BUNDLE_DIR}"
local daemon_output_name="tuttid"
local cli_output_name="tutti"
if is_windows_package_variant; then
daemon_output_name="tuttid.exe"
cli_output_name="tutti.exe"
fi
if is_macos_package_variant; then
require_macos_packaging_tools
build_macos_universal_go_binary \
"${ROOT_DIR}/services/tuttid" \
"." \
"${DAEMON_BUNDLE_DIR}/${daemon_output_name}" \
"${daemon_output_name}" || return
build_macos_universal_go_binary \
"${ROOT_DIR}/apps/cli" \
"./cmd/tutti" \
"${CLI_BUNDLE_DIR}/${cli_output_name}" \
"${cli_output_name}" || return
return
fi
if is_windows_package_variant; then
(
cd "${ROOT_DIR}/services/tuttid"
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
go build -o "${DAEMON_BUNDLE_DIR}/${daemon_output_name}" .
)
(
cd "${ROOT_DIR}/apps/cli"
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
go build -o "${CLI_BUNDLE_DIR}/${cli_output_name}" ./cmd/tutti
)
return
fi
(
cd "${ROOT_DIR}/services/tuttid"
go build -o "${DAEMON_BUNDLE_DIR}/${daemon_output_name}" .
)
(
cd "${ROOT_DIR}/apps/cli"
go build -o "${CLI_BUNDLE_DIR}/${cli_output_name}" ./cmd/tutti
)
}
prepare_builtin_apps() {
if [[ "${BUILTIN_APPS_PREPARED}" == "true" ]]; then
local archives=("${ROOT_DIR}"/services/tuttid/builtin-apps/generated/tutti-onboarding/*.zip)
if [[ ! -f "${archives[0]}" ]]; then
echo "Prepared builtin apps are missing; run pnpm generate:builtin-apps first." >&2
return 1
fi
release_timing_log "builtin_apps=prepared"
return
fi
(
cd "${ROOT_DIR}"
pnpm generate:builtin-apps
)
}
prepare_browser_mcp() {
# Vendors a pinned chrome-devtools-mcp into build/browser-mcp so packaged
# browser use never fetches it over the network at runtime. The daemon
# launcher (resolveBrowserMcpDaemonEnv) points the daemon at the bundle.
node "${ROOT_DIR}/apps/desktop/scripts/vendor-browser-mcp.mjs"
}
prepare_claude_sdk_sidecar() {
# Vendors the Claude SDK sidecar into build/claude-sdk-sidecar so packaged
# Claude SDK sessions do not depend on repository source paths at runtime.
# The native claude binaries are provisioned by tuttid at runtime and are
# deliberately absent from the bundle.
node "${ROOT_DIR}/apps/desktop/scripts/vendor-claude-sdk-sidecar.mjs"
}
prepare_managed_posix_shell() {
if ! is_windows_package_variant; then
return
fi
node "${ROOT_DIR}/apps/desktop/scripts/vendor-managed-posix-shell.mjs" --platform=windows-amd64
}
prepare_mutagen() {
if ! is_windows_package_variant; then
return
fi
node "${ROOT_DIR}/apps/desktop/scripts/vendor-mutagen.mjs" --platform=windows-amd64
}
prepare_managed_uv() {
local platforms=()
case "${VARIANT}" in
win|win-unpacked|win-store)
platforms=(windows-amd64)
;;
linux)
platforms=(linux-amd64)
;;
mac|mac-unsigned|mac-signed)
# The upstream macOS uv archives contain the uv and uvx executables.
# They are intentionally left out of the signed app: Apple notarization
# recursively validates executable members in nested tar.gz files and
# rejects these upstream binaries because they are not signed with the
# app's Developer ID identity. macOS keeps the daemon's verified dynamic
# download fallback; Windows and Linux still receive the bundled archive.
rm -rf "${APP_DIR}/build/managed-uv"
mkdir -p "${APP_DIR}/build/managed-uv"
return
;;
unpack)
platforms=("$(node -e 'const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux"; const arch = process.arch === "arm64" ? "arm64" : "amd64"; process.stdout.write(`${os}-${arch}`)')")
;;
esac
local args=()
local platform
for platform in "${platforms[@]}"; do
args+=("--platform=${platform}")
done
node "${ROOT_DIR}/apps/desktop/scripts/vendor-managed-uv.mjs" "${args[@]}"
}
prepare_rtk() {
local platforms=()
case "${VARIANT}" in
win|win-unpacked|win-store)
platforms=(windows-amd64)
;;
linux)
platforms=(linux-amd64)
;;
mac|mac-unsigned|mac-signed)
platforms=(darwin-arm64 darwin-amd64)
;;
unpack)
platforms=("$(node -e 'const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux"; const arch = process.arch === "arm64" ? "arm64" : "amd64"; process.stdout.write(`${os}-${arch}`)')")
;;
esac
local args=()
local platform
for platform in "${platforms[@]}"; do
args+=("--platform=${platform}")
done
node "${ROOT_DIR}/apps/desktop/scripts/vendor-rtk.mjs" "${args[@]}"
}
run_pnpm_build() {
pnpm build
}
resolve_desktop_build_version() {
if [[ -n "${DESKTOP_BUILD_VERSION}" ]]; then
return
fi
DESKTOP_BUILD_VERSION="$(node "${APP_DIR}/scripts/resolve-build-version.mjs")"
export DESKTOP_BUILD_VERSION
release_timing_log "desktop_version=${DESKTOP_BUILD_VERSION}"
}
run_electron_builder_unpack() {
CSC_IDENTITY_AUTO_DISCOVERY=false pnpm exec electron-builder --dir --publish never "-c.extraMetadata.version=${DESKTOP_BUILD_VERSION}"
}
resolve_macos_arch_args() {
case "${MAC_ARCH}" in
all)
MAC_ARCH_ARGS=(--x64 --arm64 --universal)
;;
x64)
MAC_ARCH_ARGS=(--x64)
;;
arm64)
MAC_ARCH_ARGS=(--arm64)
;;
universal)
MAC_ARCH_ARGS=(--universal)
;;
*)
echo "Unsupported macOS architecture: ${MAC_ARCH}" >&2
return 1
;;
esac
}
run_electron_builder_mac_unsigned() {
resolve_macos_arch_args
env \
-u CSC_LINK \
-u CSC_KEY_PASSWORD \
-u CSC_NAME \
-u APPLE_API_KEY \
-u APPLE_API_KEY_ID \
-u APPLE_API_ISSUER \
-u APPLE_ID \
-u APPLE_APP_SPECIFIC_PASSWORD \
-u APPLE_TEAM_ID \
-u APPLE_KEYCHAIN_PROFILE \
CSC_IDENTITY_AUTO_DISCOVERY=false \
pnpm exec electron-builder --mac "${MAC_ARCH_ARGS[@]}" --publish never -c.mac.notarize=false "-c.extraMetadata.version=${DESKTOP_BUILD_VERSION}"
}
run_electron_builder_mac_signed() {
resolve_macos_arch_args
pnpm exec electron-builder --mac "${MAC_ARCH_ARGS[@]}" --publish never -c.mac.notarize=true "-c.extraMetadata.version=${DESKTOP_BUILD_VERSION}"
}
run_electron_builder_win() {
env \
npm_package_json="${ROOT_DIR}/package.json" \
INIT_CWD="${ROOT_DIR}" \
pnpm exec electron-builder --win --publish never "-c.extraMetadata.version=${DESKTOP_BUILD_VERSION}"
}
run_electron_builder_win_unpack() {
env \
-u CSC_LINK \
-u CSC_KEY_PASSWORD \
-u CSC_NAME \
npm_package_json="${ROOT_DIR}/package.json" \
INIT_CWD="${ROOT_DIR}" \
CSC_IDENTITY_AUTO_DISCOVERY=false \
pnpm exec electron-builder --win --x64 --dir --publish never \
"-c.extraMetadata.version=${DESKTOP_BUILD_VERSION}"
}
run_electron_builder_win_prepackaged() {
local prepackaged_dir="${TSH_WINDOWS_PREPACKAGED_DIR:-}"
local signer="${TSH_CERTUM_ELECTRON_BUILDER_SIGNER:-}"
local publisher="${TSH_WINDOWS_EXPECTED_PUBLISHER:-}"
local certum_config="${APP_DIR}/scripts/electron-builder-certum-options.cjs"
local wine_runtime_dir="${TSH_WINDOWS_WINE_RUNTIME_DIR:-}"
[[ -d "${prepackaged_dir}" ]] || {
echo "TSH_WINDOWS_PREPACKAGED_DIR must point to the signed unpacked Windows application." >&2
return 1
}
[[ -f "${signer}" ]] || {
echo "TSH_CERTUM_ELECTRON_BUILDER_SIGNER must point to the Certum signing hook." >&2
return 1
}
[[ -n "${publisher}" ]] || {
echo "TSH_WINDOWS_EXPECTED_PUBLISHER is required for signed Windows packaging." >&2
return 1
}
wine_runtime_dir="${wine_runtime_dir:-${RUNNER_TEMP:?RUNNER_TEMP is required}/windows-signing-wine}"
mkdir -p "${wine_runtime_dir}/home" "${wine_runtime_dir}/prefix"
env \
-u CSC_LINK \
-u CSC_KEY_PASSWORD \
-u CSC_NAME \
HOME="${wine_runtime_dir}/home" \
WINEPREFIX="${wine_runtime_dir}/prefix" \
npm_package_json="${ROOT_DIR}/package.json" \
INIT_CWD="${ROOT_DIR}" \
CSC_IDENTITY_AUTO_DISCOVERY=false \
pnpm exec electron-builder \
--win --x64 \
--prepackaged "${prepackaged_dir}" \
--publish never \
--config "${certum_config}" \
"-c.extraMetadata.version=${DESKTOP_BUILD_VERSION}"
}
require_store_value() {
local name="$1"
if ! has_env "${name}"; then
echo "Windows Store packaging requires ${name}." >&2
return 1
fi
}
require_windows_store_packaging_config() {
require_store_value TUTTI_STORE_IDENTITY_NAME
require_store_value TUTTI_STORE_PUBLISHER
require_store_value TUTTI_STORE_PUBLISHER_DISPLAY_NAME
if [[ ! "${DESKTOP_BUILD_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Windows Store packaging accepts only a plain stable version, got ${DESKTOP_BUILD_VERSION}." >&2
return 1
fi
local version_part
local -a version_parts=()
IFS='.' read -r -a version_parts <<<"${DESKTOP_BUILD_VERSION}"
for version_part in "${version_parts[@]}"; do
if ((version_part > 65535)); then
echo "Windows Store version components must not exceed 65535: ${DESKTOP_BUILD_VERSION}." >&2
return 1
fi
done
}
run_electron_builder_win_store() {
require_windows_store_packaging_config
local application_id="${TUTTI_STORE_APPLICATION_ID:-Tutti}"
local display_name="${TUTTI_STORE_DISPLAY_NAME:-Tutti}"
env \
npm_package_json="${ROOT_DIR}/package.json" \
INIT_CWD="${ROOT_DIR}" \
CSC_IDENTITY_AUTO_DISCOVERY=false \
pnpm exec electron-builder --win appx --x64 --publish never \
"-c.extraMetadata.version=${DESKTOP_BUILD_VERSION}" \
"-c.appx.applicationId=${application_id}" \
"-c.appx.displayName=${display_name}" \
"-c.appx.identityName=${TUTTI_STORE_IDENTITY_NAME}" \
"-c.appx.publisher=${TUTTI_STORE_PUBLISHER}" \
"-c.appx.publisherDisplayName=${TUTTI_STORE_PUBLISHER_DISPLAY_NAME}"
}
run_electron_builder_linux() {
pnpm exec electron-builder --linux AppImage --publish never "-c.extraMetadata.version=${DESKTOP_BUILD_VERSION}"
}
case "${VARIANT}" in
win-prepackaged)
(
cd "${APP_DIR}"
run_timed_phase "resolve_desktop_build_version" resolve_desktop_build_version
run_timed_phase "electron_builder_win_prepackaged" run_electron_builder_win_prepackaged
)
;;
unpack|mac|mac-unsigned|mac-signed|win|win-unpacked|win-store|linux)
release_timing_log "variant=${VARIANT} status=start"
run_timed_phase "prepare_builtin_apps" prepare_builtin_apps
run_timed_phase "prepare_packaged_daemon" prepare_packaged_daemon
run_timed_phase "prepare_browser_mcp" prepare_browser_mcp
run_timed_phase "prepare_claude_sdk_sidecar" prepare_claude_sdk_sidecar
run_timed_phase "prepare_managed_posix_shell" prepare_managed_posix_shell
run_timed_phase "prepare_mutagen" prepare_mutagen
run_timed_phase "prepare_managed_uv" prepare_managed_uv
run_timed_phase "prepare_rtk" prepare_rtk
(
cd "${APP_DIR}"
run_timed_phase "resolve_desktop_build_version" resolve_desktop_build_version
run_timed_phase "pnpm_build" run_pnpm_build
case "${VARIANT}" in
unpack)
run_timed_phase "electron_builder_unpack" run_electron_builder_unpack
;;
mac|mac-unsigned)
run_timed_phase "electron_builder_mac_unsigned" run_electron_builder_mac_unsigned
;;
mac-signed)
run_timed_phase "require_signed_macos_release_environment" require_signed_macos_release_environment
run_timed_phase "electron_builder_mac_signed" run_electron_builder_mac_signed
;;
win)
run_timed_phase "electron_builder_win" run_electron_builder_win
;;
win-unpacked)
run_timed_phase "electron_builder_win_unpack" run_electron_builder_win_unpack
;;
win-store)
run_timed_phase "electron_builder_win_store" run_electron_builder_win_store
;;
linux)
run_timed_phase "electron_builder_linux" run_electron_builder_linux
;;
esac
)
release_timing_log "variant=${VARIANT} status=done"
;;
*)
echo "Usage: $0 <unpack|mac|mac-unsigned|mac-signed|win|win-unpacked|win-prepackaged|win-store|linux>" >&2
exit 1
;;
esac