13 KiB
CLI-Only Workflow
Build, run, debug, and monitor macOS apps entirely from command line without opening Xcode.
```bash # Ensure Xcode is installed and selected xcode-select -p # Should show: /Applications/Xcode.app/Contents/DeveloperIf not, run:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
Install XcodeGen for project creation
brew install xcodegen
Optional: prettier build output
brew install xcbeautify
</prerequisites>
<create_project>
**Create a new project entirely from CLI**:
```bash
# Create directory structure
mkdir MyApp && cd MyApp
mkdir -p Sources Tests Resources
# Create project.yml (Claude generates this)
cat > project.yml << 'EOF'
name: MyApp
options:
bundleIdPrefix: com.yourcompany
deploymentTarget:
macOS: "14.0"
targets:
MyApp:
type: application
platform: macOS
sources: [Sources]
settings:
PRODUCT_BUNDLE_IDENTIFIER: com.yourcompany.myapp
DEVELOPMENT_TEAM: YOURTEAMID
EOF
# Create app entry point
cat > Sources/MyApp.swift << 'EOF'
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
Text("Hello, World!")
}
}
}
EOF
# Generate .xcodeproj
xcodegen generate
# Verify
xcodebuild -list -project MyApp.xcodeproj
# Build
xcodebuild -project MyApp.xcodeproj -scheme MyApp build
See project-scaffolding.md for complete project.yml templates. </create_project>
```bash # See available schemes and targets xcodebuild -list -project MyApp.xcodeproj ```<build_debug>
# Build debug configuration
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-configuration Debug \
-derivedDataPath ./build \
build
# Output location
ls ./build/Build/Products/Debug/MyApp.app
</build_debug>
<build_release>
# Build release configuration
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-configuration Release \
-derivedDataPath ./build \
build
</build_release>
<build_with_signing>
# Build with code signing for distribution
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-configuration Release \
-derivedDataPath ./build \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name" \
DEVELOPMENT_TEAM=YOURTEAMID \
build
</build_with_signing>
```bash # Clean build artifacts xcodebuild -project MyApp.xcodeproj \ -scheme MyApp \ cleanRemove derived data
rm -rf ./build
</clean>
<build_errors>
Build output goes to stdout. Filter for errors:
```bash
xcodebuild -project MyApp.xcodeproj -scheme MyApp build 2>&1 | grep -E "error:|warning:"
For prettier output, use xcpretty (install with gem install xcpretty):
xcodebuild -project MyApp.xcodeproj -scheme MyApp build | xcpretty
</build_errors>
```bash # Run the built app open ./build/Build/Products/Debug/MyApp.appOr run directly (shows stdout in terminal)
./build/Build/Products/Debug/MyApp.app/Contents/MacOS/MyApp
</launch_app>
<run_with_arguments>
```bash
# Pass command line arguments
./build/Build/Products/Debug/MyApp.app/Contents/MacOS/MyApp --debug-mode
# Pass environment variables
MYAPP_DEBUG=1 ./build/Build/Products/Debug/MyApp.app/Contents/MacOS/MyApp
</run_with_arguments>
```bash # Run in background (don't bring to front) open -g ./build/Build/Products/Debug/MyApp.appRun hidden (no dock icon)
open -j ./build/Build/Products/Debug/MyApp.app
</background>
</run>
<logging>
<os_log_in_code>
Add logging to your Swift code:
```swift
import os
class DataService {
private let logger = Logger(subsystem: "com.yourcompany.MyApp", category: "Data")
func loadItems() async throws -> [Item] {
logger.info("Loading items...")
do {
let items = try await fetchItems()
logger.info("Loaded \(items.count) items")
return items
} catch {
logger.error("Failed to load items: \(error.localizedDescription)")
throw error
}
}
func saveItem(_ item: Item) {
logger.debug("Saving item: \(item.id)")
// ...
}
}
Log levels:
.debug- Verbose development info.info- General informational.notice- Notable conditions.error- Errors.fault- Critical failures </os_log_in_code>
<stream_logs>
# Stream logs from your app (run while app is running)
log stream --predicate 'subsystem == "com.yourcompany.MyApp"' --level info
# Filter by category
log stream --predicate 'subsystem == "com.yourcompany.MyApp" and category == "Data"'
# Filter by process name
log stream --predicate 'process == "MyApp"' --level debug
# Include debug messages
log stream --predicate 'subsystem == "com.yourcompany.MyApp"' --level debug
# Show only errors
log stream --predicate 'subsystem == "com.yourcompany.MyApp" and messageType == error'
</stream_logs>
<search_past_logs>
# Search recent logs (last hour)
log show --predicate 'subsystem == "com.yourcompany.MyApp"' --last 1h
# Search specific time range
log show --predicate 'subsystem == "com.yourcompany.MyApp"' \
--start "2024-01-15 10:00:00" \
--end "2024-01-15 11:00:00"
# Export to file
log show --predicate 'subsystem == "com.yourcompany.MyApp"' --last 1h > app_logs.txt
</search_past_logs>
<system_logs>
# See app lifecycle events
log stream --predicate 'process == "MyApp" or (sender == "lsd" and message contains "MyApp")'
# Network activity (if using NSURLSession)
log stream --predicate 'subsystem == "com.apple.network" and process == "MyApp"'
# Core Data / SwiftData activity
log stream --predicate 'subsystem == "com.apple.coredata"'
</system_logs>
```bash # Start app, then attach lldb ./build/Build/Products/Debug/MyApp.app/Contents/MacOS/MyApp &Attach by process name
lldb -n MyApp
Or attach by PID
lldb -p $(pgrep MyApp)
</lldb_attach>
<lldb_launch>
```bash
# Launch app under lldb directly
lldb ./build/Build/Products/Debug/MyApp.app/Contents/MacOS/MyApp
# In lldb:
(lldb) run
</lldb_launch>
<common_lldb_commands>
# In lldb session:
# Set breakpoint by function name
(lldb) breakpoint set --name saveItem
(lldb) b DataService.swift:42
# Set conditional breakpoint
(lldb) breakpoint set --name saveItem --condition 'item.name == "Test"'
# Continue execution
(lldb) continue
(lldb) c
# Step over/into/out
(lldb) next
(lldb) step
(lldb) finish
# Print variable
(lldb) p item
(lldb) po self.items
# Print with format
(lldb) p/x pointer # hex
(lldb) p/t flags # binary
# Backtrace
(lldb) bt
(lldb) bt all # all threads
# List threads
(lldb) thread list
# Switch thread
(lldb) thread select 2
# Frame info
(lldb) frame info
(lldb) frame variable # all local variables
# Watchpoint (break when value changes)
(lldb) watchpoint set variable self.items.count
# Expression evaluation
(lldb) expr self.items.append(newItem)
</common_lldb_commands>
<debug_entitlement>
For lldb to attach, your app needs the get-task-allow entitlement (included in Debug builds by default):
<key>com.apple.security.get-task-allow</key>
<true/>
If you have attachment issues:
# Check entitlements
codesign -d --entitlements - ./build/Build/Products/Debug/MyApp.app
</debug_entitlement>
<crash_logs>
# User crash logs
ls ~/Library/Logs/DiagnosticReports/
# System crash logs (requires sudo)
ls /Library/Logs/DiagnosticReports/
# Find your app's crashes
ls ~/Library/Logs/DiagnosticReports/ | grep MyApp
<read_crash>
# View latest crash
cat ~/Library/Logs/DiagnosticReports/MyApp_*.ips | head -200
# Symbolicate (if you have dSYM)
atos -arch arm64 -o ./build/Build/Products/Debug/MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x100001234
</read_crash>
<monitor_crashes>
# Watch for new crashes
fswatch ~/Library/Logs/DiagnosticReports/ | grep MyApp
</monitor_crashes> </crash_logs>
```bash # List available templates instruments -s templatesProfile CPU usage
instruments -t "Time Profiler" -D trace.trace ./build/Build/Products/Debug/MyApp.app
Profile memory
instruments -t "Allocations" -D memory.trace ./build/Build/Products/Debug/MyApp.app
Profile leaks
instruments -t "Leaks" -D leaks.trace ./build/Build/Products/Debug/MyApp.app
</instruments_cli>
<signposts>
Add signposts for custom profiling:
```swift
import os
class DataService {
private let signposter = OSSignposter(subsystem: "com.yourcompany.MyApp", category: "Performance")
func loadItems() async throws -> [Item] {
let signpostID = signposter.makeSignpostID()
let state = signposter.beginInterval("Load Items", id: signpostID)
defer {
signposter.endInterval("Load Items", state)
}
return try await fetchItems()
}
}
View in Instruments with "os_signpost" instrument.
<code_signing> <check_signature>
# Verify signature
codesign -v ./build/Build/Products/Release/MyApp.app
# Show signature details
codesign -dv --verbose=4 ./build/Build/Products/Release/MyApp.app
# Show entitlements
codesign -d --entitlements - ./build/Build/Products/Release/MyApp.app
</check_signature>
<sign_manually>
# Sign with Developer ID (for distribution outside App Store)
codesign --force --sign "Developer ID Application: Your Name (TEAMID)" \
--entitlements MyApp/MyApp.entitlements \
--options runtime \
./build/Build/Products/Release/MyApp.app
</sign_manually>
```bash # Create ZIP for notarization ditto -c -k --keepParent ./build/Build/Products/Release/MyApp.app MyApp.zipSubmit for notarization
xcrun notarytool submit MyApp.zip
--apple-id your@email.com
--team-id YOURTEAMID
--password @keychain:AC_PASSWORD
--wait
Staple ticket to app
xcrun stapler staple ./build/Build/Products/Release/MyApp.app
**Store password in keychain**:
```bash
xcrun notarytool store-credentials --apple-id your@email.com --team-id TEAMID
Run specific test class
xcodebuild -project MyApp.xcodeproj
-scheme MyApp
-only-testing:MyAppTests/DataServiceTests
test
Run specific test method
xcodebuild -project MyApp.xcodeproj
-scheme MyApp
-only-testing:MyAppTests/DataServiceTests/testLoadItems
test
</run_tests>
<test_output>
```bash
# Pretty test output
xcodebuild test -project MyApp.xcodeproj -scheme MyApp | xcpretty --test
# Generate test report
xcodebuild test -project MyApp.xcodeproj -scheme MyApp \
-resultBundlePath ./TestResults.xcresult
# View result bundle
xcrun xcresulttool get --path ./TestResults.xcresult --format json
</test_output>
<test_coverage>
# Build with coverage
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-enableCodeCoverage YES \
-derivedDataPath ./build \
test
# Generate coverage report
xcrun llvm-cov report \
./build/Build/Products/Debug/MyApp.app/Contents/MacOS/MyApp \
-instr-profile=./build/Build/ProfileData/*/Coverage.profdata
</test_coverage>
<complete_workflow> Typical development cycle without opening Xcode:
# 1. Edit code (in your editor of choice)
# Claude Code, vim, VS Code, etc.
# 2. Build
xcodebuild -project MyApp.xcodeproj -scheme MyApp -configuration Debug -derivedDataPath ./build build 2>&1 | grep -E "error:|warning:" || echo "Build succeeded"
# 3. Run
open ./build/Build/Products/Debug/MyApp.app
# 4. Monitor logs (in separate terminal)
log stream --predicate 'subsystem == "com.yourcompany.MyApp"' --level debug
# 5. If crash, check logs
cat ~/Library/Logs/DiagnosticReports/MyApp_*.ips | head -100
# 6. Debug if needed
lldb -n MyApp
# 7. Run tests
xcodebuild -project MyApp.xcodeproj -scheme MyApp test
# 8. Build release
xcodebuild -project MyApp.xcodeproj -scheme MyApp -configuration Release -derivedDataPath ./build build
</complete_workflow>
<helper_script> Create a build script for convenience:
#!/bin/bash
# build.sh
PROJECT="MyApp.xcodeproj"
SCHEME="MyApp"
CONFIG="${1:-Debug}"
echo "Building $SCHEME ($CONFIG)..."
xcodebuild -project "$PROJECT" \
-scheme "$SCHEME" \
-configuration "$CONFIG" \
-derivedDataPath ./build \
build 2>&1 | tee build.log | grep -E "error:|warning:|BUILD"
if [ ${PIPESTATUS[0]} -eq 0 ]; then
echo "✓ Build succeeded"
echo "App: ./build/Build/Products/$CONFIG/$SCHEME.app"
else
echo "✗ Build failed - see build.log"
exit 1
fi
chmod +x build.sh
./build.sh # Debug build
./build.sh Release # Release build
</helper_script>
<useful_aliases> Add to ~/.zshrc or ~/.bashrc:
# Build current project
alias xb='xcodebuild -project *.xcodeproj -scheme $(basename *.xcodeproj .xcodeproj) -derivedDataPath ./build build'
# Build and run
alias xbr='xb && open ./build/Build/Products/Debug/*.app'
# Run tests
alias xt='xcodebuild -project *.xcodeproj -scheme $(basename *.xcodeproj .xcodeproj) test'
# Stream logs for current project
alias xl='log stream --predicate "subsystem contains \"$(defaults read ./build/Build/Products/Debug/*.app/Contents/Info.plist CFBundleIdentifier)\"" --level debug'
# Clean
alias xc='xcodebuild -project *.xcodeproj -scheme $(basename *.xcodeproj .xcodeproj) clean && rm -rf ./build'
</useful_aliases>