Skip to content

Commit 43f27a3

Browse files
authored
feat(macos): bundle self-contained runtime (#49)
Signed-off-by: xunzhuo <xunzhuo@vllm-semantic-router.ai>
1 parent 853e8d1 commit 43f27a3

6 files changed

Lines changed: 366 additions & 9 deletions

File tree

Makefile

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ MACOS_APP_BUILD_NUMBER ?=
2121
MACOS_SIGNING_IDENTITY ?=
2222
MACOS_NOTARIZE ?=
2323
MACOS_ASSET_DIR ?=
24+
MACOS_BUNDLE_RUNTIME ?= auto
25+
MACOS_RUNTIME_PYTHON_VERSION ?= 3.12
26+
MACOS_RUNTIME_PYTHON ?=
2427
MACOS_RELEASE_TAG ?= latest
2528
MACOS_RELEASE_TITLE ?= Elephant Agent latest
2629
RESET_API_E2E_TARGETS = \
@@ -107,6 +110,8 @@ macos-help:
107110
@echo " make macos-build MACOS_TARGET=x86_64-apple-darwin"
108111
@echo " make macos-build-all"
109112
@echo " make macos-release-latest"
113+
@echo " MACOS_BUNDLE_RUNTIME=auto|1|0"
114+
@echo " MACOS_RUNTIME_PYTHON=/path/to/python3.12"
110115
@echo ""
111116
@echo "Signing/notarization variables:"
112117
@echo " MACOS_SIGNING_IDENTITY='Developer ID Application: ...'"
@@ -121,6 +126,9 @@ macos-build:
121126
MACOS_SIGNING_IDENTITY="$(MACOS_SIGNING_IDENTITY)" \
122127
MACOS_NOTARIZE="$(MACOS_NOTARIZE)" \
123128
MACOS_ASSET_DIR="$(MACOS_ASSET_DIR)" \
129+
MACOS_BUNDLE_RUNTIME="$(MACOS_BUNDLE_RUNTIME)" \
130+
MACOS_RUNTIME_PYTHON_VERSION="$(MACOS_RUNTIME_PYTHON_VERSION)" \
131+
MACOS_RUNTIME_PYTHON="$(MACOS_RUNTIME_PYTHON)" \
124132
bash apps/macos/Scripts/build-app.sh
125133

126134
macos-build-all:
@@ -132,7 +140,10 @@ macos-build-all:
132140
MACOS_APP_BUILD_NUMBER="$(MACOS_APP_BUILD_NUMBER)" \
133141
MACOS_SIGNING_IDENTITY="$(MACOS_SIGNING_IDENTITY)" \
134142
MACOS_NOTARIZE="$(MACOS_NOTARIZE)" \
135-
MACOS_ASSET_DIR="$(MACOS_ASSET_DIR)"; \
143+
MACOS_ASSET_DIR="$(MACOS_ASSET_DIR)" \
144+
MACOS_BUNDLE_RUNTIME="$(MACOS_BUNDLE_RUNTIME)" \
145+
MACOS_RUNTIME_PYTHON_VERSION="$(MACOS_RUNTIME_PYTHON_VERSION)" \
146+
MACOS_RUNTIME_PYTHON="$(MACOS_RUNTIME_PYTHON)"; \
136147
done
137148

138149
macos-release-latest: macos-build-all

apps/macos/Entitlements.plist

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.cs.allow-jit</key>
6+
<true/>
7+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8+
<true/>
9+
<key>com.apple.security.cs.disable-library-validation</key>
10+
<true/>
11+
</dict>
12+
</plist>

apps/macos/README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,42 @@ The mac app does not replace the Python core. It starts the existing `apps.api`
77
## Build
88

99
```bash
10-
swift build --package-path apps/macos
11-
apps/macos/Scripts/build-app.sh
12-
open -n "apps/macos/.build/release/Elephant Agent.app"
10+
make macos-build
11+
open -n "apps/macos/.build/release/$(uname -m | sed 's/arm64/aarch64/')-apple-darwin/Elephant Agent.app"
1312
```
1413

1514
Full Xcode is not required when the installed Command Line Tools and macOS SDK match. The packaging script creates a local `.app`, copies the site brand assets into the bundle resources, signs ad hoc by default, and emits a `.dmg`, `.app.zip`, and SHA256 files under `apps/macos/.build/artifacts/<target>/`.
1615

16+
By default, `make macos-build` attempts to produce a self-contained app on the current Mac architecture. When `uv` is available and the requested `MACOS_TARGET` matches the host architecture, the bundle includes:
17+
18+
- `Contents/Resources/Runtime/python`: managed CPython 3.12
19+
- `Contents/Resources/Runtime/site-packages`: Elephant Agent and Python dependencies
20+
- `Contents/Resources/Runtime/ms-playwright`: Playwright Chromium headless shell
21+
22+
Set `MACOS_BUNDLE_RUNTIME=0` for a lightweight bootstrap build that falls back to the bundled `Install/install.sh` on machines without a developer repo. Set `MACOS_BUNDLE_RUNTIME=1` to require the embedded runtime and fail instead of falling back. Cross-architecture self-contained builds should run on a matching macOS runner or pass `MACOS_RUNTIME_PYTHON=/path/to/python3.12` for the target architecture.
23+
1724
The repo-level Makefile wraps the release paths:
1825

1926
```bash
2027
make macos-build
2128
make macos-build MACOS_TARGET=aarch64-apple-darwin
2229
make macos-build MACOS_TARGET=x86_64-apple-darwin
2330
make macos-build-all
31+
make macos-build MACOS_BUNDLE_RUNTIME=1
2432
```
2533

2634
Developer ID distribution builds can opt into signing and notarization:
2735

2836
```bash
2937
make macos-build-all \
38+
MACOS_BUNDLE_RUNTIME=1 \
3039
MACOS_SIGNING_IDENTITY="Developer ID Application: Example Team (TEAMID)" \
3140
MACOS_NOTARIZE=1 \
3241
APPLE_ID="apple-id@example.com" \
3342
APPLE_PASSWORD="app-specific-password" \
3443
APPLE_TEAM_ID="TEAMID"
3544
```
3645

46+
Without `MACOS_SIGNING_IDENTITY`, builds remain ad-hoc signed and notarization is skipped so local developers can still build a DMG. Ad-hoc artifacts are useful for testing but are not Gatekeeper-clean for broad distribution. Official shareable releases should use Developer ID signing and notarization.
47+
3748
`make macos-release-latest` expects `gh` authentication and replaces the GitHub `latest` release/tag with the current local artifacts. The CI workflow `.github/workflows/macos-latest-release.yml` runs the same build on each push to `main`, uploads both macOS architecture artifacts, writes `latest.json`, and replaces the `latest` GitHub release.

apps/macos/Scripts/build-app.sh

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ BUNDLE_IDENTIFIER="${MACOS_BUNDLE_IDENTIFIER:-ai.agentic.elephant.mac}"
1010
DEPLOYMENT_TARGET="${MACOS_DEPLOYMENT_TARGET:-13.0}"
1111
SIGNING_IDENTITY="${MACOS_SIGNING_IDENTITY:--}"
1212
NOTARIZE="${MACOS_NOTARIZE:-0}"
13+
SIGNING_ENTITLEMENTS="${MACOS_CODESIGN_ENTITLEMENTS:-${APP_DIR}/Entitlements.plist}"
14+
BUNDLE_RUNTIME_REQUEST="${MACOS_BUNDLE_RUNTIME:-auto}"
1315

1416
host_target() {
1517
case "$(uname -m)" in
@@ -97,6 +99,49 @@ RESOURCES="${CONTENTS}/Resources"
9799
ARTIFACT_PREFIX="ElephantAgent_${APP_VERSION_SAFE}_${ARTIFACT_TARGET}"
98100
ARTIFACT_DMG="${ARTIFACT_DIR}/${ARTIFACT_PREFIX}.dmg"
99101
ARTIFACT_APP_ZIP="${ARTIFACT_DIR}/${ARTIFACT_PREFIX}.app.zip"
102+
RUNTIME_ROOT="${RESOURCES}/Runtime"
103+
RUNTIME_BUILD_CACHE="${MACOS_RUNTIME_BUILD_CACHE:-${BUILD_DIR}/runtime-cache}"
104+
105+
can_auto_bundle_runtime() {
106+
if [[ -n "${MACOS_RUNTIME_PYTHON:-}" ]]; then
107+
return 0
108+
fi
109+
if [[ "${ARTIFACT_TARGET}" != "$(host_target)" ]]; then
110+
return 1
111+
fi
112+
command -v uv >/dev/null 2>&1
113+
}
114+
115+
resolve_bundle_runtime() {
116+
case "${BUNDLE_RUNTIME_REQUEST}" in
117+
1|true|yes|on)
118+
if [[ -z "${MACOS_RUNTIME_PYTHON:-}" && "${ARTIFACT_TARGET}" != "$(host_target)" ]]; then
119+
echo "MACOS_BUNDLE_RUNTIME=1 requires a matching host target or MACOS_RUNTIME_PYTHON for ${ARTIFACT_TARGET}." >&2
120+
exit 1
121+
fi
122+
printf '%s\n' "1"
123+
;;
124+
0|false|no|off)
125+
printf '%s\n' "0"
126+
;;
127+
auto|"")
128+
if [[ -n "${MACOS_RUNTIME_PYTHON:-}" ]]; then
129+
printf '%s\n' "1"
130+
elif can_auto_bundle_runtime; then
131+
printf '%s\n' "1"
132+
else
133+
echo "Bundled runtime unavailable for ${ARTIFACT_TARGET}; falling back to installer bootstrap." >&2
134+
printf '%s\n' "0"
135+
fi
136+
;;
137+
*)
138+
echo "Unsupported MACOS_BUNDLE_RUNTIME: ${BUNDLE_RUNTIME_REQUEST}" >&2
139+
exit 2
140+
;;
141+
esac
142+
}
143+
144+
BUNDLE_RUNTIME="$(resolve_bundle_runtime)"
100145

