Skip to content
Merged
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
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

# test, shellcheck, and build run in parallel; all are required for merge
jobs:
test:
name: Test
Expand Down Expand Up @@ -48,11 +49,20 @@ jobs:
run: |
for pkg in Packages/*/; do
if [ -d "$pkg/Tests" ]; then
echo "Testing $pkg..."
echo "==> Testing $(basename "$pkg")..."
swift test --package-path "$pkg"
fi
done

shellcheck:
name: Shell Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Check scripts
run: shellcheck scripts/*.sh

build:
name: Build
runs-on: macos-15
Expand Down
12 changes: 10 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,23 @@ cd crow
make build
```

> Always use `make build` for full builds. It handles submodules, the Ghostty
> framework, and the Swift build. Use `swift build` only for quick iteration
> after the initial build.

**Note:** Code signing is not required for development. Unsigned builds work normally for local testing. Official releases are signed and notarized automatically via GitHub Actions.

### Running Tests

```bash
swift test # or: mise test
make test # Preferred — runs all package tests
swift test # Also works (runs root package tests)
mise test # If using mise
```

Tests use the Swift Testing framework (`@Test` macros).
Tests use the Swift Testing framework (`@Test` macros). Currently, tests exist
in `CrowCore`. When adding new functionality to any package, include a test
target in that package's `Package.swift` and add tests.

## Code Style

Expand Down
18 changes: 11 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ help:
@echo " build Full build: submodules + ghostty + swift build (default)"
@echo " setup Init submodules and check build prerequisites"
@echo " check Verify all build and runtime prerequisites"
@echo " test Run unit tests for CrowCore and CrowPersistence"
@echo " test Run all package tests"
@echo " ghostty Build GhosttyKit framework"
@echo " app Swift build only (debug)"
@echo " release Release build + .app bundle"
Expand Down Expand Up @@ -58,6 +58,16 @@ release: $(XCFW)
sign: release
bash scripts/sign-and-notarize.sh

# --- Test ---

test:
@for pkg in Packages/*/; do \
if [ -d "$$pkg/Tests" ]; then \
echo "==> Testing $$(basename $$pkg)..."; \
swift test --package-path "$$pkg"; \
fi; \
done

# --- Clean ---

clean:
Expand All @@ -66,12 +76,6 @@ clean:
clean-all: clean
rm -rf $(FRAMEWORKS_DIR)

# --- Test ---

test:
swift test --package-path Packages/CrowCore
swift test --package-path Packages/CrowPersistence

# --- Check ---

check: setup
Expand Down
4 changes: 2 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ description = "Run the app in debug mode"
run = "swift run CrowApp"

[tasks.build]
description = "Build debug"
run = "swift build"
description = "Full build: submodules + ghostty + swift build"
run = "make build"

[tasks."build:release"]
description = "Build release"
Expand Down
60 changes: 39 additions & 21 deletions scripts/build-ghostty.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
#!/usr/bin/env bash
# Build GhosttyKit.xcframework from the Ghostty submodule.
# Requires: zig 0.15.2, Xcode with Metal Toolchain
#
# Usage: bash scripts/build-ghostty.sh (or: make ghostty)
# Prerequisites: zig 0.15.2, Xcode with Metal Toolchain, ghostty submodule
# Output: Frameworks/GhosttyKit.xcframework/
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
GHOSTTY_DIR="$ROOT_DIR/vendor/ghostty"
FRAMEWORKS_DIR="$ROOT_DIR/Frameworks"
# NOTE: This script extracts individual object files from Zig's internal cache
# (.zig-cache/o/) to assemble a fat library. This approach is fragile and may
# break when Zig updates its cache layout. If builds fail after a Zig upgrade,
# this is the first place to investigate.
CACHE="$GHOSTTY_DIR/.zig-cache/o"
XCFW_DIR="$FRAMEWORKS_DIR/GhosttyKit.xcframework/macos-arm64"

