Integrate into an existing project
June 13, 2026 · View on GitHub
Wire SolidLikeARock into Xcode, CI, SwiftPM, Danger, an AI agent, or generate an architecture graph. New here? Start with the README.
Xcode — Run Script build phase
Select your app target → Build Phases → + → New Run Script Phase, then paste:
if command -v solid-like-a-rock >/dev/null; then
solid-like-a-rock --config "$SRCROOT/.solid.yml" "$SRCROOT/Sources"
else
echo "warning: solid-like-a-rock not installed — skipping architecture lint"
fi
Because the output uses the file:line: error: format, violations show up as
red errors inline in the editor and fail the build. Drop the command -v
guard if you'd rather make the tool mandatory for everyone.
Xcode will warn that the phase "will be run during every build because it does not specify any outputs" — that's exactly what you want for a linter. Silence it by unchecking "Based on dependency analysis" on the phase (it then runs every build, as intended).
Prefer it as a separate, run-on-demand step instead of part of the app build?
Make it a dedicated aggregate target (+ under TARGETS → Aggregate) with
the same Run Script phase, and build that scheme to lint.
Zero-install variant (auto-download a pinned binary)
If you don't want every contributor to install the tool, have the build phase
fetch a pinned release binary into a gitignored cache. Add a
scripts/lint-architecture.sh to your repo:
#!/usr/bin/env bash
set -euo pipefail
VERSION="v0.9.0"
# Universal binary (arm64 + x86_64) since v0.4.2 — no arch detection needed.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN="$ROOT/.tools/solid-like-a-rock-$VERSION"
if [[ ! -x "$BIN" ]]; then
mkdir -p "$ROOT/.tools"; tmp="$(mktemp -d)"
url="https://github.com/nenadvulic/solid-like-a-rock/releases/download/$VERSION/solid-like-a-rock-macos-universal.tar.gz"
if ! curl -fsSL "$url" -o "$tmp/slr.tar.gz" 2>/dev/null; then
echo "warning: could not download solid-like-a-rock — skipping architecture lint"; exit 0
fi
tar -xzf "$tmp/slr.tar.gz" -C "$tmp"; mv "$tmp/solid-like-a-rock" "$BIN"; chmod +x "$BIN"
fi
exec "$BIN" --config "$ROOT/.solid.yml" "$ROOT/Sources"
Then point the Run Script phase at it — "${SRCROOT}/../scripts/lint-architecture.sh"
(adjust the relative path to your layout). Add .tools/ to .gitignore.
Two gotchas: the binary is pinned to a version and cached, so the network is only hit once (subsequent builds are offline). And if Xcode's
ENABLE_USER_SCRIPT_SANDBOXINGisYES, that first download is blocked — run the script once from a terminal to seed the cache, or set it toNOfor the target.
CI — GitHub Action (easiest)
- uses: nenadvulic/solid-like-a-rock-action@v1
with:
paths: Sources
See solid-like-a-rock-action for all inputs (version, config, baseline).
CI — download the released binary
Each tagged release publishes a prebuilt macOS binary, so CI doesn't pay the SwiftSyntax build cost:
# .github/workflows/architecture.yml
name: Architecture
on: [push, pull_request]
jobs:
lint:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Install solid-like-a-rock
run: |
curl -fsSL -o slr.tar.gz \
https://github.com/nenadvulic/solid-like-a-rock/releases/latest/download/solid-like-a-rock-macos-universal.tar.gz
tar -xzf slr.tar.gz && sudo mv solid-like-a-rock /usr/local/bin/
- name: Lint import boundaries
run: solid-like-a-rock --reporter github --config .solid.yml Sources
--reporter github emits ::error file=…,line=…:: workflow commands, so each
violation shows up as an inline annotation on the pull request (not just in
the log). Reporters: text (default, Xcode/CI-friendly file:line: error:),
json (tooling/Danger), github (PR annotations). --format/-f is a kept alias.
CI — run from source, nothing to install
If your project is already a SwiftPM package (or you just want it for free in CI), build and run the tool straight from a checkout — no binary to manage:
git clone --depth 1 https://github.com/nenadvulic/solid-like-a-rock /tmp/slr
swift run --package-path /tmp/slr solid-like-a-rock --config .solid.yml Sources
Either way the non-zero exit code fails the job, so a layer violation blocks the merge the same way a failing test would.
SwiftPM command plugin (zero-install)
If your project is a SwiftPM package, add SolidLikeARock as a dependency and run the bundled command plugin — nothing to install, and it works the same locally and in CI:
// your Package.swift
dependencies: [
.package(url: "https://github.com/nenadvulic/solid-like-a-rock", from: "0.9.0"),
],
swift package solid-lint --config .solid.yml Sources
# or, with the conventional layout (.solid.yml at the package root + Sources/):
swift package solid-lint
The plugin runs read-only and exits non-zero when violations are found, so it drops straight into a CI step.
SwiftPM build-tool plugin (lint on every build)
For SwiftPM targets, apply the build-tool plugin to lint automatically as a
prebuild step on every swift build (and in Xcode) — violations appear inline,
no separate run-script. It uses a prebuilt binary from the release artifactbundle,
so no compile-from-source cost:
// your Package.swift
dependencies: [
.package(url: "https://github.com/nenadvulic/solid-like-a-rock", from: "0.9.0"),
],
targets: [
.target(
name: "MyLib",
plugins: [.plugin(name: "SolidLintBuildTool", package: "solid-like-a-rock")]
),
],
It reads .solid.yml from the package root (or discovers it) and fails the build
on an error-level violation — so a layering regression breaks the build like a
failing test would.
Danger (comment on the PR diff)
Pass --format json to get a machine-readable array instead of the text
diagnostics — ideal for feeding Danger:
solid-like-a-rock --format json --config .solid.yml Sources
# [ { "file": "...", "line": 5, "module": "NetworkProvider",
# "layer": "Presentation", "reason": "deniedImport", "message": "..." }, ... ]
A ready-to-use Danger Swift example lives at
examples/Dangerfile.swift. It lints only the
files the PR touches and posts each violation as an inline comment on the exact
line — so contributors are flagged for what they introduce, not for pre-existing
debt. (The exit code still reflects whether violations were found.)
Claude Code (lint on every AI edit)
When you let an AI agent edit your codebase, the linter only helps if it actually
runs. A Claude Code PostToolUse
hook runs it automatically after the agent touches a .swift file and feeds
any violation straight back, so the agent fixes the boundary instead of moving on
— it never decides whether to check. This repository dogfoods the hook on its own
sources.
Copy .claude/hooks/solid-lint-changed.sh
and .claude/settings.json into the consuming project.
See .claude/README.md for activation, example output, the
SOLID_BIN override, and a Stop-hook alternative.
Architecture graph
Generate an always-accurate diagram of your layers straight from the real import graph — forbidden edges show up red. It renders natively in a GitHub README/PR (Mermaid) and never goes stale, because it's generated, not drawn.
solid-like-a-rock graph --config .solid.yml Sources # Mermaid (default)
solid-like-a-rock graph --format dot Sources | dot -Tsvg > architecture.svg
Example — the bundled TCA sample, where one feature imports a sibling feature
(isolatePeers), so that edge is flagged red:
graph TD Dependencies --> Models Features --> Models Features --> Dependencies Features -->|❌ peer| Features App --> Features linkStyle 3 stroke:#e00,stroke-width:2px
And on a real 88-module app — isowords, Point-Free's open-source game — a hand-curated layer config collapses the whole package to its client architecture, every dependency pointing inward:
graph TD Clients --> SharedModels BuildingBlocks --> SharedModels BuildingBlocks --> Clients Features --> SharedModels Features --> Clients Features --> BuildingBlocks ClientsLive --> SharedModels ClientsLive --> Clients ClientsLive --> BuildingBlocks App --> SharedModels App --> Clients App --> BuildingBlocks App --> Features
Regenerate it in CI so the committed diagram always tracks the code — fail the build if it drifts:
# .github/workflows/architecture-graph.yml (excerpt)
- run: |
solid-like-a-rock graph --config .solid.yml Sources > docs/architecture.mmd
git diff --exit-code docs/architecture.mmd \
|| { echo "::error::architecture.mmd is stale — regenerate and commit it"; exit 1; }