feat: add source runtime bundle target (#2277)

This commit is contained in:
José Almeida
2026-08-07 12:58:53 +01:00
committed by GitHub
parent 88002a60a3
commit 774ce6d9b4
15 changed files with 699 additions and 42 deletions
@@ -158,6 +158,17 @@ class ReleaseWorkflowContractTest(unittest.TestCase):
self.assertIn("inputs.channel == 'stable'", block)
self.assertNotIn("github.event_name == 'schedule'", block)
def test_platform_archives_are_validated_before_tap_consumers_receive_them(
self,
) -> None:
package = step_block("Package bundle")
validation = "bash scripts/release/package-bundle.sh --bundle-dir dist/kandev"
archive = 'tar -czf "kandev-${{ matrix.platform }}.tar.gz" kandev'
self.assertIn(validation, package)
self.assertIn(archive, package)
self.assertLess(package.index(validation), package.index(archive))
def test_stable_jobs_continue_past_skipped_nightly_branch_only_after_successful_needs(
self,
) -> None:
@@ -446,12 +457,28 @@ class ReleaseWorkflowContractTest(unittest.TestCase):
self.assertIn('"scripts/release/nightly-version.test.mjs"', trigger_block.group(0))
self.assertIn('"scripts/release/nightly-release.sh"', trigger_block.group(0))
self.assertIn('"scripts/release/nightly-release.test.mjs"', trigger_block.group(0))
self.assertIn('"Makefile"', trigger_block.group(0))
self.assertIn('"apps/backend/Makefile"', trigger_block.group(0))
self.assertIn('"scripts/release/package-bundle.sh"', trigger_block.group(0))
self.assertIn('"scripts/release/runtime-bundle.test.sh"', trigger_block.group(0))
self.assertIn(
'"scripts/release/validate-darwin-arm64-helper.mjs"',
trigger_block.group(0),
)
setup_node = (
"uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6"
)
self.assertIn(setup_node, LINT_WORKFLOW)
self.assertIn('node-version: "24"', LINT_WORKFLOW)
self.assertIn(
"run: bash scripts/release/runtime-bundle.test.sh",
LINT_WORKFLOW,
)
self.assertLess(
LINT_WORKFLOW.index(setup_node),
LINT_WORKFLOW.index("- name: Test runtime bundle contract"),
)
self.assertLess(
LINT_WORKFLOW.index(setup_node),
LINT_WORKFLOW.index("- name: Test npm release helpers"),
+13
View File
@@ -12,6 +12,11 @@ on:
- ".github/scripts/release-workflow-contract_test.py"
- ".github/release-signing-key.asc"
- "docs/public/release-process.md"
- "Makefile"
- "apps/backend/Makefile"
- "scripts/release/package-bundle.sh"
- "scripts/release/runtime-bundle.test.sh"
- "scripts/release/validate-darwin-arm64-helper.mjs"
- "scripts/release/npm-packages.sh"
- "scripts/release/nightly-version.mjs"
- "scripts/release/nightly-version.test.mjs"
@@ -33,6 +38,11 @@ on:
- ".github/scripts/release-workflow-contract_test.py"
- ".github/release-signing-key.asc"
- "docs/public/release-process.md"
- "Makefile"
- "apps/backend/Makefile"
- "scripts/release/package-bundle.sh"
- "scripts/release/runtime-bundle.test.sh"
- "scripts/release/validate-darwin-arm64-helper.mjs"
- "scripts/release/npm-packages.sh"
- "scripts/release/nightly-version.mjs"
- "scripts/release/nightly-version.test.mjs"
@@ -77,6 +87,9 @@ jobs:
with:
node-version: "24"
- name: Test runtime bundle contract
run: bash scripts/release/runtime-bundle.test.sh
- name: Test npm release helpers
run: node --test scripts/release/nightly-version.test.mjs scripts/release/nightly-release.test.mjs scripts/release/npm-view-version.test.mjs scripts/release/publish-npm.test.mjs
+1
View File
@@ -809,6 +809,7 @@ jobs:
cp apps/backend/bin/agentctl-linux-arm64 dist/kandev/bin/
cp apps/backend/bin/agentctl-darwin-arm64 dist/kandev/bin/
cp apps/backend/bin/agentctl-darwin-amd64 dist/kandev/bin/
bash scripts/release/package-bundle.sh --bundle-dir dist/kandev
cd dist
tar -czf "kandev-${{ matrix.platform }}.tar.gz" kandev
shasum -a 256 "kandev-${{ matrix.platform }}.tar.gz" > "kandev-${{ matrix.platform }}.tar.gz.sha256"
+40 -16
View File
@@ -11,6 +11,7 @@ EMBEDDED_WEB_DIR := $(BACKEND_DIR)/internal/webapp/embedded/generated
# Tools
PNPM := pnpm
GOFLAGS ?= -v
MAKE := make
# Cross-platform commands
@@ -46,10 +47,12 @@ MAGENTA := \033[35m
VERBOSE ?= 0
NODE ?= $(shell command -v node $(NULL_REDIR) || echo node)
SERVICE_LAUNCHER := $(CURDIR)/dist/kandev/bin/kandev
SERVICE_BUNDLE_DIR := $(CURDIR)/dist/kandev
SERVICE_VERSION := $(shell git rev-parse --short HEAD $(NULL_REDIR) || echo dev)
SERVICE_ENV := KANDEV_BUNDLE_DIR="$(SERVICE_BUNDLE_DIR)" KANDEV_VERSION="$(SERVICE_VERSION)"
RUNTIME_BUNDLE_DIR ?= $(CURDIR)/dist/kandev
RUNTIME_VERSION ?= $(shell git describe --tags --always --dirty $(NULL_REDIR) || echo dev)
SERVICE_BUNDLE_DIR ?= $(CURDIR)/dist/kandev
SERVICE_LAUNCHER = $(SERVICE_BUNDLE_DIR)/bin/kandev
SERVICE_VERSION ?= $(RUNTIME_VERSION)
SERVICE_ENV = KANDEV_BUNDLE_DIR="$(SERVICE_BUNDLE_DIR)" KANDEV_VERSION="$(SERVICE_VERSION)"
SERVICE_PORT_FLAG := $(if $(PORT),--port $(PORT),)
SERVICE_HOME_DIR_FLAG := $(if $(HOME_DIR),--home-dir "$(HOME_DIR)",)
SERVICE_NO_BOOT_START_FLAG := $(if $(filter 1 true yes,$(NO_BOOT_START)),--no-boot-start,)
@@ -111,6 +114,7 @@ help:
@echo " build Build backend and web app"
@echo " build-backend Build backend binary"
@echo " build-web Build web app for production"
@echo " runtime-bundle Build the package-manager runtime bundle (deps must exist)"
@echo " desktop-runtime Build/copy runtime resources for the macOS desktop app"
@echo " desktop-build Build the macOS Tauri app bundle/DMG"
@echo " desktop-open Build and open the macOS app"
@@ -310,22 +314,41 @@ start-windows-debug:
# Service
#
.PHONY: service-bundle
service-bundle: install build
$(call phase,Packaging Service Bundle)
@test -n "$(SERVICE_BUNDLE_DIR)" || { echo "SERVICE_BUNDLE_DIR is empty; aborting."; exit 1; }
@test "$(SERVICE_BUNDLE_DIR)" != "/" || { echo "SERVICE_BUNDLE_DIR must not be /; aborting."; exit 1; }
@$(MAKE) -C $(BACKEND_DIR) build-agentctl-remote
@$(RMDIR) "$(SERVICE_BUNDLE_DIR)/bin"
@mkdir -p "$(SERVICE_BUNDLE_DIR)/bin"
@cp "$(BACKEND_DIR)/bin/kandev" "$(BACKEND_DIR)/bin/agentctl" \
.PHONY: runtime-bundle
runtime-bundle:
$(call phase,Packaging Runtime Bundle)
@test -n "$(RUNTIME_BUNDLE_DIR)" || { echo "RUNTIME_BUNDLE_DIR is empty; aborting."; exit 1; }
@test "$(RUNTIME_BUNDLE_DIR)" != "/" || { echo "RUNTIME_BUNDLE_DIR must not be /; aborting."; exit 1; }
@$(MAKE) -s build-web
@$(MAKE) -s sync-embedded-web
@$(MAKE) -C $(BACKEND_DIR) build-runtime VERSION="$(RUNTIME_VERSION)" GOFLAGS="$(GOFLAGS)"
@set -eu; \
requested_bundle_dir="$(RUNTIME_BUNDLE_DIR)"; \
mkdir -p "$$requested_bundle_dir"; \
resolved_bundle_dir="$$(cd "$$requested_bundle_dir" && pwd -P)"; \
test -n "$$resolved_bundle_dir" || { echo "RUNTIME_BUNDLE_DIR could not be resolved; aborting."; exit 1; }; \
test "$$resolved_bundle_dir" != "/" || { echo "RUNTIME_BUNDLE_DIR must not resolve to /; aborting."; exit 1; }; \
staging_bundle_dir="$$(mktemp -d "$$resolved_bundle_dir/.runtime-bundle.XXXXXX")"; \
trap 'rm -rf "$$staging_bundle_dir"' EXIT; \
mkdir -p "$$staging_bundle_dir/bin"; \
cp "$(BACKEND_DIR)/bin/kandev" "$(BACKEND_DIR)/bin/agentctl" \
"$(BACKEND_DIR)/bin/agentctl-linux-amd64" \
"$(BACKEND_DIR)/bin/agentctl-linux-arm64" \
"$(BACKEND_DIR)/bin/agentctl-darwin-arm64" \
"$(BACKEND_DIR)/bin/agentctl-darwin-amd64" \
"$(SERVICE_BUNDLE_DIR)/bin/"
@scripts/release/package-bundle.sh
$(call success,Service bundle packaged at $(SERVICE_BUNDLE_DIR))
"$$staging_bundle_dir/bin/"; \
scripts/release/package-bundle.sh --bundle-dir "$$staging_bundle_dir"; \
rm -rf "$$resolved_bundle_dir/bin"; \
mv "$$staging_bundle_dir/bin" "$$resolved_bundle_dir/bin"; \
rmdir "$$staging_bundle_dir"; \
trap - EXIT
$(call success,Runtime bundle packaged at $(RUNTIME_BUNDLE_DIR))
.PHONY: service-bundle
service-bundle: install
@$(MAKE) -s runtime-bundle \
RUNTIME_BUNDLE_DIR="$(SERVICE_BUNDLE_DIR)" \
RUNTIME_VERSION="$(SERVICE_VERSION)"
.PHONY: service-cli-check
service-cli-check:
@@ -512,6 +535,7 @@ test-scripts:
@python3 scripts/lint-harness-files.test.py
@python3 scripts/lint-architecture.test.py
@bash scripts/release-desktop.test.sh
@bash scripts/release/runtime-bundle.test.sh
@node --test apps/desktop/e2e/desktop-launch-smoke.test.mjs
@python3 .github/scripts/release-workflow-contract_test.py
@node --test scripts/release/nightly-version.test.mjs scripts/release/nightly-release.test.mjs scripts/release/npm-view-version.test.mjs scripts/release/publish-npm.test.mjs
+8 -5
View File
@@ -133,7 +133,7 @@ LDFLAGS += -X main.Version=$(VERSION) -X main.Commit=$(COMMIT) -X main.BuildTime
DEADCODE_VERSION ?= v0.48.0
DEADCODE_GO_TOOLCHAIN ?= go1.26.0
.PHONY: all build build-all build-agentctl build-agentctl-remote build-agentctl-linux build-agentctl-linux-arm64 build-agentctl-darwin-arm64 build-agentctl-darwin-amd64 build-acpdbg acpdbg build-mock-agent build-mock-agent-linux build-preview build-winjob e2e-plugin-package clean run dev start-debug test test-e2e test-sprites-e2e test-lifecycle-goleak lint deadcode fmt vet check-make-shells help
.PHONY: all build build-runtime build-all build-agentctl build-agentctl-remote build-agentctl-linux build-agentctl-linux-arm64 build-agentctl-darwin-arm64 build-agentctl-darwin-amd64 build-acpdbg acpdbg build-mock-agent build-mock-agent-linux build-preview build-winjob e2e-plugin-package clean run dev start-debug test test-e2e test-sprites-e2e test-lifecycle-goleak lint deadcode fmt vet check-make-shells help
## Default target
all: build
@@ -169,10 +169,13 @@ build-acpdbg:
-@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/acpdbg$(EXE) ./cmd/acpdbg
## Build the unified kandev binary. Always includes remote agentctl helpers
## because SSH/Docker/Sprites executors upload or bind-mount platform-specific
## sidecars into remote environments.
build: build-agentctl build-agentctl-remote build-mock-agent build-acpdbg build-winjob
## Build the complete development binary set.
build: build-runtime build-mock-agent build-acpdbg build-winjob
## Build only the binaries shipped in a runtime bundle. Always includes remote
## agentctl helpers because SSH/Docker/Sprites executors upload or bind-mount
## platform-specific sidecars into remote environments.
build-runtime: build-agentctl build-agentctl-remote
@echo "Building $(BINARY_NAME)..."
-@$(MKDIR) $(BUILD_DIR)
$(CGO_PREFIX) $(GO) build $(GOFLAGS) -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)$(EXE) ./cmd/kandev
+15 -2
View File
@@ -360,6 +360,19 @@ describe("release desktop artifacts", () => {
expect(homebrewScript).not.toContain("kandev-desktop-");
});
it("keeps the generated tap formula on the installed runtime smoke contract", () => {
const formula = readRepoFile("scripts/release/kandev.rb");
expect(formula).toContain(
'assert_equal "v#{version}", shell_output("#{bin}/kandev --version").strip',
);
expect(formula).toContain('spawn bin/"kandev", "--headless", "--port", port.to_s');
expect(formula).toContain("/health");
expect(formula).toContain('"status":"ok"');
expect(formula).toContain("<title>Kandev</title>");
expect(formula).not.toContain('version "__VERSION__"');
});
it("bumps desktop package and Tauri versions during release preparation", () => {
const workflow = releaseWorkflow();
@@ -457,8 +470,8 @@ describe("release desktop artifacts", () => {
expect(workflow).toContain("persist-credentials: false");
expect(workflow).toContain("Desktop validation summary");
expect(workflow).toContain("No release PR, tag, GitHub release, public container tags");
expect(workflow).toContain(
"if: ${{ github.event_name == 'workflow_dispatch' && !inputs.dry_run && !inputs.desktop_validation_only }}",
expect(workflow).toMatch(
/github\.event_name == 'workflow_dispatch' &&\s+inputs\.channel == 'stable' &&\s+!inputs\.dry_run &&\s+!inputs\.desktop_validation_only/,
);
expect(workflow).toContain('if [ "$DESKTOP_VALIDATION_ONLY" = "true" ]; then');
expect(workflow).toContain("scripts/release/desktop-signing-ready.sh macos");
@@ -0,0 +1,45 @@
# ADR-2026-08-05-homebrew-remote-helper-audit: Preserve Remote Helpers in Homebrew Installs
**Status:** accepted
**Date:** 2026-08-05
**Area:** infra, workflow
## Context
Kandev release bundles contain the host launcher and `agentctl` plus four platform-specific
`agentctl` helpers used by Docker and SSH executors. Homebrew audits every binary installed in a
formula prefix and rejects helpers whose CPU architecture differs from the installation host.
Removing those helpers would make a Homebrew installation pass audit while silently breaking
remote execution on the omitted platforms.
## Decision
The `kdlbs/homebrew-kandev` tap preserves the complete validated release bundle. The tap owns an
`audit_exceptions/mismatched_binary_allowlist.json` entry for `kandev` that names only these paths:
- `libexec/bin/agentctl-darwin-amd64`
- `libexec/bin/agentctl-darwin-arm64`
- `libexec/bin/agentctl-linux-amd64`
- `libexec/bin/agentctl-linux-arm64`
The exception is not a wildcard and does not cover the host `kandev` or `agentctl` binaries. Tap CI
must continue running Homebrew's formula audit on macOS and Linux so path or bundle changes fail
before release publication.
## Consequences
Homebrew installations retain Docker and SSH support across the release bundle's supported remote
platforms. The tap uses Homebrew's supported, path-scoped audit mechanism for intentional foreign
binaries. Any helper rename, relocation, addition, or removal requires coordinated updates to the
release-bundle contract, the tap allowlist, and tap CI.
## Alternatives Considered
- Prune helpers that do not match the installation host. Rejected because remote targets need not
match the host and Docker commonly needs a Linux helper on macOS.
- Compress helpers and materialize them at runtime. Rejected because it adds mutable runtime-cache
behavior and failure modes solely to satisfy an audit that already supports scoped exceptions.
- Download helpers lazily. Rejected because it reintroduces runtime self-downloads and makes an
installed package incomplete offline.
- Split helpers into separate formulae. Rejected because it complicates installation and release
synchronization without improving runtime behavior.
+1
View File
@@ -126,3 +126,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
| 2026-08-03-scope-and-merge-repository-secrets | [Scope and Merge Repository Secrets](2026-08-03-scope-and-merge-repository-secrets.md) | accepted | backend, frontend, security, protocol | 2026-08-03 |
| 2026-08-05-server-owned-quick-terminal-descriptors | [Server-Owned Quick Terminal Descriptors](2026-08-05-server-owned-quick-terminal-descriptors.md) | accepted | backend, frontend, protocol, security | 2026-08-05 |
| 2026-08-05-nested-submodules-as-repository-scopes | [Model Nested Submodules as Repository Scopes](2026-08-05-nested-submodules-as-repository-scopes.md) | accepted | backend, frontend, protocol | 2026-08-05 |
| 2026-08-05-homebrew-remote-helper-audit | [Preserve Remote Helpers in Homebrew Installs](2026-08-05-homebrew-remote-helper-audit.md) | accepted | infra, workflow | 2026-08-05 |
+1 -1
View File
@@ -215,7 +215,7 @@ System pages (Radarr/Sonarr-style) for status, disk usage, database maintenance,
| [create-local-repository](create-local-repository/spec.md) | shipped |
| [workflow-cycle-guardrails](workflow-cycle-guardrails/spec.md) | building |
| [improve-kandev](improve-kandev/spec.md) | building |
| [homebrew-core](homebrew-core/spec.md) | draft |
| [homebrew-core](homebrew-core/spec.md) | building |
| [native-kandev-cli](native-kandev-cli/spec.md) | draft |
| [desktop-tauri-app](desktop-tauri-app/spec.md) | shipped |
| [lsp-file-intelligence](lsp-file-intelligence/spec.md) | building |
+27 -10
View File
@@ -1,5 +1,5 @@
---
status: draft
status: building
created: 2026-05-14
owner: tbd
---
@@ -16,24 +16,41 @@ Kandev is currently installable via `brew install kdlbs/kandev/kandev` from the
## What
- Author a homebrew-core-compliant `Formula/kandev.rb` that **builds entirely from source** — native Go `kandev` launcher/backend binary (cgo + sqlite via `mattn/go-sqlite3`), agentctl helper binaries, and the Vite web bundle embedded into the Go binary.
- Source comes from the GitHub-generated tag archive (`https://github.com/kdlbs/kandev/archive/refs/tags/vX.Y.Z.tar.gz`); per-release sha256 is captured at submission/bump time.
- Build deps: `go => :build`, `pnpm => :build`. Runtime dep: none for Node; `uses_from_macos "sqlite"` (cgo sqlite3 link) ships with macOS and is only installed via Homebrew on Linux.
- Install layout: `libexec/bin` plus a single `bin/kandev` wrapper produced by `write_env_script`, setting `KANDEV_BUNDLE_DIR=<libexec>` and `KANDEV_VERSION=<version>`. The native launcher uses these values to find helper binaries and log the installed release version.
- `livecheck do; url :stable; regex(/^v?(\d+(?:\.\d+)+)$/i); end` — Git strategy (the default for `url :stable`) checks tags directly and avoids the GitHub API rate limit.
- Test block: spawns `libexec/bin/kandev` with an isolated `KANDEV_HOME_DIR` and random `KANDEV_SERVER_PORT`, polls `/api/v1/system/health` until the response contains `"healthy"` (60s timeout), then shuts the backend down. Also asserts the formula `version` appears in `bin/kandev --version`.
- Tap (`kdlbs/homebrew-kandev`) and its update script (`scripts/release/update-homebrew-tap.sh`) stay untouched — it remains the binary-install fast path; the homebrew-core formula is a parallel, source-built distribution.
- Add a package-manager build contract that produces the native runtime bundle from an already-installed source checkout. It builds the Vite SPA, syncs it into the Go embed input, compiles the host `kandev` and `agentctl` binaries, and cross-compiles the four Linux/macOS remote `agentctl` helpers.
- Keep package-manager dependency installation outside that contract. Homebrew declares Go, Node 24, and pnpm as build dependencies and Git as a runtime facility.
- Compile only the host `kandev` binary with cgo and the `fts5` tag. `mattn/go-sqlite3` supplies the SQLite amalgamation, so the formula does not declare an external SQLite dependency unless build or linkage evidence requires it.
- Merge and publish the source-build contract in a Stable Kandev release before authoring the Core formula. Core must consume the immutable GitHub tag archive for that release, never a branch or unreleased commit.
- Install the bundle under `libexec/bin` and expose one `bin/kandev` wrapper produced by `write_env_script`. The wrapper sets `KANDEV_BUNDLE_DIR=<libexec>` and `KANDEV_VERSION=<version>`.
- Use stable numeric tags for `livecheck`; prerelease and Nightly npm versions are not Homebrew channels.
- Keep `kdlbs/homebrew-kandev` as the upstream binary fast path alongside Homebrew Core's source-built bottles. The shared release-bundle validator must run before those binary archives are published, and the generated tap formula must smoke-test its version, readiness endpoint, and embedded SPA.
- Preserve all four remote `agentctl` helpers in custom-tap installations. The tap must use an exact-path Homebrew mismatched-binary audit allowlist rather than pruning helpers, so Docker and SSH targets can differ from the Homebrew host. Decision: [ADR-2026-08-05-homebrew-remote-helper-audit](../../decisions/2026-08-05-homebrew-remote-helper-audit.md).
The runtime bundle contains exactly:
- host `kandev`;
- host `agentctl`;
- `agentctl-linux-amd64`;
- `agentctl-linux-arm64`;
- `agentctl-darwin-arm64`;
- `agentctl-darwin-amd64`.
The Darwin arm64 helper must carry a Mach-O code signature so Apple Silicon can execute it.
## Scenarios
- **GIVEN** the homebrew-core PR is merged, **WHEN** a macOS user runs `brew install kandev`, **THEN** Homebrew downloads the source tarball, builds the Vite web assets, syncs them into the Go embed directory, compiles the Go binaries, installs them under `Cellar/kandev/X.Y.Z/{bin,libexec}`, and `kandev --help` prints "kandev launcher".
- **GIVEN** the source-build contract is merged, **WHEN** a Stable release is published, **THEN** its immutable tag archive contains the contract used by the Homebrew formula.
- **GIVEN** the homebrew-core PR is merged, **WHEN** a user runs `brew install kandev`, **THEN** Homebrew builds the Vite assets and native runtime bundle from source, installs it under `Cellar/kandev/X.Y.Z/{bin,libexec}`, and `kandev --version` prints `X.Y.Z` without Node being present at runtime.
- **GIVEN** the installed formula starts with an isolated home directory and loopback port, **WHEN** `brew test kandev` polls `GET /health`, **THEN** readiness returns `{"status":"ok"}` and `/` serves the embedded page containing `<title>Kandev</title>`.
- **GIVEN** a new kandev release `vX.Y.Z` is tagged, **WHEN** Homebrew's auto-bump worker runs, **THEN** `livecheck` resolves the new tag from GitHub Releases and a bump PR is opened against the formula.
- **GIVEN** a maintainer reviews the PR, **WHEN** they run `brew install --build-from-source kandev` locally, **THEN** the build completes without network or sandbox failures and `brew test kandev` passes.
- **GIVEN** a Stable release updates `kdlbs/homebrew-kandev`, **WHEN** its platform archive is built and the tap formula is tested, **THEN** the archive contains the complete executable runtime and the installed launcher serves both `/health` and the embedded Kandev page.
- **GIVEN** a tap archive contains remote helpers for CPU architectures other than the Homebrew host, **WHEN** Homebrew audits the installed formula on macOS or Linux, **THEN** only the four declared remote-helper paths are exempted and the complete runtime remains installed.
## Out of scope
- Migrating users from `kdlbs/homebrew-kandev` to homebrew-core (both can coexist; users opt in by switching tap reference).
- Linuxbrew bottle parity beyond what homebrew-core's CI provides by default.
- Vendoring JS dependencies via `resource` blocks — falls back here only if maintainers reject network-during-install.
- Changes to `.github/workflows/release.yml` or `scripts/release/update-homebrew-tap.sh`.
- Changing the custom tap's direct-push publication or deploy-key authentication model.
- Advertising untapped `brew install kandev` before the Core formula merges.
- Notability lobbying — submission goes in as-is; maintainers decide.
+15 -2
View File
@@ -4,7 +4,6 @@ class Kandev < Formula
desc "Manage tasks, orchestrate agents, review changes, and ship value"
homepage "https://github.com/kdlbs/kandev"
license "AGPL-3.0-only"
version "__VERSION__"
on_macos do
if Hardware::CPU.arm?
@@ -37,6 +36,20 @@ class Kandev < Formula
end
test do
assert_match "kandev launcher", shell_output("#{bin}/kandev --help")
assert_equal "v#{version}", shell_output("#{bin}/kandev --version").strip
ENV["KANDEV_HOME_DIR"] = testpath.to_s
ENV["KANDEV_DATABASE_PATH"] = (testpath/"kandev.db").to_s
ENV["KANDEV_SERVER_HOST"] = "127.0.0.1"
port = free_port
pid = spawn bin/"kandev", "--headless", "--port", port.to_s
health_url = "http://127.0.0.1:#{port}/health"
curl = "curl --silent --show-error --fail --retry 30 --retry-connrefused --retry-delay 1"
health = shell_output("#{curl} #{health_url}")
assert_match '"status":"ok"', health
assert_match "<title>Kandev</title>", shell_output("#{curl} http://127.0.0.1:#{port}/")
ensure
Process.kill("TERM", pid) if pid
Process.wait(pid) if pid
end
end
+73 -5
View File
@@ -1,13 +1,15 @@
#!/usr/bin/env bash
# Finalize the dist/kandev/ release layout from already-built pieces.
# Validate a release layout assembled from already-built pieces.
# Usage: package-bundle.sh [--bundle-dir DIR]
# Caller must have run, in this order:
# - Vite assets synced into apps/backend/internal/webapp/embedded/generated
# - go build ./cmd/{kandev,agentctl} plus remote agentctl helpers into dist/kandev/bin/...
# After this: dist/kandev/bin is ready to install or tar.
# - go build ./cmd/{kandev,agentctl} plus remote agentctl helpers into the bundle's bin directory
# The default bundle is dist/kandev; package managers may select another path.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BUNDLE="$ROOT_DIR/dist/kandev"
DARWIN_HELPER_VALIDATOR="$ROOT_DIR/scripts/release/validate-darwin-arm64-helper.mjs"
REMOTE_AGENTCTL_HELPERS=(
agentctl-linux-amd64
agentctl-linux-arm64
@@ -15,21 +17,87 @@ REMOTE_AGENTCTL_HELPERS=(
agentctl-darwin-amd64
)
if [ ! -f "$BUNDLE/bin/kandev" ] && [ ! -f "$BUNDLE/bin/kandev.exe" ]; then
usage() {
echo "usage: $0 [--bundle-dir DIR]" >&2
}
case "$#" in
0) ;;
2)
if [ "$1" != "--bundle-dir" ] || [ -z "$2" ]; then
usage
exit 2
fi
BUNDLE="$2"
;;
*)
usage
exit 2
;;
esac
if [ "$BUNDLE" = "/" ]; then
echo "bundle directory must not be /" >&2
exit 2
fi
launcher="kandev"
if [ ! -f "$BUNDLE/bin/$launcher" ] && [ -f "$BUNDLE/bin/kandev.exe" ]; then
launcher="kandev.exe"
fi
if [ ! -f "$BUNDLE/bin/$launcher" ]; then
echo "Missing native launcher in $BUNDLE/bin; build cmd/kandev first" >&2
exit 1
fi
if [ ! -x "$BUNDLE/bin/$launcher" ]; then
echo "Runtime binary $launcher is not executable in $BUNDLE/bin" >&2
exit 1
fi
if [ ! -f "$BUNDLE/bin/agentctl" ] && [ ! -f "$BUNDLE/bin/agentctl.exe" ]; then
agentctl="agentctl"
if [ ! -f "$BUNDLE/bin/$agentctl" ] && [ -f "$BUNDLE/bin/agentctl.exe" ]; then
agentctl="agentctl.exe"
fi
if [ ! -f "$BUNDLE/bin/$agentctl" ]; then
echo "Missing agentctl in $BUNDLE/bin; build cmd/agentctl first" >&2
exit 1
fi
if [ ! -x "$BUNDLE/bin/$agentctl" ]; then
echo "Runtime binary $agentctl is not executable in $BUNDLE/bin" >&2
exit 1
fi
for helper in "${REMOTE_AGENTCTL_HELPERS[@]}"; do
if [ ! -f "$BUNDLE/bin/$helper" ]; then
echo "Missing remote agentctl helper $helper in $BUNDLE/bin; run make -C apps/backend build-agentctl-remote first" >&2
exit 1
fi
if [ ! -x "$BUNDLE/bin/$helper" ]; then
echo "Runtime binary $helper is not executable in $BUNDLE/bin" >&2
exit 1
fi
if [ "$helper" = "agentctl-darwin-arm64" ]; then
if ! command -v node >/dev/null 2>&1; then
echo "Node.js is required to validate $helper" >&2
exit 1
fi
node "$DARWIN_HELPER_VALIDATOR" "$BUNDLE/bin/$helper"
fi
done
while IFS= read -r -d '' entry; do
artifact="${entry##*/}"
expected=false
for required in "$launcher" "$agentctl" "${REMOTE_AGENTCTL_HELPERS[@]}"; do
if [ "$artifact" = "$required" ]; then
expected=true
break
fi
done
if [ "$expected" != true ]; then
echo "Unexpected runtime artifact $artifact in $BUNDLE/bin" >&2
exit 1
fi
done < <(find "$BUNDLE/bin" -mindepth 1 -maxdepth 1 -print0)
echo "Bundle assembled at $BUNDLE"
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PACKAGE_SCRIPT="$ROOT_DIR/scripts/release/package-bundle.sh"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
write_macho() {
local path="$1"
local signature="$2"
node - "$path" "$signature" <<'NODE'
const fs = require("node:fs");
const path = process.argv[2];
const signature = process.argv[3];
const headerSize = 32;
const loadCommandSize = 16;
const codeDirectorySize = 83;
const superBlobSize = 20 + codeDirectorySize;
const signatureOffset = headerSize + loadCommandSize;
const data = Buffer.alloc(signatureOffset + (signature === "signed" ? superBlobSize : 0));
data.writeUInt32LE(0xfeedfacf, 0);
data.writeUInt32LE(0x0100000c, 4);
data.writeUInt32LE(1, 16);
data.writeUInt32LE(loadCommandSize, 20);
data.writeUInt32LE(signature === "unsigned" ? 0x1b : 0x1d, headerSize);
data.writeUInt32LE(loadCommandSize, headerSize + 4);
if (signature !== "unsigned") {
data.writeUInt32LE(signatureOffset, headerSize + 8);
data.writeUInt32LE(superBlobSize, headerSize + 12);
}
if (signature === "signed") {
data.writeUInt32BE(0xfade0cc0, signatureOffset);
data.writeUInt32BE(superBlobSize, signatureOffset + 4);
data.writeUInt32BE(1, signatureOffset + 8);
data.writeUInt32BE(0, signatureOffset + 12);
data.writeUInt32BE(20, signatureOffset + 16);
const codeDirectoryOffset = signatureOffset + 20;
data.writeUInt32BE(0xfade0c02, codeDirectoryOffset);
data.writeUInt32BE(codeDirectorySize, codeDirectoryOffset + 4);
data.writeUInt32BE(0x20001, codeDirectoryOffset + 8);
data.writeUInt32BE(0x2, codeDirectoryOffset + 12);
data.writeUInt32BE(51, codeDirectoryOffset + 16);
data.writeUInt32BE(44, codeDirectoryOffset + 20);
data.writeUInt32BE(0, codeDirectoryOffset + 24);
data.writeUInt32BE(1, codeDirectoryOffset + 28);
data.writeUInt32BE(signatureOffset, codeDirectoryOffset + 32);
data.writeUInt8(32, codeDirectoryOffset + 36);
data.writeUInt8(2, codeDirectoryOffset + 37);
data.writeUInt8(12, codeDirectoryOffset + 39);
data.write("kandev\0", codeDirectoryOffset + 44, "utf8");
}
fs.writeFileSync(path, data, { mode: 0o755 });
NODE
}
make_complete_bundle() {
local bundle_dir="$1"
mkdir -p "$bundle_dir/bin"
touch \
"$bundle_dir/bin/kandev" \
"$bundle_dir/bin/agentctl" \
"$bundle_dir/bin/agentctl-linux-amd64" \
"$bundle_dir/bin/agentctl-linux-arm64" \
"$bundle_dir/bin/agentctl-darwin-arm64" \
"$bundle_dir/bin/agentctl-darwin-amd64"
chmod +x "$bundle_dir/bin/"*
write_macho "$bundle_dir/bin/agentctl-darwin-arm64" signed
}
custom_bundle="$TMP_DIR/custom bundle"
make_complete_bundle "$custom_bundle"
if ! bash "$PACKAGE_SCRIPT" --bundle-dir "$custom_bundle" >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator rejected a complete custom bundle: $(cat "$TMP_DIR/err")"
fi
grep -Fq "Bundle assembled at $custom_bundle" "$TMP_DIR/out" ||
fail "validator did not report the custom bundle path"
extra_bundle="$TMP_DIR/extra artifact"
cp -R "$custom_bundle" "$extra_bundle"
touch "$extra_bundle/bin/debugger"
chmod +x "$extra_bundle/bin/debugger"
if bash "$PACKAGE_SCRIPT" --bundle-dir "$extra_bundle" >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator accepted a seventh runtime artifact"
fi
grep -Fq "Unexpected runtime artifact debugger" "$TMP_DIR/err" ||
fail "extra-artifact error was not actionable"
missing_bundle="$TMP_DIR/missing helper"
cp -R "$custom_bundle" "$missing_bundle"
rm "$missing_bundle/bin/agentctl-linux-arm64"
if bash "$PACKAGE_SCRIPT" --bundle-dir "$missing_bundle" >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator accepted a bundle with a missing remote helper"
fi
grep -Fq "Missing remote agentctl helper agentctl-linux-arm64" "$TMP_DIR/err" ||
fail "missing-helper error was not actionable"
non_executable_bundle="$TMP_DIR/non-executable helper"
cp -R "$custom_bundle" "$non_executable_bundle"
chmod -x "$non_executable_bundle/bin/agentctl-darwin-arm64"
if bash "$PACKAGE_SCRIPT" --bundle-dir "$non_executable_bundle" >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator accepted a non-executable runtime binary"
fi
grep -Fq "Runtime binary agentctl-darwin-arm64 is not executable" "$TMP_DIR/err" ||
fail "non-executable-binary error was not actionable"
unsigned_bundle="$TMP_DIR/unsigned darwin helper"
cp -R "$custom_bundle" "$unsigned_bundle"
write_macho "$unsigned_bundle/bin/agentctl-darwin-arm64" unsigned
if bash "$PACKAGE_SCRIPT" --bundle-dir "$unsigned_bundle" >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator accepted an unsigned darwin/arm64 helper"
fi
grep -Fq "agentctl-darwin-arm64 is not code-signed" "$TMP_DIR/err" ||
fail "unsigned-helper error was not actionable"
invalid_signature_bundle="$TMP_DIR/invalid darwin signature"
cp -R "$custom_bundle" "$invalid_signature_bundle"
write_macho "$invalid_signature_bundle/bin/agentctl-darwin-arm64" invalid-signature
if bash "$PACKAGE_SCRIPT" --bundle-dir "$invalid_signature_bundle" >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator accepted a darwin/arm64 helper with missing signature data"
fi
grep -Fq "agentctl-darwin-arm64 does not contain a valid code signature" "$TMP_DIR/err" ||
fail "invalid-signature error was not actionable"
unparsable_bundle="$TMP_DIR/unparsable darwin helper"
cp -R "$custom_bundle" "$unparsable_bundle"
printf 'not a Mach-O\n' > "$unparsable_bundle/bin/agentctl-darwin-arm64"
chmod +x "$unparsable_bundle/bin/agentctl-darwin-arm64"
if bash "$PACKAGE_SCRIPT" --bundle-dir "$unparsable_bundle" >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator accepted an unparsable darwin/arm64 helper"
fi
grep -Fq "agentctl-darwin-arm64 is not a parsable thin darwin/arm64 Mach-O" "$TMP_DIR/err" ||
fail "unparsable-helper error was not actionable"
if bash "$PACKAGE_SCRIPT" --bundle-dir / >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator accepted the filesystem root as a bundle"
fi
grep -Fq "bundle directory must not be /" "$TMP_DIR/err" ||
fail "unsafe-root error was not actionable"
if bash "$PACKAGE_SCRIPT" --unknown >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "validator accepted an unknown argument"
fi
grep -Fq "usage:" "$TMP_DIR/err" || fail "invalid arguments did not print usage"
if ! make --dry-run -C "$ROOT_DIR/apps/backend" build-runtime \
VERSION=1.2.3 GOFLAGS=-trimpath >"$TMP_DIR/backend-dry-run" 2>"$TMP_DIR/err"; then
fail "backend runtime target is unavailable: $(cat "$TMP_DIR/err")"
fi
expected_runtime_binaries="$(printf '%s\n' \
bin/agentctl \
bin/agentctl-darwin-amd64 \
bin/agentctl-darwin-arm64 \
bin/agentctl-linux-amd64 \
bin/agentctl-linux-arm64 \
bin/kandev)"
actual_runtime_binaries="$(grep -oE 'bin/[[:alnum:]_.-]+' "$TMP_DIR/backend-dry-run" | sort -u)"
[ "$actual_runtime_binaries" = "$expected_runtime_binaries" ] ||
fail "backend runtime target built an unexpected binary set: $actual_runtime_binaries"
grep -Fq -- "-tags fts5" "$TMP_DIR/backend-dry-run" ||
fail "backend runtime target omitted the fts5 build tag"
grep -Fq -- "-X main.Version=1.2.3" "$TMP_DIR/backend-dry-run" ||
fail "backend runtime target did not forward VERSION"
runtime_output="$TMP_DIR/runtime output"
if ! make --dry-run -C "$ROOT_DIR" runtime-bundle \
PNPM="pnpm with current" \
GOFLAGS=-trimpath \
RUNTIME_VERSION=1.2.3 \
RUNTIME_BUNDLE_DIR="$runtime_output" \
>"$TMP_DIR/runtime-dry-run" 2>"$TMP_DIR/err"; then
fail "root runtime target is unavailable: $(cat "$TMP_DIR/err")"
fi
grep -Fq "pnpm with current --filter @kandev/web build" "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not build the Vite web package"
grep -Fq "internal/webapp/embedded/generated" "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not sync embedded web assets"
grep -Fq "build-runtime" "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not invoke the minimal backend target"
grep -Fq "VERSION=\"1.2.3\"" "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not forward RUNTIME_VERSION"
grep -Fq "GOFLAGS=\"-trimpath\"" "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not forward GOFLAGS"
grep -Fq -- 'requested_bundle_dir='"\"$runtime_output\"" "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not retain the selected output directory"
grep -Fq -- '--bundle-dir "$staging_bundle_dir"' "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not validate the staged bundle"
grep -Fq 'resolved_bundle_dir=' "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not canonicalize the bundle directory before writing"
grep -Fq 'pwd -P' "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not resolve symlinked bundle directories"
grep -Fq 'mktemp -d "$resolved_bundle_dir/.runtime-bundle.XXXXXX"' "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not create a clean staging bundle"
if grep -Fq "rm -rf \"$runtime_output/bin\"" "$TMP_DIR/runtime-dry-run"; then
fail "runtime target recursively deleted through an unresolved bundle path"
fi
for binary in \
kandev \
agentctl \
agentctl-linux-amd64 \
agentctl-linux-arm64 \
agentctl-darwin-arm64 \
agentctl-darwin-amd64; do
grep -Fq "apps/backend/bin/$binary" "$TMP_DIR/runtime-dry-run" ||
fail "runtime target did not package $binary"
done
if grep -Eq 'pnpm( with current)? .*install|playwright install' "$TMP_DIR/runtime-dry-run"; then
fail "runtime target attempted to install dependencies or Playwright"
fi
runtime_backend="$TMP_DIR/runtime backend"
mkdir -p "$runtime_backend/bin"
for binary in \
kandev \
agentctl \
agentctl-linux-amd64 \
agentctl-linux-arm64 \
agentctl-darwin-arm64 \
agentctl-darwin-amd64; do
printf 'new %s\n' "$binary" > "$runtime_backend/bin/$binary"
chmod +x "$runtime_backend/bin/$binary"
done
write_macho "$runtime_backend/bin/agentctl-darwin-arm64" signed
failed_copy_bundle="$TMP_DIR/failed copy bundle"
make_complete_bundle "$failed_copy_bundle"
cp -R "$failed_copy_bundle/bin" "$TMP_DIR/failed copy snapshot"
rm "$runtime_backend/bin/agentctl-darwin-amd64"
if make -C "$ROOT_DIR" runtime-bundle \
MAKE=true \
BACKEND_DIR="$runtime_backend" \
RUNTIME_VERSION=1.2.3 \
RUNTIME_BUNDLE_DIR="$failed_copy_bundle" \
>"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "runtime target ignored a failed binary copy"
fi
if ! diff -r "$TMP_DIR/failed copy snapshot" "$failed_copy_bundle/bin" >/dev/null; then
fail "failed binary copy changed the previous runtime bundle"
fi
printf 'new agentctl-darwin-amd64\n' > "$runtime_backend/bin/agentctl-darwin-amd64"
chmod +x "$runtime_backend/bin/agentctl-darwin-amd64"
stale_bundle="$TMP_DIR/stale bundle"
make_complete_bundle "$stale_bundle"
touch "$stale_bundle/bin/debugger"
chmod +x "$stale_bundle/bin/debugger"
if ! make -C "$ROOT_DIR" runtime-bundle \
MAKE=true \
BACKEND_DIR="$runtime_backend" \
RUNTIME_VERSION=1.2.3 \
RUNTIME_BUNDLE_DIR="$stale_bundle" \
>"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "repeat runtime bundle failed instead of replacing stale artifacts: $(cat "$TMP_DIR/err")"
fi
[ ! -e "$stale_bundle/bin/debugger" ] ||
fail "repeat runtime bundle retained a stale artifact"
root_symlink="$TMP_DIR/root symlink"
ln -s / "$root_symlink"
if make -C "$ROOT_DIR" runtime-bundle \
MAKE=true \
RUNTIME_BUNDLE_DIR="$root_symlink" \
>"$TMP_DIR/out" 2>"$TMP_DIR/err"; then
fail "runtime target accepted a bundle directory resolving to the filesystem root"
fi
grep -Fq "RUNTIME_BUNDLE_DIR must not resolve to /" "$TMP_DIR/out" ||
fail "symlinked-root error was not actionable"
if ! make --dry-run -C "$ROOT_DIR" service-bundle \
RUNTIME_VERSION=1.2.3 \
SERVICE_BUNDLE_DIR="$TMP_DIR/service bundle" \
>"$TMP_DIR/service-dry-run" 2>"$TMP_DIR/err"; then
fail "service bundle dry run failed: $(cat "$TMP_DIR/err")"
fi
grep -Fq 'RUNTIME_VERSION="1.2.3"' "$TMP_DIR/service-dry-run" ||
fail "service bundle replaced the release SemVer with another version"
echo "PASS: runtime bundle contract"
-1
View File
@@ -118,7 +118,6 @@ mkdir -p "$(dirname "$FORMULA_PATH")"
GITHUB_BASE="https://github.com/kdlbs/kandev/releases/download/${TAG}"
sed \
-e "s|__VERSION__|$VERSION|g" \
-e "s|__GITHUB_BASE__|$GITHUB_BASE|g" \
-e "s|__SHA_MACOS_ARM64__|$SHA_MACOS_ARM64|g" \
-e "s|__SHA_MACOS_X64__|$SHA_MACOS_X64|g" \
@@ -0,0 +1,141 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
const MACHO_MAGIC_64_LE = 0xfeedfacf;
const CPU_TYPE_ARM64 = 0x0100000c;
const LC_CODE_SIGNATURE = 0x1d;
const MACHO_HEADER_SIZE = 32;
const LOAD_COMMAND_HEADER_SIZE = 8;
const LINKEDIT_DATA_COMMAND_SIZE = 16;
const CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0;
const CSMAGIC_CODEDIRECTORY = 0xfade0c02;
const CSSLOT_CODEDIRECTORY = 0;
const CODE_DIRECTORY_BASE_SIZE = 44;
function fail(file, message) {
process.stderr.write(`Runtime binary ${path.basename(file)} ${message}\n`);
process.exit(1);
}
function isValidCodeDirectory(blob, signedDataOffset) {
if (blob.length < CODE_DIRECTORY_BASE_SIZE) return false;
if (blob.readUInt32BE(0) !== CSMAGIC_CODEDIRECTORY) return false;
const length = blob.readUInt32BE(4);
if (length < CODE_DIRECTORY_BASE_SIZE || length > blob.length) return false;
const version = blob.readUInt32BE(8);
const hashOffset = blob.readUInt32BE(16);
const identifierOffset = blob.readUInt32BE(20);
const specialSlotCount = blob.readUInt32BE(24);
const codeSlotCount = blob.readUInt32BE(28);
const codeLimit = blob.readUInt32BE(32);
const hashSize = blob.readUInt8(36);
const hashType = blob.readUInt8(37);
const pageSize = blob.readUInt8(39);
if (version < 0x20000 || hashSize === 0 || hashType === 0 || pageSize > 31) return false;
if (identifierOffset < CODE_DIRECTORY_BASE_SIZE || identifierOffset >= length) return false;
const identifierEnd = blob.indexOf(0, identifierOffset);
if (identifierEnd < identifierOffset || identifierEnd >= length) return false;
if (codeSlotCount === 0 || codeLimit === 0 || codeLimit > signedDataOffset) return false;
const specialHashBytes = specialSlotCount * hashSize;
const codeHashBytes = codeSlotCount * hashSize;
if (hashOffset < CODE_DIRECTORY_BASE_SIZE + specialHashBytes) return false;
if (hashOffset > length || codeHashBytes > length - hashOffset) return false;
return true;
}
function isValidSuperBlob(data, dataOffset, dataSize) {
if (dataSize < 12 || dataOffset > data.length || dataSize > data.length - dataOffset) return false;
const blob = data.subarray(dataOffset, dataOffset + dataSize);
if (blob.readUInt32BE(0) !== CSMAGIC_EMBEDDED_SIGNATURE) return false;
const length = blob.readUInt32BE(4);
const count = blob.readUInt32BE(8);
if (length < 12 || length > blob.length || count > Math.floor((length - 12) / 8)) return false;
const indexEnd = 12 + count * 8;
let hasCodeDirectory = false;
for (let index = 0; index < count; index += 1) {
const entryOffset = 12 + index * 8;
const slotType = blob.readUInt32BE(entryOffset);
const nestedOffset = blob.readUInt32BE(entryOffset + 4);
if (nestedOffset < indexEnd || nestedOffset > length - 8) return false;
const nestedLength = blob.readUInt32BE(nestedOffset + 4);
if (nestedLength < 8 || nestedLength > length - nestedOffset) return false;
if (slotType === CSSLOT_CODEDIRECTORY) {
const nestedBlob = blob.subarray(nestedOffset, nestedOffset + nestedLength);
if (!isValidCodeDirectory(nestedBlob, dataOffset)) return false;
hasCodeDirectory = true;
}
}
return hasCodeDirectory;
}
function codeSignatureStatus(data) {
const commandCount = data.readUInt32LE(16);
const commandBytes = data.readUInt32LE(20);
if (
commandBytes > data.length - MACHO_HEADER_SIZE ||
commandCount > Math.floor(commandBytes / LOAD_COMMAND_HEADER_SIZE)
) {
return "malformed";
}
const commandsEnd = MACHO_HEADER_SIZE + commandBytes;
let offset = MACHO_HEADER_SIZE;
for (let index = 0; index < commandCount; index += 1) {
if (offset + LOAD_COMMAND_HEADER_SIZE > commandsEnd) return "malformed";
const command = data.readUInt32LE(offset);
const commandSize = data.readUInt32LE(offset + 4);
if (commandSize < LOAD_COMMAND_HEADER_SIZE || commandSize > commandsEnd - offset) {
return "malformed";
}
if (command === LC_CODE_SIGNATURE) {
if (commandSize !== LINKEDIT_DATA_COMMAND_SIZE) return "invalid";
const dataOffset = data.readUInt32LE(offset + 8);
const dataSize = data.readUInt32LE(offset + 12);
return isValidSuperBlob(data, dataOffset, dataSize) ? "valid" : "invalid";
}
offset += commandSize;
}
return offset === commandsEnd ? "missing" : "malformed";
}
const file = process.argv[2];
if (!file) {
process.stderr.write(`usage: ${path.basename(process.argv[1])} FILE\n`);
process.exit(2);
}
let data;
try {
data = fs.readFileSync(file);
} catch (error) {
fail(file, `could not be read: ${error.message}`);
}
if (
data.length < MACHO_HEADER_SIZE ||
data.readUInt32LE(0) !== MACHO_MAGIC_64_LE ||
data.readUInt32LE(4) !== CPU_TYPE_ARM64
) {
fail(file, "is not a parsable thin darwin/arm64 Mach-O");
}
const signatureStatus = codeSignatureStatus(data);
if (signatureStatus === "malformed") {
fail(file, "is not a parsable thin darwin/arm64 Mach-O");
}
if (signatureStatus === "invalid") {
fail(file, "does not contain a valid code signature");
}
if (signatureStatus === "missing") {
fail(file, "is not code-signed; Apple Silicon will refuse to run it");
}