# Clean up temp files on exit or interruption
cleanup() {
rm -rf "${ROOT_DIR}/.build/_ghostty_extract_$$" 2>/dev/null || true
rm -f "${ROOT_DIR}/.build/_ghostty_extract_$$.wuffs_full.o" 2>/dev/null || true
rm -f "${ROOT_DIR}/.build/_ghostty_extract_$$.vt_simd.o" 2>/dev/null || true
}
trap cleanup EXIT

# Ensure submodule is initialized
if [ ! -f "$GHOSTTY_DIR/build.zig" ]; then
echo "==> Initializing Ghostty submodule..."
Expand All @@ -35,17 +50,21 @@ fi
echo "==> Building GhosttyKit..."
cd "$GHOSTTY_DIR"

# Build (xcframework + app). The app link may fail but that's OK —
# we only need the xcframework and cached build artifacts.
# Build xcframework. The zig build may exit non-zero due to the app link step
# failing (expected — we only need the xcframework). We capture the exit code
# and verify the xcframework was actually produced.
set +e
zig build \
-Demit-xcframework=true \
-Dxcframework-target=native \
-Doptimize=ReleaseFast || true
-Doptimize=ReleaseFast
ZIG_EXIT=$?
set -e

# Check that the xcframework was produced
XCFW_SRC="$GHOSTTY_DIR/macos/GhosttyKit.xcframework"
if [ ! -d "$XCFW_SRC" ]; then
echo "ERROR: GhosttyKit.xcframework not found"
echo "ERROR: GhosttyKit.xcframework not found after zig build (exit code: $ZIG_EXIT)"
exit 1
fi

Expand All @@ -54,14 +73,17 @@ mkdir -p "$XCFW_DIR"

# Copy xcframework structure
cp "$XCFW_SRC/Info.plist" "$FRAMEWORKS_DIR/GhosttyKit.xcframework/"
cp -R "$XCFW_SRC/macos-arm64/Headers" "$XCFW_DIR/" 2>/dev/null || true
cp -R "$XCFW_SRC/macos-arm64/Headers" "$XCFW_DIR/" 2>/dev/null || true # Headers may not exist in all configurations

# Start with the dependency library from the xcframework
OUTPUT="$XCFW_DIR/libghostty-fat.a"
cp "$XCFW_SRC/macos-arm64/libghostty-fat.a" "$OUTPUT"

# Find and add the Zig-compiled ghostty API object (libghostty_zcu.o)
ZCU=$(find "$CACHE" -name "libghostty_zcu.o" -print -quit 2>/dev/null)
if [ -z "$ZCU" ]; then
echo "WARNING: libghostty_zcu.o not found in Zig cache — fat library may be incomplete"
fi
if [ -n "$ZCU" ]; then
ar r "$OUTPUT" "$ZCU" 2>/dev/null
echo " Added libghostty_zcu.o"
Expand All @@ -87,7 +109,9 @@ for libname in libglslang.a libspirv_cross.a libdcimgui.a libfreetype.a \
mkdir -p "$TMPEXTRACT"
cd "$TMPEXTRACT"
ar x "$found" 2>/dev/null
# shellcheck disable=SC2035 # Glob *.o is intentional — we want all extracted objects
chmod 644 *.o 2>/dev/null
# shellcheck disable=SC2035
ar r "$OUTPUT" *.o 2>/dev/null
cd "$ROOT_DIR"
rm -rf "$TMPEXTRACT"
Expand All @@ -102,26 +126,22 @@ fi