101146
if [[ "${NOTARIZE}" == "auto" ]]; then
102147
if [[ "${SIGNING_IDENTITY}" != "-" \
@@ -138,12 +183,40 @@ sign_path() {
138183
local path="$1"
139184
require_macos_tool codesign
140185
if [[ "${SIGNING_IDENTITY}" == "-" ]]; then
141-
codesign --force --deep --sign - "${path}" >/dev/null
186+
codesign --force --sign - "${path}" >/dev/null
142187
else
143-
codesign --force --deep --options runtime --timestamp --sign "${SIGNING_IDENTITY}" "${path}" >/dev/null
188+
if [[ -f "${SIGNING_ENTITLEMENTS}" ]]; then
189+
codesign --force --options runtime --timestamp --entitlements "${SIGNING_ENTITLEMENTS}" --sign "${SIGNING_IDENTITY}" "${path}" >/dev/null
190+
else
191+
codesign --force --options runtime --timestamp --sign "${SIGNING_IDENTITY}" "${path}" >/dev/null
192+
fi
144193
fi
145194
}
146195

196+
sign_macho_file() {
197+
local path="$1"
198+
require_macos_tool codesign
199+
if [[ "${SIGNING_IDENTITY}" == "-" ]]; then
200+
codesign --force --sign - "${path}" >/dev/null
201+
else
202+
if [[ -f "${SIGNING_ENTITLEMENTS}" ]]; then
203+
codesign --force --options runtime --timestamp --entitlements "${SIGNING_ENTITLEMENTS}" --sign "${SIGNING_IDENTITY}" "${path}" >/dev/null
204+
else
205+
codesign --force --options runtime --timestamp --sign "${SIGNING_IDENTITY}" "${path}" >/dev/null
206+
fi
207+
fi
208+
}
209+
210+
sign_nested_macho_files() {
211+
local root="$1"
212+
require_macos_tool file
213+
while IFS= read -r -d '' path; do
214+
if file "${path}" | grep -q "Mach-O"; then
215+
sign_macho_file "${path}"
216+
fi
217+
done < <(find "${root}" -type f -print0)
218+
}
219+
147220
notarize_submission() {
148221
local path="$1"
149222
local label="$2"
@@ -211,9 +284,13 @@ fi
211284
mkdir -p "${MACOS}" "${RESOURCES}/Brand" "${RESOURCES}/Resources" "${RESOURCES}/Install"
212285
install -m 755 "${BINARY}" "${MACOS}/${APP_NAME}"
213286

214-
printf "%s\n" "${REPO_ROOT}" > "${RESOURCES}/RepoRoot.txt"
215-
if command -v python3 >/dev/null 2>&1; then
216-
command -v python3 > "${RESOURCES}/PythonPath.txt"
287+
if [[ "${BUNDLE_RUNTIME}" == "1" && "${MACOS_INCLUDE_DEV_PATHS:-0}" != "1" ]]; then
288+
rm -f "${RESOURCES}/RepoRoot.txt" "${RESOURCES}/PythonPath.txt"
289+
else
290+
printf "%s\n" "${REPO_ROOT}" > "${RESOURCES}/RepoRoot.txt"
291+
if command -v python3 >/dev/null 2>&1; then
292+
command -v python3 > "${RESOURCES}/PythonPath.txt"
293+
fi
217294
fi
218295
install -m 755 "${REPO_ROOT}/install.sh" "${RESOURCES}/Install/install.sh"
219296

@@ -238,6 +315,12 @@ if [[ -d "${MACOS_RESOURCES_DIR}" ]]; then
238315
ditto "${MACOS_RESOURCES_DIR}" "${RESOURCES}"
239316
fi
240317

318+
if [[ "${BUNDLE_RUNTIME}" == "1" ]]; then
319+
bash "${SCRIPT_DIR}/package-runtime.sh" "${RUNTIME_ROOT}" "${REPO_ROOT}" "${ARTIFACT_TARGET}" "${RUNTIME_BUILD_CACHE}"
320+
else
321+
echo "Bundled runtime: disabled. This build will use Install/install.sh bootstrap on machines without a developer repo."
322+
fi
323+
241324
ICON_SOURCE="${RESOURCES}/Brand/favicon.png"
242325
ICONSET="${RESOURCES}/AppIcon.iconset"
243326
if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1 && [[ -f "${ICON_SOURCE}" ]]; then
@@ -300,6 +383,7 @@ cat > "${CONTENTS}/Info.plist" <<PLIST
300383
</plist>
301384
PLIST
302385

386+
sign_nested_macho_files "${CONTENTS}"
303387
sign_path "${BUNDLE}"
304388
codesign --verify --deep --strict --verbose=2 "${BUNDLE}"
305389

@@ -323,6 +407,8 @@ hdiutil create -volname "${APP_NAME}" -srcfolder "${STAGE}" -ov -format UDZO "${
323407
if [[ "${SIGNING_IDENTITY}" != "-" ]]; then
324408
codesign --force --timestamp --sign "${SIGNING_IDENTITY}" "${DMG}" >/dev/null
325409
notarize_path "${DMG}" "${APP_NAME}.dmg"
410+
else
411+
echo "DMG signing/notarization skipped: MACOS_SIGNING_IDENTITY is ad-hoc."
326412
fi
327413

328414
ditto -c -k --keepParent "${BUNDLE}" "${ARTIFACT_APP_ZIP}"
@@ -337,3 +423,4 @@ printf ' artifact_dmg: %s\n' "${ARTIFACT_DMG}"
337423
printf ' artifact_dmg_sha256: %s\n' "${ARTIFACT_DMG}.sha256"
338424
printf ' artifact_app_zip: %s\n' "${ARTIFACT_APP_ZIP}"
339425
printf ' artifact_app_zip_sha256: %s\n' "${ARTIFACT_APP_ZIP}.sha256"
426+
printf ' bundled_runtime: %s\n' "${BUNDLE_RUNTIME}"

0 commit comments

Comments
 (0)