feat: add source runtime bundle target (#2277)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Executable
+292
@@ -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"
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user