Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 63 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
include:
# Linux builds use ubuntu-22.04 (glibc 2.35) rather than a manylinux
# container: manylinux ships a static CPython without libpython*.so,
# which PyInstaller cannot embed. The glibc-2.35 floor still covers
# which Nuitka cannot embed. The glibc-2.35 floor still covers
# Ubuntu 22.04+, Debian 12+, RHEL 9+, and all current modern distros.
- target: linux-amd64
runner: ubuntu-22.04
Expand All @@ -95,10 +95,13 @@ jobs:
runner: macos-latest
ext: ''
archive: tar.gz
- target: windows-amd64
runner: windows-latest
ext: '.exe'
archive: zip
# Windows build temporarily disabled: MSVC path can't use ccache,
# so every run is a cold compile and takes >30 min on the default
# GitHub runner. Re-enable after switching to sccache or MinGW-w64.
# - target: windows-amd64
# runner: windows-latest
# ext: '.exe'
# archive: zip

steps:
- uses: actions/checkout@v4
Expand All @@ -111,23 +114,74 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install project + PyInstaller
- name: Install Nuitka build toolchain (Linux)
if: startsWith(matrix.target, 'linux-')
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq patchelf ccache

- name: Install ccache (macOS)
if: startsWith(matrix.target, 'darwin-')
shell: bash
run: brew install ccache

# Persist ccache across runs. Key hashes pyproject.toml + build-binary.sh
# so a Nuitka pin bump or a build-script flag change buckets the cache
# into a fresh partition. ccache itself is content-addressed, so stale
# .o reuse is not a concern: if the generated C changes, it misses.
# Skipped on Windows because Nuitka uses MSVC there and ccache does not
# integrate with cl.exe.
- name: Restore ccache
if: ${{ !startsWith(matrix.target, 'windows-') }}
uses: actions/cache@v4
with:
path: |
~/.cache/ccache
~/Library/Caches/ccache
key: ccache-${{ matrix.target }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'scripts/build-binary.sh') }}
restore-keys: |
ccache-${{ matrix.target }}-py${{ env.PYTHON_VERSION }}-
ccache-${{ matrix.target }}-

- name: Configure ccache
if: ${{ !startsWith(matrix.target, 'windows-') }}
shell: bash
run: |
ccache --max-size=2G
ccache --zero-stats

- name: Install project + Nuitka
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install pyinstaller
python -m pip install "nuitka>=2.4" "zstandard>=0.22"

- name: Build binary
shell: bash
run: |
pyinstaller --clean --noconfirm agentrun.spec
bash scripts/build-binary.sh
ls -lh dist/

- name: Print ccache stats
if: ${{ !startsWith(matrix.target, 'windows-') }}
shell: bash
run: ccache --show-stats

- name: Smoke test binary
shell: bash
run: |
./dist/agentrun${{ matrix.ext }} --version
BIN="./dist/agentrun${{ matrix.ext }}"
"$BIN" --version
"$BIN" --help >/dev/null
"$BIN" config --help >/dev/null
"$BIN" model --help >/dev/null
"$BIN" sandbox --help >/dev/null
"$BIN" skill --help >/dev/null
"$BIN" super-agent --help >/dev/null
"$BIN" tool --help >/dev/null
echo "All 8 subcommand invocations OK on ${{ matrix.target }}"

# --- Package (Unix) -----------------------------------------------
- name: Package tar.gz (Unix)
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ venv/
.idea/
.vscode/
*.spec
!agentrun.spec
# Nuitka build artifacts
*.dist/
*.build/
*.bin
*.onefile-build/
.coverage
coverage.json
htmlcov/
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ make test # Run all tests
.venv/bin/pytest tests/test_cli_basic.py::TestConfigCommands::test_set_and_get -v # Single test

