# AICX Build System
# Local developer flow + release/readiness helpers
.PHONY: all build build-native release-binaries install install-bin install-config install-cargo git-hooks
.PHONY: precheck precheck-native test test-native check fmt fmt-check clippy clippy-native semgrep ci clean help manifest-check
.PHONY: embeddings-check embeddings-test embeddings-clippy embeddings-hydrate embeddings-info
.PHONY: version version-show version-check version-bump version-patch bump-patch changelog-close release-notes release-plan release-prepare release-check release-tag release-push package-check release-bundle release-bundle-only-binaries
all: build
PACKAGE_NAME := $(shell python3 -c 'import tomllib; print(tomllib.load(open("Cargo.toml","rb"))["package"]["name"])')
VERSION := $(shell python3 -c 'import tomllib; print(tomllib.load(open("Cargo.toml","rb"))["package"]["version"])')
TAG := v$(VERSION)
KEYS ?= $(if $(AICX_KEYS_DIR),$(AICX_KEYS_DIR),$(HOME)/.keys)
NOTARY_PROFILE ?= $(AICX_NOTARY_PROFILE)
CLEAN ?= 1
EMBEDDER_PROFILE ?= base
NATIVE ?= 0
FEATURES ?=
TARGET ?= $(shell rustc -vV | sed -n 's/^host: //p')
CODESIGN ?= auto
DIST_DIR ?= $(CURDIR)/dist
DRY_RUN ?= 0
RELEASE_BINARIES := aicx aicx-mcp
build:
cargo build --locked --release --bin aicx --bin aicx-mcp
build-native:
cargo build --locked --release --features native-embedder --bin aicx --bin aicx-mcp
release-binaries:
@if [ -z "$(STAGING_DIR)" ]; then \
echo "STAGING_DIR is required. Usage: make release-binaries STAGING_DIR=/tmp/stage TARGET=$(TARGET)" >&2; \
exit 1; \
fi
cargo build --locked --release --target "$(TARGET)" --bin aicx --bin aicx-mcp
@mkdir -p "$(STAGING_DIR)/bin" "$(STAGING_DIR)/components"
@for bin in $(RELEASE_BINARIES); do \
install -m 0755 "target/$(TARGET)/release/$$bin" "$(STAGING_DIR)/bin/$$bin"; \
printf ' %s -> %s\n' "$$bin" "$(STAGING_DIR)/bin/$$bin"; \
done
@case "$(TARGET)" in \
*apple-darwin) \
if [ "$(CODESIGN)" = "0" ]; then \
echo " codesign skipped (CODESIGN=0)"; \
elif [ -n "$${MACOS_DEVELOPER_ID_APPLICATION:-}" ]; then \
for bin in $(RELEASE_BINARIES); do \
codesign --force --timestamp --options runtime --sign "$$MACOS_DEVELOPER_ID_APPLICATION" "$(STAGING_DIR)/bin/$$bin"; \
codesign --verify --verbose=2 "$(STAGING_DIR)/bin/$$bin" >/dev/null; \
printf ' codesigned %s\n' "$$bin"; \
done; \
elif [ "$(CODESIGN)" = "1" ]; then \
echo "MACOS_DEVELOPER_ID_APPLICATION is required for CODESIGN=1" >&2; \
exit 1; \
else \
echo " codesign skipped (set CODESIGN=1 and MACOS_DEVELOPER_ID_APPLICATION for release)"; \
fi ;; \
esac
@python3 -c 'import json, pathlib, sys; staging=pathlib.Path(sys.argv[1]); version=sys.argv[2]; commit=sys.argv[3]; data={"source":"loctree-aicx","commit":commit,"components":[{"name":"aicx","version":version,"source":"loctree-aicx"},{"name":"aicx-mcp","version":version,"source":"loctree-aicx"}]}; path=staging/"components"/"loctree-aicx.json"; path.write_text(json.dumps(data, indent=2)+"\n", encoding="utf-8"); print(f" metadata -> {path}")' "$(STAGING_DIR)" "$(VERSION)" "$$(git rev-parse --short=12 HEAD)"
install:
./install.sh
@$(MAKE) git-hooks
install-bin:
cargo install --path . --locked --force --bin aicx --bin aicx-mcp
install-config:
./install.sh --skip-install
install-cargo:
@echo "crates.io install is not the active AICX distribution path."
@echo "Use GitHub Release bundles, npm, Homebrew tap, or a local checkout install."
@echo "For this checkout, run: make install-bin"
git-hooks:
@echo "Installing git hooks..."
@bash ./tools/install-githooks.sh
@echo "✓ pre-commit + pre-push hooks installed"
precheck:
cargo check --locked -p aicx --all-targets
cargo check --locked -p aicx-embeddings
precheck-native:
cargo check --locked -p aicx-embeddings --features gguf
cargo check --locked -p aicx --features native-embedder --all-targets
manifest-check:
@python3 -c 'import tomllib; data = tomllib.load(open("Cargo.toml", "rb")); allow = {("dependencies", "rmcp-memex"), ("dependencies", "aicx-embeddings")}; bad = [(section, name, spec["path"]) for section in ("dependencies", "dev-dependencies", "build-dependencies") for name, spec in data.get(section, {}).items() if isinstance(spec, dict) and "path" in spec and (section, name) not in allow]; \
print("Manifest policy: ok (approved local product deps only)") if not bad else (_ for _ in ()).throw(SystemExit("Manifest policy check failed:\n" + "\n".join(f" - {section}.{name} uses unexpected local path dependency {path}" for section, name, path in bad)))'
test:
cargo test --locked -p aicx --all-targets
cargo test --locked -p aicx-embeddings
test-native:
cargo test --locked -p aicx-embeddings --features gguf
cargo test --locked -p aicx --features native-embedder --test native_embedder
check:
@echo "=== AICX Quality Gate ==="
@echo "[1/10] Checking manifest portability..."
@$(MAKE) manifest-check
@echo "[2/10] Checking formatting..."
@cargo fmt --all --check || (echo "Run 'make fmt' to fix formatting." && exit 1)
@echo "[3/10] Running default cargo check..."
@$(MAKE) precheck
@echo "[4/10] Running native GGUF cargo check..."
@$(MAKE) precheck-native
@echo "[5/10] Running default clippy..."
@$(MAKE) clippy
@echo "[6/10] Running native GGUF clippy..."
@$(MAKE) clippy-native
@echo "[7/10] Running default tests..."
@$(MAKE) test
@echo "[8/10] Running native GGUF tests..."
@$(MAKE) test-native
@echo "[9/10] Building slim release binaries..."
@cargo build --locked --release --bin aicx --bin aicx-mcp
@echo "[10/10] Running Semgrep (if available)..."
@if command -v semgrep >/dev/null 2>&1 || command -v pipx >/dev/null 2>&1; then \
SEMGREP=$$(command -v semgrep || echo "pipx run semgrep"); \
$$SEMGREP --config auto --error --quiet . --exclude target; \
else \
echo "[!] Semgrep not available, skipping (install: pipx install semgrep)"; \
fi
@echo "=== All checks passed ==="
fmt:
cargo fmt --all
fmt-check:
cargo fmt --all --check
clippy:
cargo clippy --locked -p aicx --all-targets -- -D warnings
cargo clippy --locked -p aicx-embeddings -- -D warnings
clippy-native:
cargo clippy --locked -p aicx-embeddings --features gguf -- -D warnings
cargo clippy --locked -p aicx --features native-embedder --all-targets -- -D warnings
embeddings-info:
@case "$(EMBEDDER_PROFILE)" in \
base) repo="mradermacher/F2LLM-v2-0.6B-GGUF"; file="F2LLM-v2-0.6B.Q4_K_M.gguf"; size="~397 MB"; dim="1024";; \
dev) repo="mradermacher/F2LLM-v2-1.7B-GGUF"; file="F2LLM-v2-1.7B.Q4_K_M.gguf"; size="~1.1 GB"; dim="2048";; \
premium) repo="mradermacher/F2LLM-v2-1.7B-GGUF"; file="F2LLM-v2-1.7B.Q6_K.gguf"; size="~1.4 GB"; dim="2048";; \
*) echo "Unsupported EMBEDDER_PROFILE=$(EMBEDDER_PROFILE). Use base, dev, or premium." >&2; exit 1;; \
esac; \
printf "profile: %s\nrepo: %s\nfile: %s\nsize: %s\ndim: %s\n" "$(EMBEDDER_PROFILE)" "$$repo" "$$file" "$$size" "$$dim"
embeddings-hydrate:
@if ! command -v hf >/dev/null 2>&1; then \
echo "Missing 'hf' CLI. Install HuggingFace CLI first, then retry."; \
exit 1; \
fi
@case "$(EMBEDDER_PROFILE)" in \
base) repo="mradermacher/F2LLM-v2-0.6B-GGUF"; file="F2LLM-v2-0.6B.Q4_K_M.gguf";; \
dev) repo="mradermacher/F2LLM-v2-1.7B-GGUF"; file="F2LLM-v2-1.7B.Q4_K_M.gguf";; \
premium) repo="mradermacher/F2LLM-v2-1.7B-GGUF"; file="F2LLM-v2-1.7B.Q6_K.gguf";; \
*) echo "Unsupported EMBEDDER_PROFILE=$(EMBEDDER_PROFILE). Use base, dev, or premium." >&2; exit 1;; \
esac; \
echo "Hydrating AICX native embedder: $$repo $$file"; \
hf download "$$repo" "$$file"
embeddings-check:
cargo check --locked -p aicx-embeddings --features gguf
embeddings-test:
cargo test --locked -p aicx-embeddings --features gguf
embeddings-clippy:
cargo clippy --locked -p aicx-embeddings --features gguf -- -D warnings
semgrep:
@if command -v semgrep >/dev/null 2>&1 || command -v pipx >/dev/null 2>&1; then \
SEMGREP=$$(command -v semgrep || echo "pipx run semgrep"); \
$$SEMGREP --config auto --error --quiet . --exclude target; \
else \
echo "[!] Semgrep not available, skipping (install: pipx install semgrep)"; \
fi
ci: check
@echo "CI-equivalent local checks passed."
version: version-show
version-show:
@printf "package: %s\n" "$(PACKAGE_NAME)"
@printf "version: %s\n" "$(VERSION)"
@printf "tag: %s\n" "$(TAG)"
@if git rev-parse --verify "refs/tags/$(TAG)" >/dev/null 2>&1; then \
echo "tag-state: exists"; \
else \
echo "tag-state: missing"; \
fi
version-check:
@python3 tools/release_sync.py check
version-bump:
ifeq ($(origin VERSION),command line)
@python3 tools/release_sync.py bump "$(VERSION)"
@echo ""
@echo "Versioned release surfaces synced from Cargo.toml into docs + distribution/npm."
@echo "Cargo.lock is intentionally not touched by version-bump."
@echo "To sync the lockfile for this package only (no network):"
@echo " cargo update --package $(PACKAGE_NAME) --offline"
@echo "Or rely on 'make release-prepare' to sync it for you."
else
@echo "VERSION is required. Usage: make version-bump VERSION={patch|minor|major|x.y.z}" >&2 && exit 1
endif
version-patch bump-patch:
@$(MAKE) version-bump VERSION=patch
changelog-close:
@python3 tools/changelog_close.py $(if $(CHANGELOG_GENERATE),--generate-if-empty)
release-notes:
@python3 tools/release_sync.py notes $(if $(origin VERSION),$(VERSION),) $(if $(OUTPUT),--output $(OUTPUT),)
release-plan:
@echo "AICX release flow"
@echo ""
@echo "1. Ensure branch is merged and green."
@echo "2. Prepare the release bundle:"
@echo " make release-prepare VERSION={patch|minor|major|x.y.z}"
@echo " (runs version-bump + changelog-close + release-notes preview + precheck)"
@echo "3. Review diff, commit Cargo.toml + Cargo.lock + CHANGELOG.md + any synced docs/package manifests."
@echo "4. Run: make release-check"
@echo "5. Create annotated tag: make release-tag"
@echo "6. Push tag: make release-push"
@echo "7. Wait for GitHub Actions to build signed release archives from the pushed tag."
@echo "8. Publish npm after GitHub Release assets exist:"
@echo " gh workflow run npm-publish.yml -f version=$(VERSION)"
@echo "9. Optional local macOS signed bundle:"
@echo " make release-bundle KEYS=$(HOME)/.keys"
@echo " make release-bundle KEYS=$(HOME)/.keys NATIVE=1"
@echo " make release-bundle KEYS=$(HOME)/.keys NOTARY_PROFILE=my-notary-profile"
@echo " make release-bundle KEYS=$(HOME)/.keys CLEAN=0 # keep local target artifacts"
@echo "10. Optional native embedder sanity:"
@echo " make embeddings-info EMBEDDER_PROFILE=base"
@echo " make embeddings-hydrate EMBEDDER_PROFILE=base"
@echo " make test-native"
@echo "11. GitHub Actions release workflow builds archives and derives GitHub release notes from CHANGELOG.md."
@echo ""
@echo "Reference docs:"
@echo " - docs/RELEASES.md"
@echo " - docs/COMMANDS.md"
release-prepare:
ifeq ($(origin VERSION),command line)
@$(MAKE) version-bump VERSION=$(VERSION)
@$(MAKE) changelog-close CHANGELOG_GENERATE=1
@cargo update --package $(PACKAGE_NAME) --offline
@$(MAKE) version-check
@python3 tools/release_sync.py notes --output dist/release-notes.md
@$(MAKE) precheck
else
@echo "VERSION is required. Usage: make release-prepare VERSION={patch|minor|major|x.y.z}" >&2 && exit 1
endif
@echo ""
@echo "=== Release prepared ==="
@echo "Next: review diff, commit, then:"
@echo " make release-check"
@echo " make release-tag"
@echo " make release-push"
@echo " cat dist/release-notes.md # preview GitHub release body"
@echo " make release-bundle KEYS=$(HOME)/.keys [CLEAN=0]"
release-check:
@python3 tools/release_sync.py check --require-version-section
@$(MAKE) check
@echo "Release readiness passed."
release-tag:
@if git rev-parse --verify "refs/tags/$(TAG)" >/dev/null 2>&1; then \
echo "Tag $(TAG) already exists."; \
exit 1; \
fi
git tag -a "$(TAG)" -m "Release $(TAG)"
@echo "Created annotated tag $(TAG)"
release-push:
git push origin "$(TAG)"
package-check:
@echo "crates.io packaging is intentionally disabled for aicx."
@echo "Use GitHub Release archives + npm platform packages instead."
@echo "Run: make release-check"
release-bundle:
@KEYS="$(KEYS)" \
NOTARY_PROFILE="$(NOTARY_PROFILE)" \
TARGET="$(TARGET)" \
DIST_DIR="$(DIST_DIR)" \
DRY_RUN="$(DRY_RUN)" \
AICX_CLEAN_AFTER_BUILD="$(CLEAN)" \
NATIVE="$(NATIVE)" \
FEATURES="$(FEATURES)" \
PACKAGE_NAME="$(PACKAGE_NAME)" \
./tools/release_bundle.sh
release-bundle-only-binaries:
@AICX_RELEASE_BUNDLE_ONLY_BINARIES=1 \
TARGET="$(TARGET)" \
DIST_DIR="$(DIST_DIR)" \
DRY_RUN="$(DRY_RUN)" \
AICX_CLEAN_AFTER_BUILD="$(CLEAN)" \
NATIVE="$(NATIVE)" \
FEATURES="$(FEATURES)" \
PACKAGE_NAME="$(PACKAGE_NAME)" \
./tools/release_bundle.sh
clean:
cargo clean
help:
@echo "AICX Build System"
@echo ""
@echo "Core Commands:"
@echo " make build - Build release binaries (aicx + aicx-mcp)"
@echo " make build-native - Build release binaries with native GGUF embedder support"
@echo " make install - Install binaries + configure local MCP clients via install.sh"
@echo " make install-bin - Install only aicx + aicx-mcp from the current checkout"
@echo " make install-config - Configure local MCP clients without reinstalling binaries"
@echo " make install-cargo - Explain why crates.io install is not the active path"
@echo " make git-hooks - Install repo-local pre-commit + pre-push hooks"
@echo " make precheck - Quick default cargo check"
@echo " make precheck-native - Quick native GGUF cargo check"
@echo " make manifest-check - Fail if Cargo.toml uses local path dependencies"
@echo " make check - Full local gate (fmt, check, clippy, test, build, semgrep)"
@echo " make test - Run all tests"
@echo " make test-native - Run native GGUF embedder tests"
@echo " make fmt - Format all Rust code"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Native Embeddings:"
@echo " make embeddings-info EMBEDDER_PROFILE=base|dev|premium - Show GGUF profile details"
@echo " make embeddings-hydrate EMBEDDER_PROFILE=base|dev|premium - Download selected GGUF into HF cache"
@echo " make embeddings-check - Check aicx-embeddings with GGUF backend"
@echo " make embeddings-test - Test aicx-embeddings with GGUF backend"
@echo " make embeddings-clippy - Clippy aicx-embeddings with GGUF backend"
@echo ""
@echo "Release / Version:"
@echo " make version - Alias for version-show"
@echo " make version-show - Show package version and tag state"
@echo " make version-check - Validate synced release surfaces (Cargo/docs/npm/changelog basics)"
@echo " make version-bump VERSION=X - Bump version and sync docs/npm surfaces. X={patch|minor|major|x.y.z}"
@echo " make version-patch - Alias for version-bump VERSION=patch"
@echo " make bump-patch - Alias for version-bump VERSION=patch"
@echo " make changelog-close - Close CHANGELOG '## [Unreleased]' to current version + date"
@echo " make release-notes - Print release notes body derived from CHANGELOG current version section"
@echo " make release-plan - Print the full post-merge release flow"
@echo " make release-prepare VERSION=X - version-bump + changelog-close + notes preview + precheck. X={patch|minor|major|x.y.z}"
@echo " make release-check - Strict release readiness gate"
@echo " make release-tag - Create annotated tag from Cargo.toml version"
@echo " make release-push - Push the current release tag to origin"
@echo " make package-check - Explain binary-release packaging track"
@echo " make release-bundle - Local macOS bundle + codesign + notarize using KEYS/NOTARY_PROFILE (NATIVE=1 for GGUF backend)"
@echo ""
@echo "Quick start:"
@echo " make install - Contributor/local operator setup"
@echo " make check - Full local verification"
@echo " make release-plan - Review release flow before tagging"