# Add the full wuffs object (contains all image decoders)
WUFFS_FULL=$(find "$CACHE" -name "wuffs-v0.4.o" -exec sh -c 'nm "$1" 2>/dev/null | grep -q "T _wuffs_jpeg__decoder__decode_frame" && echo "$1"' _ {} \; 2>/dev/null | head -1)
if [ -n "$WUFFS_FULL" ]; then
cp "$WUFFS_FULL" "$TMPEXTRACT.wuffs_full.o" 2>/dev/null || true
if [ -f "$TMPEXTRACT.wuffs_full.o" ]; then
ar r "$OUTPUT" "$TMPEXTRACT.wuffs_full.o" 2>/dev/null
rm "$TMPEXTRACT.wuffs_full.o"
fi
if [ -n "$WUFFS_FULL" ] && [ -f "$WUFFS_FULL" ]; then
cp "$WUFFS_FULL" "$TMPEXTRACT.wuffs_full.o"
ar r "$OUTPUT" "$TMPEXTRACT.wuffs_full.o" 2>/dev/null
rm "$TMPEXTRACT.wuffs_full.o"
fi

# Add the SIMD vt.o (decode_utf8 functions)
SIMD_VT=$(find "$CACHE" -name "vt.o" -exec sh -c 'nm "$1" 2>/dev/null | grep -q "T _ghostty_simd_decode_utf8" && echo "$1"' _ {} \; 2>/dev/null | head -1)
if [ -n "$SIMD_VT" ]; then
cp "$SIMD_VT" "$TMPEXTRACT.vt_simd.o" 2>/dev/null || true
if [ -f "$TMPEXTRACT.vt_simd.o" ]; then
ar r "$OUTPUT" "$TMPEXTRACT.vt_simd.o" 2>/dev/null
rm "$TMPEXTRACT.vt_simd.o"
fi
if [ -n "$SIMD_VT" ] && [ -f "$SIMD_VT" ]; then
cp "$SIMD_VT" "$TMPEXTRACT.vt_simd.o"
ar r "$OUTPUT" "$TMPEXTRACT.vt_simd.o" 2>/dev/null
rm "$TMPEXTRACT.vt_simd.o"
fi

# Regenerate symbol table
ranlib "$OUTPUT" 2>&1 || true
ranlib "$OUTPUT" 2>/dev/null || true

echo " Fat library: $(stat -f%z "$OUTPUT") bytes"

Expand All @@ -133,7 +153,5 @@ if [ -d "$RESOURCES_SRC" ]; then
echo " Bundled Ghostty resources"
fi

rm -rf "$TMPEXTRACT" 2>/dev/null || true

echo "==> Done! GhosttyKit.xcframework is ready."
echo " Verify: swift build"
13 changes: 12 additions & 1 deletion scripts/bundle.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/usr/bin/env bash
# Bundle Crow into a .app
#
# Usage: bash scripts/bundle.sh (or: make release)
# Prerequisites: GhosttyKit.xcframework must be built first (run: make ghostty)
# Output: Crow.app/ in the repo root
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down Expand Up @@ -32,7 +36,12 @@ echo "==> Building release..."
cd "$ROOT_DIR"
swift build -c release

echo "==> Creating app bundle..."
if [ ! -f "$BUILD_DIR/CrowApp" ]; then
echo "ERROR: Release binary not found at $BUILD_DIR/CrowApp"
exit 1
fi

echo "==> Creating app bundle (v$VERSION)..."
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR/Contents/MacOS"
mkdir -p "$APP_DIR/Contents/Resources"
Expand All @@ -46,6 +55,8 @@ if [ -d "$FRAMEWORKS_DIR/ghostty-resources" ]; then
echo " Bundled Ghostty resources"
fi

# TODO: Add CFBundleIconFile and bundle AppIcon.icns once icon asset pipeline is created

# Create Info.plist
cat > "$APP_DIR/Contents/Info.plist" << PLIST
<?xml version="1.0" encoding="UTF-8"?>
Expand Down
5 changes: 4 additions & 1 deletion scripts/generate-build-info.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/bin/env bash
# Generate BuildInfo.swift and CLIVersion.swift with version, git SHA, and build date
# Generate BuildInfo.swift and CLIVersion.swift with version, git SHA, and build date.
#
# Output: Sources/Crow/Generated/BuildInfo.swift
# Sources/CrowCLI/Generated/CLIVersion.swift
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down
Loading