# Build standalone binary
make build # PyInstaller binary → dist/ar
make build # Nuitka --onefile binary → dist/agentrun (warm-start cache: ~/.agentrun/cache/)
make build-all # macOS + Linux (via Docker)
```

Expand Down
20 changes: 8 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ VENV := .venv
BIN := $(VENV)/bin
PIP := $(BIN)/pip
PIP_MIRROR := -i https://mirrors.aliyun.com/pypi/simple/
PYINST := $(BIN)/pyinstaller
APP_NAME := agentrun
SPEC := agentrun.spec
VERSION := $(shell $(PYTHON) -c "from src.agentrun_cli import __version__; print(__version__)" 2>/dev/null || echo "0.1.0")

help: ## Show this help message
Expand All @@ -21,7 +19,6 @@ install: ## Install the package in editable mode
dev: ## Install with dev dependencies
$(PYTHON) -m venv $(VENV)
$(PIP) install $(PIP_MIRROR) -e ".[dev]" || $(PIP) install $(PIP_MIRROR) -e .
$(PIP) install $(PIP_MIRROR) pyinstaller

lint: ## Run ruff linter
$(BIN)/ruff check src/ tests/
Expand All @@ -45,18 +42,18 @@ clean: ## Remove build artifacts
rm -rf build/ dist/ *.spec __pycache__
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true

build: ## Build binary for the current platform (uses agentrun.spec)
DISABLE_BREAKING_CHANGES_WARNING=1 \
$(PYINST) --clean --noconfirm $(SPEC)
build: ## Build single-file binary for the current platform (uses Nuitka)
bash scripts/build-binary.sh
@echo ""
@echo "Binary built: dist/$(APP_NAME)"
@ls -lh dist/$(APP_NAME)

build-macos: build ## Alias for build (on macOS, just run 'make build')
@echo "macOS binary ready at dist/$(APP_NAME)"

# Cross-compiling Python to Linux is not supported by PyInstaller.
# Use this target inside a Linux environment (Docker / CI).
# Nuitka compiles Python to native C, so cross-compiling is not supported.
# Use this target inside a Linux environment (Docker / CI) to produce a
# Linux binary when you're on a non-Linux host.
build-linux: build ## Build Linux binary (run inside Linux or Docker)
@echo "Linux binary ready at dist/$(APP_NAME)"

Expand All @@ -69,10 +66,9 @@ build-all: ## Build for all platforms (macOS local + Linux via Docker)
tar cf - --exclude=.venv --exclude=.git --exclude=build --exclude=dist --exclude=__pycache__ --exclude='*.pyc' . | \
docker run --rm -i -v $(PWD)/dist:/out python:3.10-slim sh -c \
"mkdir /build && cd /build && tar xf - && \
apt-get update -qq && apt-get install -y -qq binutils >/dev/null 2>&1 && \
pip install $(PIP_MIRROR) -e . && pip install $(PIP_MIRROR) pyinstaller && \
DISABLE_BREAKING_CHANGES_WARNING=1 \
pyinstaller --clean --noconfirm $(SPEC) && \
apt-get update -qq && apt-get install -y -qq binutils patchelf ccache gcc >/dev/null 2>&1 && \
pip install $(PIP_MIRROR) -e . && pip install $(PIP_MIRROR) nuitka zstandard && \
bash scripts/build-binary.sh && \
cp dist/$(APP_NAME) /out/$(APP_NAME)"
@mkdir -p dist/linux && cp dist/$(APP_NAME) dist/linux/$(APP_NAME)
@echo ""
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ agents that you configure declaratively without writing or deploying any runtime
- **Multiple output formats** — `json` (default), `table`, `yaml`, and `quiet` for shell piping.
- **Agent-friendly** — JSON-by-default output, deterministic exit codes, no interactive prompts when stdin isn't a TTY.
- **Rich sandbox primitives** — code execution, file system, process management, and CDP/VNC-backed browser automation.
- **Single-file distribution** — PyInstaller produces standalone `ar` / `agentrun` binaries for Linux, macOS and Windows (x86_64 + arm64).
- **Single-file distribution** — Nuitka `--onefile` produces standalone `ar` / `agentrun` binaries for Linux, macOS and Windows (x86_64 + arm64) with warm-start caching under `~/.agentrun/cache/`.

## Installation

Expand Down
2 changes: 1 addition & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Agent)**:一种由平台托管、用户只需声明配置、无需编写或
- **多种输出格式** — 默认 `json`,支持 `table` / `yaml` / `quiet`(适合 shell 管道)。
- **对 Agent 友好** — 默认 JSON 输出、确定性退出码、非 TTY 下不弹交互提示。
- **完整沙箱能力** — 代码执行、文件系统、进程管理、CDP/VNC 浏览器自动化。
- **单文件分发** — PyInstaller 产出 Linux / macOS / Windows(x86_64 + arm64)上的独立 `ar` / `agentrun` 二进制。
- **单文件分发** — 基于 Nuitka `--onefile` 产出 Linux / macOS / Windows(x86_64 + arm64)上的独立 `ar` / `agentrun` 二进制,热启动复用 `~/.agentrun/cache/` 缓存

## 安装

Expand Down
89 changes: 0 additions & 89 deletions agentrun.spec

This file was deleted.

8 changes: 8 additions & 0 deletions docs/en/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ make build # local binary → dist/agentrun
After installation, both `ar` and `agentrun` are available as entry points and behave
identically. `ar` is shorter; the examples in this manual use it.

### Binary startup cache

The prebuilt binary is a Nuitka `--onefile` executable. On first launch it extracts its payload into `~/.agentrun/cache/agentrun-<version>/` (about 20 MB); subsequent launches reuse the cache, bringing warm start-up below 300 ms.

- **Safe to delete.** Remove `~/.agentrun/cache/` at any time; the next invocation re-extracts.
- **Upgrades.** A new binary version writes to a new subdirectory; old ones stay until you clean them up.
- **Read-only `$HOME`.** If `~/.agentrun/cache/` is not writable, the bootstrap falls back to `$TMPDIR` with full re-extraction on every run (~2 s). Either grant write access or run from a shell where `$HOME` points somewhere writable.

## Authentication

The CLI resolves credentials from three sources, in this order:
Expand Down
8 changes: 8 additions & 0 deletions docs/zh/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ make build # 本地打独立二进制 → dist/agentrun
安装完成后 `ar` 和 `agentrun` 都是入口点,行为完全一致。`ar` 更短,文档里的示例
默认用 `ar`。

### 二进制启动缓存

预编译二进制是一个 Nuitka `--onefile` 可执行文件。首次运行会把内置 payload 解压到 `~/.agentrun/cache/agentrun-<version>/`(约 20 MB),之后每次启动复用缓存,热启动耗时低于 300 ms。

- **可以随时删除。**`~/.agentrun/cache/` 删掉后下一次运行自动重建。
- **升级行为。**新版本二进制会写入新的子目录,老目录保留直到手动清理。
- **`$HOME` 只读的情况。**若 `~/.agentrun/cache/` 不可写,bootstrap 会回落到 `$TMPDIR` 且每次都完整解压(~2 秒)。请确保目录可写,或从 `$HOME` 指向可写位置的 shell 启动。

## 认证

CLI 按以下顺序解析凭证(上面优先):
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Issues = "https://github.com/Serverless-Devs/agentrun-cli/issues"
dev = [
"pytest>=8.0.0", "pytest-cov>=6.0.0",
"pytest-asyncio>=1.2.0",
"pyinstaller>=6.0.0",
"nuitka>=2.4",
"zstandard>=0.22",
"ruff>=0.14.0",
"mypy>=1.11.0",
"types-PyYAML>=6.0",
Expand All @@ -48,7 +49,8 @@ dev = [
dev = [
"pytest>=8.0.0", "pytest-cov>=6.0.0",
"pytest-asyncio>=1.2.0",
"pyinstaller>=6.0.0",
"nuitka>=2.4",
"zstandard>=0.22",
"ruff>=0.14.0",
"mypy>=1.11.0",
"types-PyYAML>=6.0",
Expand Down
49 changes: 49 additions & 0 deletions scripts/bench-startup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Measure agentrun binary cold-start + warm-start times.
#
# Usage: bash scripts/bench-startup.sh [binary-path]
# Default binary: ./dist/agentrun
#
# Prints per-run `real` time and the median of runs 2..10 (warm runs).

set -euo pipefail

BINARY="${1:-./dist/agentrun}"
if [ ! -x "$BINARY" ]; then
echo "Binary not found or not executable: $BINARY" >&2
exit 2
fi

echo "=== Benchmark: $BINARY --help ==="
echo "(run 1 = cold; runs 2..10 = warm)"
echo ""

# Portable high-resolution timer. macOS BSD `date` lacks %N, so use python3
# (required anyway as a build dep). Returns seconds as a float.
now() { python3 -c 'import time; print(time.perf_counter())'; }

TIMES=()
for i in $(seq 1 10); do
START=$(now)
"$BINARY" --help >/dev/null
END=$(now)
ELAPSED=$(awk "BEGIN{print $END - $START}")
TIMES+=("$ELAPSED")
printf "Run %2d: %.3f s\n" "$i" "$ELAPSED"
done

# Stats over runs 2..10 (warm). Report min as well as median: on dev
# machines with on-access AV scanners attached to unsigned binaries the
# scanner adds ~10s to some runs, polluting the median. `min` reflects
# Nuitka's true warm-start cost when the scan is cached; `median` is the
# authoritative figure on clean environments (Linux CI, signed releases).
WARM=("${TIMES[@]:1}")
SORTED=$(printf '%s\n' "${WARM[@]}" | sort -n)
N=$(echo "$SORTED" | wc -l | tr -d ' ')
MID=$(( (N + 1) / 2 ))
MIN=$(echo "$SORTED" | sed -n '1p')
MEDIAN=$(echo "$SORTED" | sed -n "${MID}p")

echo ""
printf "Warm min (runs 2..10): %.3f s\n" "$MIN"
printf "Warm median (runs 2..10): %.3f s\n" "$MEDIAN"
Loading
Loading