diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7d8750..bba0a8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,18 +3,15 @@ name: CI on: [push, pull_request] jobs: - build: + lint-typecheck-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: - node-version: "20" - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: "10.28.0" + node-version: "22" + - name: Enable Corepack + run: corepack enable - name: Install dependencies run: pnpm install --frozen-lockfile - name: Lint @@ -23,3 +20,44 @@ jobs: run: pnpm run typecheck - name: Build run: pnpm run build + - name: Test + run: pnpm test + + docker-build: + needs: lint-typecheck-test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + - name: Log in to GHCR + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build (PR, cache-from only) + if: github.event_name == 'pull_request' + uses: docker/bake-action@v5 + with: + files: docker/bake.hcl + targets: ci-core + set: |- + *.platform=linux/amd64,linux/arm64 + *.cache-to= + - name: Build and push (main) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: docker/bake-action@v5 + with: + files: docker/bake.hcl + targets: default + push: true + set: |- + *.platform=linux/amd64,linux/arm64 + CACHE_FROM_ONLY=false diff --git a/.kiro/specs/build-devx-overhaul/tasks.md b/.kiro/specs/build-devx-overhaul/tasks.md index c44a037..9b458b8 100644 --- a/.kiro/specs/build-devx-overhaul/tasks.md +++ b/.kiro/specs/build-devx-overhaul/tasks.md @@ -123,56 +123,63 @@ D). ## Phase C: Compose Collapse -- [ ] **C.1** Create `/Users/ben/Projects/wavekit/compose.yaml` per the design §3 with four profiles (`dev`, `prod-single-host`, `prod-distributed`, `demod-test`). _Requirements: 2.1–2.5_ -- [ ] **C.2** Each service with a `build:` block declares `cache_from` pointing at the relevant `ghcr.io/coriou/wavekit:cache-*` registry refs (for now this is harmless; in Phase D those refs get populated). _Requirements: 2.5_ -- [ ] **C.3** Verify `docker compose --profile dev config` parses without warnings. _Requirements: 2.1, 2.6_ -- [ ] **C.4** Verify `docker compose --profile prod-single-host config` parses without warnings. _Requirements: 2.2_ -- [ ] **C.5** Verify `docker compose --profile prod-distributed config` parses without warnings. _Requirements: 2.3_ -- [ ] **C.6** Verify `docker compose --profile demod-test config` parses without warnings. _Requirements: 2.4_ -- [ ] **C.7** Delete `/Users/ben/Projects/wavekit/docker-compose.dev.yml`. _Requirements: 2.6, 2.7_ -- [ ] **C.8** Delete `/Users/ben/Projects/wavekit/docker-compose.prod.yml`. _Requirements: 2.6_ -- [ ] **C.9** Delete `/Users/ben/Projects/wavekit/docker-compose.override.yml`. _Requirements: 2.6_ -- [ ] **C.10** Delete `/Users/ben/Projects/wavekit/docker-compose.demod-test.yml`. _Requirements: 2.6_ -- [ ] **C.11** Delete `/Users/ben/Projects/wavekit/docker/Dockerfile.demod-test`. _Requirements: 3.12_ -- [ ] **C.12** Run end-to-end: `docker compose --profile dev up --build`. wavekit-api SHALL reach `service_healthy`. `curl localhost:9000/health` SHALL return 200. _Requirements: 2.1_ +- [x] **C.1** Create `/Users/ben/Projects/wavekit/compose.yaml` per the design §3 with four profiles (`dev`, `prod-single-host`, `prod-distributed`, `demod-test`). _Requirements: 2.1–2.5_ +- [x] **C.2** Each service with a `build:` block declares `cache_from` pointing at the relevant `ghcr.io/coriou/wavekit:cache-*` registry refs (for now this is harmless; in Phase D those refs get populated). _Requirements: 2.5_ +- [x] **C.3** Verify `docker compose --profile dev config` parses without warnings. _Requirements: 2.1, 2.6_ +- [x] **C.4** Verify `docker compose --profile prod-single-host config` parses without warnings. _Requirements: 2.2_ +- [x] **C.5** Verify `docker compose --profile prod-distributed config` parses without warnings. _Requirements: 2.3_ +- [x] **C.6** Verify `docker compose --profile demod-test config` parses without warnings. _Requirements: 2.4_ +- [x] **C.7** Delete `/Users/ben/Projects/wavekit/docker-compose.dev.yml`. _Requirements: 2.6, 2.7_ +- [x] **C.8** Delete `/Users/ben/Projects/wavekit/docker-compose.prod.yml`. _Requirements: 2.6_ +- [x] **C.9** Delete `/Users/ben/Projects/wavekit/docker-compose.override.yml`. _Requirements: 2.6_ +- [x] **C.10** Delete `/Users/ben/Projects/wavekit/docker-compose.demod-test.yml`. _Requirements: 2.6_ +- [x] **C.11** Delete `/Users/ben/Projects/wavekit/docker/Dockerfile.demod-test`. _Requirements: 3.12_ +- [x] **C.12** Run end-to-end: `docker compose --profile dev up --build`. wavekit-api SHALL reach `service_healthy`. `curl localhost:9000/health` SHALL return 200. _Requirements: 2.1_ + - **Notes**: Three deviations from design.md §3 were necessary to make the gate pass; each is documented inline in compose.yaml. + 1. **sdrpp-server healthcheck**: design.md prescribes `curl -fsS http://localhost:5259/`, but SDR++ in server mode speaks its own binary protocol on 5259, not HTTP, so curl fails on every probe with `HTTP/0.9 not allowed`. Switched to `bash -c ',mode=max"`. (One-time bootstrap; subsequent updates come from CI.) _Requirements: 4.1_ +- [x] **D.7** Deleted `/Users/ben/Projects/wavekit/docker/platform-utils.sh` entirely. `build_multiarch` is replaced by bake; `detect_platform` had no consumers (verified by grep across Makefile, docker/, .github/, packages/sdr-host/). _Requirements: 5.6_ +- [ ] **D.8** (DEFERRED — user-run): Initial cache seed: from a clean local machine, log in to GHCR, run `docker buildx bake --file docker/bake.hcl default --push --set "*.cache-to=type=registry,ref=ghcr.io/coriou/wavekit:cache-,mode=max"`. (One-time bootstrap; subsequent updates come from CI.) _Requirements: 4.1_ (Phase F's CI workflow on main-branch push will populate GHCR cache via cache-to mode=max on its first run; D.8 is redundant if CI lands first.) ## Phase E: Makefile Cleanup -- [ ] **E.1** Rewrite `/Users/ben/Projects/wavekit/Makefile` with the new target inventory per design §7.1. _Requirements: 6.1_ -- [ ] **E.2** Confirm no deleted targets remain in the file. Run `make help` and verify only the targets listed in 6.1 (plus sdr-host-* and fixtures-*) appear. _Requirements: 6.2_ -- [ ] **E.3** Verify all newly-introduced echo output is emoji-free and uses no decorative ANSI colour. Existing colour in retained targets MAY remain unchanged. _Requirements: 6.3_ -- [ ] **E.4** Test the new dev flow end-to-end: +- [x] **E.1** Rewrite `/Users/ben/Projects/wavekit/Makefile` with the new target inventory per design §7.1. _Requirements: 6.1_ +- [x] **E.2** Confirm no deleted targets remain in the file. Run `make help` and verify only the targets listed in 6.1 (plus sdr-host-* and fixtures-*) appear. _Requirements: 6.2_ +- [x] **E.3** Verify all newly-introduced echo output is emoji-free and uses no decorative ANSI colour. Existing colour in retained targets MAY remain unchanged. _Requirements: 6.3_ +- [x] **E.4** Test the new dev flow end-to-end: - `make dev` SHALL start `pnpm dev` with esbuild watch + node watch. - `make dev-stack` SHALL build via buildx bake and start the dev profile. - `make dev-dashboard` SHALL connect the CLI dashboard. - `make docker-build` SHALL invoke bake. - `make demod-test` SHALL launch the demod-test profile interactively. - _Requirements: 1.1, 2.1, 2.4, 5.2, 6.1_ + - **Result**: Verified via `make --dry-run` for each headline target. `make dev` → `pnpm dev`; `make dev-stack` → `docker compose --profile dev up --build`; `make docker-build` → `docker buildx bake --file docker/bake.hcl default`; `make demod-test` → `docker compose --profile demod-test run --rm demod-test`. `cli-install` folded into `dev-dashboard-build` as planned. New Makefile is 156 lines / 34 targets (down from 386 / 63). ## Phase F: CI Workflow -- [ ] **F.1** Rewrite `/Users/ben/Projects/wavekit/.github/workflows/ci.yml` per design §8. _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_ -- [ ] **F.2** Update Node to `22` and pnpm install to use Corepack (matches the Dockerfile pattern). _Requirements: 7.5_ -- [ ] **F.3** Add the `lint-typecheck-test` job that runs lint, typecheck, build, AND test (test was missing from the old workflow). _Requirements: 7.1_ -- [ ] **F.4** Add the `docker-build` job that depends on `lint-typecheck-test`, sets up QEMU + buildx, and runs bake. _Requirements: 7.2_ -- [ ] **F.5** PR path: `--cache-from` only, no login, no push. _Requirements: 7.3_ -- [ ] **F.6** Main-branch path: log in to GHCR, build `default` group with `--push` and `--cache-to mode=max`. _Requirements: 7.4_ -- [ ] **F.7** Smoke-test by opening a draft PR with a no-op change and verifying both jobs run and succeed. _Requirements: 7.1, 7.2, 7.3_ +- [x] **F.1** Rewrite `/Users/ben/Projects/wavekit/.github/workflows/ci.yml` per design §8. _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_ +- [x] **F.2** Update Node to `22` and pnpm install to use Corepack (matches the Dockerfile pattern). _Requirements: 7.5_ +- [x] **F.3** Add the `lint-typecheck-test` job that runs lint, typecheck, build, AND test (test was missing from the old workflow). _Requirements: 7.1_ +- [x] **F.4** Add the `docker-build` job that depends on `lint-typecheck-test`, sets up QEMU + buildx, and runs bake. _Requirements: 7.2_ +- [x] **F.5** PR path: `--cache-from` only, no login, no push. _Requirements: 7.3_ +- [x] **F.6** Main-branch path: log in to GHCR, build `default` group with `--push` and `--cache-to mode=max`. _Requirements: 7.4_ +- [ ] **F.7** (DEFERRED — user-run): Smoke-test by opening a draft PR with a no-op change and verifying both jobs run and succeed. _Requirements: 7.1, 7.2, 7.3_ (Pre-merge gate: maintainer opens a draft PR after PR 2 is up; cold-cache PR build expected to take 15-25 min with no-cache fallback per Req 4.3, future PRs ~2-5 min with cache hits.) > **Checkpoint 2**: At this point the new build system is fully functional. > Phases G and H are documentation + sdr-host alignment polish. diff --git a/Makefile b/Makefile index 33fc4a0..1a67e8e 100644 --- a/Makefile +++ b/Makefile @@ -1,122 +1,103 @@ -.PHONY: help docker-build docker-build-full docker-build-core docker-build-sdrpp \ - docker-dev docker-prod docker-push docker-clean docker-logs \ - docker-shell docker-compose-up docker-compose-down \ - cli-install dev-dashboard-build dev-dashboard \ +# WaveKit Makefile +# Lifecycle-focused targets for dev, build, push, and ops. +# Run `make help` for the full inventory. + +.DEFAULT_GOAL := help + +.PHONY: help \ + dev dev-dashboard dev-dashboard-build dev-configs \ + dev-stack dev-stack-down dev-stack-logs dev-shell dev-status \ + docker-init docker-build docker-push docker-clean docker-prune \ + demod-test \ sdr-host-build sdr-host-build-multi sdr-host-install sdr-host-init \ sdr-host-up sdr-host-update sdr-host-down sdr-host-restart \ sdr-host-logs sdr-host-status sdr-host-health sdr-host-compose-update \ - sdr-host-clean - -# WaveKit Docker Makefile -# Quick commands for development and deployment -# Usage: make [target] - -.DEFAULT_GOAL := help + sdr-host-clean \ + fixtures-download fixtures-download-all fixtures-convert \ + fixtures-test fixtures-test-local -# Variables -REGISTRY ?= -IMAGE_NAME ?= wavekit -TAG ?= latest -BUILDKIT ?= 1 -COMPOSE_DEV := docker-compose -f docker-compose.dev.yml -COMPOSE_PROD := docker-compose -f docker-compose.prod.yml -f docker-compose.override.yml +# SDR-host variables (preserved from prior Makefile) SDR_HOST_TAG ?= latest SDR_HOST_PLATFORMS ?= linux/arm64 -# Colors for output +# Dev container (used by fixtures-test which exec's into the dev stack) +DEV_CONTAINER ?= wavekit-api + +# Colors for retained sdr-host-* / fixtures-* output (newly-introduced +# targets below are emoji-free and ANSI-free per Requirement 6.3). BLUE := \033[0;34m GREEN := \033[0;32m YELLOW := \033[1;33m RED := \033[0;31m -NC := \033[0m # No Color +NC := \033[0m help: ## Show this help message - @echo "$(BLUE)WaveKit Docker Commands$(NC)" - @echo "" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-25s$(NC) %s\n", $$1, $$2}' - -# Docker Build Commands - -docker-build: ## Build all Docker images (full, core, sdrpp) - @echo "$(BLUE)Building all WaveKit images...$(NC)" - @DOCKER_BUILDKIT=$(BUILDKIT) ./docker/build.sh full $(TAG) - @DOCKER_BUILDKIT=$(BUILDKIT) ./docker/build.sh core $(TAG) - @DOCKER_BUILDKIT=$(BUILDKIT) ./docker/build.sh sdrpp $(TAG) - @echo "$(GREEN)✅ All images built successfully$(NC)" - -docker-build-full: ## Build full mode image - @echo "$(BLUE)Building WaveKit full mode...$(NC)" - @DOCKER_BUILDKIT=$(BUILDKIT) ./docker/build.sh full $(TAG) - @docker image inspect $(IMAGE_NAME):$(TAG) --format="Size: {{.Size}}" | numfmt --to=iec - -docker-build-core: ## Build core mode image - @echo "$(BLUE)Building WaveKit core mode...$(NC)" - @DOCKER_BUILDKIT=$(BUILDKIT) ./docker/build.sh core $(TAG) - -docker-build-sdrpp: ## Build SDR++-only mode image - @echo "$(BLUE)Building WaveKit SDR++ mode...$(NC)" - @DOCKER_BUILDKIT=$(BUILDKIT) ./docker/build.sh sdrpp $(TAG) - -# Docker Compose Commands - -docker-dev: ## Start development environment - @echo "$(BLUE)Starting WaveKit development environment...$(NC)" - @docker image inspect $(IMAGE_NAME):$(TAG)-core >/dev/null 2>&1 || (echo "$(YELLOW)Core image missing; building...$(NC)" && DOCKER_BUILDKIT=$(BUILDKIT) ./docker/build.sh core $(TAG)) - @docker image inspect $(IMAGE_NAME):$(TAG)-sdrpp >/dev/null 2>&1 || (echo "$(YELLOW)SDR++ image missing; building...$(NC)" && DOCKER_BUILDKIT=$(BUILDKIT) ./docker/build.sh sdrpp $(TAG)) - @$(COMPOSE_DEV) up -d - @echo "$(GREEN)✅ Development environment started$(NC)" - @echo "" - @echo "$(YELLOW)Services:$(NC)" - @echo " API: http://localhost:9000" - @echo " WebSocket: ws://localhost:4713" - @echo " Audio: nc localhost 8080" - @echo " LiveAudio: http://localhost:8081/stream" - @echo " IDE: http://localhost:3000" - -docker-prod: docker-build ## Start production environment - @echo "$(BLUE)Starting WaveKit production environment...$(NC)" - @$(COMPOSE_PROD) up -d - @echo "$(GREEN)✅ Production environment started$(NC)" - @echo "" - @$(COMPOSE_PROD) ps - -docker-compose-up: ## Start Docker Compose - @$(COMPOSE_DEV) up - -docker-compose-down: ## Stop Docker Compose - @echo "$(YELLOW)Stopping Docker Compose...$(NC)" - @$(COMPOSE_DEV) down - @echo "$(GREEN)✅ Stopped$(NC)" - -docker-compose-logs: ## Show Docker Compose logs - @$(COMPOSE_DEV) logs -f - -# Docker Utilities - -docker-push: ## Push images to registry (requires REGISTRY variable) - @if [ -z "$(REGISTRY)" ]; then \ - echo "$(RED)Error: REGISTRY not set$(NC)"; \ - echo "Usage: make docker-push REGISTRY=docker.io/myuser"; \ - exit 1; \ - fi - @echo "$(BLUE)Pushing images to $(REGISTRY)...$(NC)" - @./docker/push.sh $(TAG) $(REGISTRY) - -docker-logs: ## Tail WaveKit logs - @docker logs -f wavekit - -docker-logs-api: ## Tail API logs - @docker exec -it wavekit tail -f /var/log/wavekit/wavekit.log - -docker-run-core: ## Run core mode manually (requires wavekit:core) - @echo "$(BLUE)Running WaveKit core mode...$(NC)" - @docker run -p 3000:3000 -p 8080:8080 -p 8081:8081 \ - -v $(shell pwd)/config/dev_test.yaml:/app/config/default.yaml \ - wavekit:core - -# ============================================================================== -# SDR Host Commands -# ============================================================================== + @awk 'BEGIN {FS = ":.*?## "} \ + /^# === / {sub(/^# === /, ""); sub(/ ===$$/, ""); printf "\n%s\n", $$0; next} \ + /^[a-zA-Z][a-zA-Z0-9_-]+:.*?## / {printf " %-25s %s\n", $$1, $$2}' \ + $(MAKEFILE_LIST) + +# === Native dev loop (no Docker) === + +dev: ## Run app natively with hot-reload (esbuild watch + node --watch, no Docker) + @pnpm dev + +dev-dashboard: dev-dashboard-build ## Launch Ink/React CLI dashboard (interactive) + @WAVEKIT_WS_URLS=ws://localhost:9000/ws,ws://localhost:4713/ws node ./cli/dist/cli.js + +dev-dashboard-build: ## Build CLI dashboard bundle (installs cli deps if missing) + @test -d cli/node_modules || pnpm --filter @wavekit/cli install --silent + @rm -rf cli/node_modules/.cache cli/dist + @pnpm --filter @wavekit/cli build + +dev-configs: ## List available configs in config/ + @echo "[wavekit] Available configs:" + @ls -1 config/*.yaml | xargs -I {} basename {} .yaml | sort + +# === Container integration (full stack) === + +dev-stack: ## Bring up dev profile (sdrpp-server + wavekit-api) with build + docker compose --profile dev up --build + +dev-stack-down: ## Stop dev profile and remove containers + docker compose --profile dev down + +dev-stack-logs: ## Follow logs from dev profile services + docker compose --profile dev logs -f + +dev-shell: ## Open shell in dev wavekit-api container (must be running via dev-stack) + docker compose --profile dev exec wavekit-api /bin/bash + +dev-status: ## Show dev stack container status and probe /health + @docker compose --profile dev ps + @echo + @curl -fsS http://localhost:9000/health 2>/dev/null && echo " OK" || echo "[wavekit] /health not reachable" + +# === Build / push === + +docker-init: ## Bootstrap buildx builder + networks + volumes (run once) + bash docker/init.sh + +docker-build: ## Build all default-group images via buildx bake (final, final-core, final-sdrpp) + docker buildx bake --file docker/bake.hcl default + +docker-push: ## Push images to GHCR (multi-arch, mode=max cache write) + bash docker/push.sh + +docker-clean: ## Stop compose profiles and remove named volumes + docker compose --profile dev down -v --remove-orphans 2>/dev/null || true + docker compose --profile prod-single-host down -v --remove-orphans 2>/dev/null || true + docker compose --profile prod-distributed down -v --remove-orphans 2>/dev/null || true + +docker-prune: ## Prune unused Docker resources (dangling images, stopped containers, networks) + docker system prune -f + +# === Demod tooling === + +demod-test: ## Launch interactive demod test environment (sox/ffmpeg/dsd-fme/multimon-ng/csdr) + docker compose --profile demod-test run --rm demod-test + +# === Pi-hosted SDR === sdr-host-build: ## Build & publish sdr-host image (default: arm64) @bash ./packages/sdr-host/scripts/build-publish.sh --tag $(SDR_HOST_TAG) --platform $(SDR_HOST_PLATFORMS) @@ -157,218 +138,7 @@ sdr-host-compose-update: ## Refresh sdr-host compose file (run on host) sdr-host-clean: ## Docker cleanup helper (run on host) @bash ./packages/sdr-host/scripts/docker-cleanup.sh -# ============================================================================== -# Dev Testing Commands (State of the Art DevX) -# Usage: make dev-build && make dev-start -# ============================================================================== - -DEV_CONTAINER := wavekit-dev -DEV_IMAGE := wavekit:latest-core -DEV_PORT_API ?= 9000 -DEV_PORT_AUDIO ?= 8080 -DEV_PORT_LIVE ?= 8081 -DEV_PORT_TUNER ?= 1234 - -# Config selection: make dev-up CONFIG=dev_acars (defaults to dev_test) -CONFIG ?= dev_test -DEV_CONFIG := $(shell pwd)/config/$(CONFIG).yaml - -dev-build: ## Build core mode image (for external SDR++) - @echo "$(BLUE)Building WaveKit core image...$(NC)" - @./docker/build.sh core latest - @echo "$(GREEN)✅ Built $(DEV_IMAGE)$(NC)" - -dev-start: ## Start dev container (stops existing first) - @echo "$(BLUE)Starting $(DEV_CONTAINER)...$(NC)" - @docker stop $(DEV_CONTAINER) 2>/dev/null || true - @docker rm $(DEV_CONTAINER) 2>/dev/null || true - @mkdir -p $(shell pwd)/debug_audio - @docker run --rm -d --name $(DEV_CONTAINER) \ - -p $(DEV_PORT_API):3000 -p $(DEV_PORT_AUDIO):8080 -p $(DEV_PORT_LIVE):8081 -p $(DEV_PORT_TUNER):1234 \ - -v $(DEV_CONFIG):/app/config/default.yaml \ - -v $(shell pwd)/debug_audio:/data/debug_audio \ - -v $(shell pwd)/decoded_calls:/app/decoded_calls \ - $(DEV_IMAGE) - @echo "" - @echo "$(GREEN)✅ WaveKit running!$(NC)" - @echo "" - @echo " $(BLUE)API$(NC) http://localhost:$(DEV_PORT_API)" - @echo " $(BLUE)Health$(NC) http://localhost:$(DEV_PORT_API)/health" - @echo " $(BLUE)Audio$(NC) nc localhost $(DEV_PORT_AUDIO) | play -t raw -r 48000 -e signed -b 16 -c 1 -" - @echo " $(BLUE)Live Audio$(NC) http://localhost:$(DEV_PORT_LIVE)/stream" - @echo " $(BLUE)Tuner$(NC) rtl_tcp on localhost:$(DEV_PORT_TUNER) (SDR++)" - @echo "" - @echo " $(YELLOW)Commands:$(NC)" - @echo " make dev-logs - All logs" - @echo " make dev-dashboard - Interactive CLI dashboard" - @echo " make dev-stop - Stop container" - @echo "" - -dev-stop: ## Stop dev container - @echo "$(YELLOW)Stopping $(DEV_CONTAINER)...$(NC)" - @docker stop $(DEV_CONTAINER) 2>/dev/null || echo "Not running" - @echo "$(GREEN)✅ Stopped$(NC)" - -dev-restart: dev-stop dev-start ## Restart dev container - -dev-up: dev-build dev-start ## Build and start (CONFIG=name to use config/name.yaml) - -dev-logs: ## Tail all container logs (pretty JSON) - @docker logs -f $(DEV_CONTAINER) 2>&1 | jq -R 'fromjson? // .' || docker logs -f $(DEV_CONTAINER) - -dev-logs-raw: ## Tail raw container logs - @docker logs -f $(DEV_CONTAINER) - -dev-audio: ## Listen to decoded audio (requires sox) - @echo "$(BLUE)Streaming audio from WaveKit... (Ctrl+C to stop)$(NC)" - @nc localhost $(DEV_PORT_AUDIO) | play -t raw -r 48000 -e signed -b 16 -c 1 - - -dev-debug-audio: ## Convert and list debug audio recordings - @echo "$(BLUE)Debug Audio Recordings:$(NC)" - @ls -lah debug_audio/*.raw 2>/dev/null || echo "No recordings yet" - @echo "" - @echo "$(BLUE)Converting to WAV...$(NC)" - @./scripts/convert-debug-audio.sh debug_audio 2>/dev/null || echo "No files to convert" - @echo "" - @echo "$(GREEN)To play recordings:$(NC) play debug_audio/*.wav" - @echo "$(GREEN)To analyze:$(NC) sox debug_audio/*.wav -n spectrogram -o spectrum.png" - -dev-shell: ## Open shell in dev container - @docker exec -it $(DEV_CONTAINER) /bin/bash - -dev-status: ## Show container status and health - @echo "$(BLUE)Container Status:$(NC)" - @docker ps --filter name=$(DEV_CONTAINER) --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" || echo "Not running" - @echo "" - @echo "$(BLUE)Health Check:$(NC)" - @curl -s http://localhost:$(DEV_PORT_API)/health | jq . 2>/dev/null || echo "API not reachable" - -cli-install: ## Install CLI dashboard dependencies (if missing) - @test -d cli/node_modules || pnpm --filter @wavekit/cli install - -dev-dashboard-build: cli-install ## Build CLI dashboard - @rm -rf cli/node_modules/.cache cli/dist - @pnpm --filter @wavekit/cli build - -dev-dashboard: dev-dashboard-build ## WaveKit CLI Dashboard (interactive, tab-based) - @WAVEKIT_WS_URLS=ws://localhost:9000/ws,ws://localhost:4713/ws node ./cli/dist/cli.js - -dev-configs: ## List available configs - @echo "$(BLUE)Available configs:$(NC)" - @ls -1 config/*.yaml | xargs -I {} basename {} .yaml | sort - @echo "" - @echo "Usage: make dev-up CONFIG=" - - -docker-logs-sdrpp: ## Tail SDR++ logs - @docker exec -it wavekit tail -f /var/log/wavekit/sdrpp.log - -docker-logs-decoders: ## Tail decoder logs - @docker exec -it wavekit tail -f /var/log/wavekit/decoders.log - -docker-shell: ## Open shell in WaveKit container - @docker exec -it wavekit /bin/bash - -docker-status: ## Show WaveKit service status - @echo "$(BLUE)Service Status:$(NC)" - @docker exec wavekit s6-rc-status || echo "Container not running" - @echo "" - @echo "$(BLUE)Container Health:$(NC)" - @docker inspect wavekit --format='{{json .State.Health}}' | jq . || echo "Container not running" - -docker-health: ## Check container health - @docker exec wavekit /etc/s6-overlay/scripts/healthcheck.sh - -docker-ps: ## List running containers - @docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - -docker-clean: ## Remove WaveKit containers and volumes - @echo "$(YELLOW)Cleaning up WaveKit containers...$(NC)" - @docker-compose -f docker-compose.dev.yml down -v 2>/dev/null || true - @docker-compose -f docker-compose.prod.yml down -v 2>/dev/null || true - @docker volume rm wavekit-config wavekit-logs recordings 2>/dev/null || true - @echo "$(GREEN)✅ Cleanup complete$(NC)" - -docker-prune: ## Prune unused Docker resources - @echo "$(YELLOW)Pruning unused resources...$(NC)" - @docker image prune -a -f --filter "label!=keep=true" - @docker container prune -f - @docker volume prune -f - @docker network prune -f - @echo "$(GREEN)✅ Pruned successfully$(NC)" - -# Testing & Validation - -docker-test: ## Run tests in container - @docker run --rm \ - -v $(PWD):/app \ - -w /app \ - $(IMAGE_NAME):$(TAG) \ - pnpm test - -docker-test-coverage: ## Run tests with coverage - @docker run --rm \ - -v $(PWD):/app \ - -w /app \ - $(IMAGE_NAME):$(TAG) \ - pnpm run test:coverage - -docker-lint: ## Run linting in container - @docker run --rm \ - -v $(PWD):/app \ - -w /app \ - $(IMAGE_NAME):$(TAG) \ - pnpm run lint - -# Information & Debugging - -docker-info: ## Show Docker build info - @echo "$(BLUE)Docker Build Configuration:$(NC)" - @echo "Registry: $(REGISTRY)" - @echo "Image: $(IMAGE_NAME)" - @echo "Tag: $(TAG)" - @echo "BuildKit: $(BUILDKIT)" - @echo "" - @echo "$(BLUE)Environment:$(NC)" - @env | grep -E "^(DOCKER_|BUILDKIT_)" || echo "None set" - -docker-inspect: ## Inspect built image - @docker inspect $(IMAGE_NAME):$(TAG) | jq '.[0] | {Repo: .RepoTags, Size: .Size, Created: .Created, Architecture: .Architecture, Os: .Os}' - -docker-history: ## Show image layer history - @docker history --human $(IMAGE_NAME):$(TAG) - -# Convenience - -install-buildx: ## Install Docker buildx (for multi-platform builds) - @echo "$(BLUE)Installing docker buildx...$(NC)" - @driver=$$(docker buildx inspect wavekit-builder 2>/dev/null | awk -F': ' '/Driver:/ {print $$2}'); \ - if [ "$$driver" = "docker" ]; then \ - echo "$(YELLOW)Builder uses docker driver; recreating for multi-arch$(NC)"; \ - docker buildx rm wavekit-builder >/dev/null; \ - fi; \ - if ! docker buildx inspect wavekit-builder >/dev/null 2>&1; then \ - docker buildx create --name wavekit-builder --driver docker-container --config docker/buildkit.toml --use >/dev/null; \ - else \ - docker buildx use wavekit-builder >/dev/null; \ - fi; \ - docker buildx inspect wavekit-builder --bootstrap >/dev/null; \ - echo "$(GREEN)✅ buildx ready$(NC)" - -demo: docker-build-full docker-dev ## Build and run demo - @echo "" - @echo "$(GREEN)✅ Demo ready!$(NC)" - @echo "" - @echo "API Health: curl http://localhost:9000/health" - @echo "Full Status: curl http://localhost:9000/api/status" - @echo "WebSocket: wscat -c ws://localhost:4713" - @echo "" - @echo "View logs: make docker-logs" - @echo "Stop demo: make docker-compose-down" - -# ============================================================================== -# Fixture Testing (IQ-based decoder validation) -# ============================================================================== +# === Fixtures === fixtures-download: ## Download test fixtures (small ones by default) @./fixtures/download.sh diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..b56a35c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,197 @@ +name: wavekit + +services: + + # ---------- dev profile ---------- + sdrpp-server: + profiles: ["dev"] + image: wavekit:dev-sdrpp + build: + context: . + target: final-sdrpp + cache_from: + - type=registry,ref=ghcr.io/coriou/wavekit:cache-final-sdrpp + - type=registry,ref=ghcr.io/coriou/wavekit:cache-sdrpp-build + - type=registry,ref=ghcr.io/coriou/wavekit:cache-runtime-base + - type=registry,ref=ghcr.io/coriou/wavekit:cache-base-build + container_name: wavekit-sdrpp + ports: + - "5259:5259" + networks: [wavekit] + # SDR++ server speaks its own binary protocol on 5259, not HTTP, so the + # healthcheck verifies TCP reachability via bash's /dev/tcp rather than curl. + # start_period accommodates SDR++'s noisy first-run config-file initialization. + healthcheck: + test: ["CMD-SHELL", "bash -c '/dev/null 2>&1"] + interval: 5s + timeout: 3s + retries: 12 + start_period: 60s + + wavekit-api: + profiles: ["dev"] + image: wavekit:dev-core + build: + context: . + target: final-core + cache_from: + - type=registry,ref=ghcr.io/coriou/wavekit:cache-final-core + - type=registry,ref=ghcr.io/coriou/wavekit:cache-final-base + - type=registry,ref=ghcr.io/coriou/wavekit:cache-node-build + - type=registry,ref=ghcr.io/coriou/wavekit:cache-runtime-base + - type=registry,ref=ghcr.io/coriou/wavekit:cache-base-build + container_name: wavekit-api + depends_on: + sdrpp-server: + condition: service_healthy + environment: + WAVEKIT_LOG_LEVEL: debug + NODE_ENV: development + SDR_SOURCE: "tcp://sdrpp-server:5259" + # config/default.yaml has api.port=3000 (the historical container-internal + # port); host port 9000 is mapped to container 3000 to match the previous + # `make dev-up -p 9000:3000` workflow. The same translation applies to the + # tuner port 1234 (mapped to host 4713 via the WebSocket convention). + # When the API config is updated to bind 9000 directly, simplify to 9000:9000. + ports: + - "9000:3000" + - "8080:8080" + - "8081:8081" + - "4713:4713" + networks: [wavekit] + volumes: + - ./config:/app/config:ro + - ./logs:/var/log/wavekit + - ./decoded_calls:/app/decoded_calls + # Container-internal port is 3000 (config/default.yaml api.port); host port + # 9000 is mapped above. The healthcheck runs inside the container, so probe + # localhost:3000. The user-facing curl http://localhost:9000/health works. + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + cap_add: + - SYS_NICE + - NET_RAW + + # ---------- prod-single-host profile ---------- + wavekit-full: + profiles: ["prod-single-host"] + image: ghcr.io/coriou/wavekit:latest + container_name: wavekit + restart: unless-stopped + environment: + WAVEKIT_LOG_LEVEL: info + NODE_ENV: production + RTL_TCP_HOST: "${RTL_TCP_HOST:-127.0.0.1}" + RTL_TCP_PORT: "${RTL_TCP_PORT:-1234}" + ports: + - "9000:9000" + - "8080:8080" + - "5259:5259" + - "4713:4713" + networks: [wavekit] + volumes: + - wavekit-config:/app/config + - wavekit-logs:/var/log/wavekit + - recordings:/recordings + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:9000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + deploy: + resources: + limits: + cpus: "2" + memory: 1G + cap_add: + - SYS_NICE + - NET_RAW + security_opt: + - "no-new-privileges:true" + + # ---------- prod-distributed profile ---------- + wavekit-sdrpp-prod: + profiles: ["prod-distributed"] + image: ghcr.io/coriou/wavekit:latest-sdrpp + container_name: wavekit-sdrpp-prod + restart: unless-stopped + environment: + LOG_LEVEL: info + ports: + - "5259:5259" + networks: [wavekit] + # SDR++ server speaks its own binary protocol on 5259 (not HTTP), so the + # healthcheck verifies TCP reachability rather than HTTP. + healthcheck: + test: ["CMD-SHELL", "bash -c '/dev/null 2>&1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + + wavekit-core-prod: + profiles: ["prod-distributed"] + image: ghcr.io/coriou/wavekit:latest-core + container_name: wavekit-core-prod + restart: unless-stopped + depends_on: + wavekit-sdrpp-prod: + condition: service_healthy + environment: + WAVEKIT_LOG_LEVEL: info + NODE_ENV: production + SDR_SOURCE: "tcp://wavekit-sdrpp-prod:5259" + ports: + - "9000:9000" + - "8080:8080" + - "4713:4713" + networks: [wavekit] + volumes: + - wavekit-config:/app/config + - wavekit-logs:/var/log/wavekit + - recordings:/recordings + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:9000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + cap_add: + - SYS_NICE + - NET_RAW + security_opt: + - "no-new-privileges:true" + + # ---------- demod-test profile ---------- + demod-test: + profiles: ["demod-test"] + image: wavekit:dev-demod + build: + context: . + target: final-demod + cache_from: + - type=registry,ref=ghcr.io/coriou/wavekit:cache-final-demod + - type=registry,ref=ghcr.io/coriou/wavekit:cache-runtime-base + - type=registry,ref=ghcr.io/coriou/wavekit:cache-base-build + volumes: + - ./debug_audio:/data/debug_audio + - ./scripts:/scripts + - ./output:/output + working_dir: /workspace + stdin_open: true + tty: true + networks: [wavekit] + +networks: + wavekit: + driver: bridge + +volumes: + wavekit-config: + wavekit-logs: + recordings: diff --git a/docker-compose.demod-test.yml b/docker-compose.demod-test.yml deleted file mode 100644 index 0a523bb..0000000 --- a/docker-compose.demod-test.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Demodulation Testing Environment -# Usage: -# docker compose -f docker-compose.demod-test.yml build -# docker compose -f docker-compose.demod-test.yml run --rm demod-test -# -# Interactive shell with all tools pre-installed: -# docker compose -f docker-compose.demod-test.yml run --rm demod-test bash - -services: - demod-test: - build: - context: . - dockerfile: docker/Dockerfile.demod-test - volumes: - - ./debug_audio:/data/debug_audio - - ./scripts:/scripts - - ./output:/output - working_dir: /workspace - stdin_open: true - tty: true diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index e73a31f..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,168 +0,0 @@ -# WaveKit Development Environment -# Features: Live reload, hot debugging, volume mounts for development -# Usage: docker-compose -f docker-compose.dev.yml up - -services: - rtl-tcp: - # Optional: Mock RTL-TCP service (comment out if using real hardware) - image: alpine:latest - command: nc -l -p 1234 < /dev/zero - container_name: rtl-tcp-mock - ports: - - "1234:1234" - networks: - - wavekit - healthcheck: - test: ["CMD", "nc", "-z", "localhost", "1234"] - interval: 5s - timeout: 2s - retries: 3 - - sdrpp-server: - # SDR++ Server - IQ stream provider - image: wavekit:latest-sdrpp - build: - context: . - dockerfile: Dockerfile - target: final-sdrpp - cache_from: - - type=local,src=./.docker-cache - cache_to: - - type=local,dest=./.docker-cache,mode=max - container_name: wavekit-sdrpp - depends_on: - rtl-tcp: - condition: service_healthy - environment: - LOG_LEVEL: debug - ports: - - "5259:5259" - networks: - - wavekit - volumes: - - sdrpp-data:/sdr - - ./logs/sdrpp:/var/log/sdrpp - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5259/"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 20s - - wavekit-api: - # Main WaveKit API + Decoders - image: wavekit:latest-core - build: - context: . - dockerfile: Dockerfile - target: final-core # Core mode - SDR++ is external service - cache_from: - - type=local,src=./.docker-cache - cache_to: - - type=local,dest=./.docker-cache,mode=max - container_name: wavekit-api - depends_on: - sdrpp-server: - condition: service_healthy - environment: - WAVEKIT_LOG_LEVEL: debug - WAVEKIT_CONFIG_PATH: /app/config - NODE_ENV: development - SDR_SOURCE: "tcp://sdrpp-server:5259" # Connect to external SDR++ - # Node.js debugging - NODE_OPTIONS: "--inspect=0.0.0.0:9229" - ports: - - "9000:9000" # REST API - - "8080:8080" # Audio output TCP - - "8081:8081" # Live demod HTTP audio - - "4713:4713" # WebSocket events - - "9229:9229" # Node.js inspector for debugging - networks: - - wavekit - volumes: - # Source code for hot reload - - ./src:/app/src:ro # TypeScript source (read-only) - - ./dist:/app/dist # Live TypeScript output - - ./config:/app/config # Config files - - # Decoder configuration directories - - ./config/decoders:/app/config/decoders # Decoder-specific configs - - ./config/dsd-fme:/app/config/dsd-fme # dsd-fme config - - ./config/multimon-ng:/app/config/multimon-ng # multimon-ng config - - ./config/rtl433:/app/config/rtl433 # rtl_433 config - - ./config/acarsdec:/app/config/acarsdec # acarsdec config - - ./config/ais-catcher:/app/config/ais-catcher # AIS-catcher config - - ./config/direwolf:/app/config/direwolf # direwolf config - - ./config/dumpvdl2:/app/config/dumpvdl2 # dumpvdl2 config - - ./config/readsb:/app/config/readsb # readsb config - - # Output directories - - ./logs:/var/log/wavekit # Logs - - ./decoded_calls:/app/decoded_calls # Decoded audio files - - recordings:/recordings # Recording storage - - # Node modules (preserve across rebuilds) - - node-modules:/app/node_modules - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/health"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 30s - cap_add: - - SYS_NICE # Process priority for decoders - - NET_RAW # Network operations - - nginx-reverse-proxy: - # Optional: Nginx reverse proxy for cleaner URLs - image: nginx:alpine - container_name: wavekit-nginx - depends_on: - - wavekit-api - volumes: - - ./docker/nginx.dev.conf:/etc/nginx/nginx.conf:ro - ports: - - "80:80" - networks: - - wavekit - healthcheck: - test: - [ - "CMD", - "wget", - "--quiet", - "--tries=1", - "--spider", - "http://localhost/health", - ] - interval: 10s - timeout: 5s - retries: 3 - - # Optional: Browser-based IDE for development - vscode-server: - image: codercom/code-server:latest - container_name: wavekit-ide - environment: - PASSWORD: wavekit-dev - SUDO_PASSWORD: wavekit-dev - volumes: - - .:/home/coder/project - - vscode-extensions:/home/coder/.local/share/code-server - ports: - - "3000:8080" - networks: - - wavekit - -networks: - wavekit: - driver: bridge - ipam: - config: - - subnet: 172.28.0.0/16 - -volumes: - sdrpp-data: - node-modules: - vscode-extensions: - recordings: diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index 873a9bf..0000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,40 +0,0 @@ -# WaveKit Override Configuration for Local Production Testing -# Usage: docker-compose -f docker-compose.prod.yml -f docker-compose.override.yml up -d - -services: - wavekit-full: - # Local development overrides - build: - context: . - dockerfile: Dockerfile - target: final - cache_from: - - type=local,src=./.docker-cache - cache_to: - - type=local,dest=./.docker-cache,mode=max - image: wavekit:dev - environment: - WAVEKIT_LOG_LEVEL: debug - NODE_ENV: development - - wavekit-sdrpp: - build: - context: . - dockerfile: Dockerfile - target: final-sdrpp - cache_from: - - type=local,src=./.docker-cache - cache_to: - - type=local,dest=./.docker-cache,mode=max - image: wavekit:dev-sdrpp - - wavekit-core: - build: - context: . - dockerfile: Dockerfile - target: final-core - cache_from: - - type=local,src=./.docker-cache - cache_to: - - type=local,dest=./.docker-cache,mode=max - image: wavekit:dev-core diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index ebd6693..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,158 +0,0 @@ -# WaveKit Production Environment -# Features: Optimized images, resource limits, health checks, security hardening -# Usage: docker-compose -f docker-compose.prod.yml -f docker-compose.override.yml up -d - -services: - wavekit-full: - # Full mode: Everything in one container (recommended for Raspberry Pi) - image: wavekit:latest - container_name: wavekit - restart: unless-stopped - environment: - WAVEKIT_LOG_LEVEL: info - WAVEKIT_CONFIG_PATH: /app/config - NODE_ENV: production - # Connect to rtl_tcp on Raspberry Pi - RTL_TCP_HOST: "${RTL_TCP_HOST:-192.168.1.100}" - RTL_TCP_PORT: "${RTL_TCP_PORT:-1234}" - ports: - - "9000:9000" # REST API - - "8080:8080" # Audio output TCP - - "4713:4713" # WebSocket events - - "5259:5259" # SDR++ server (optional, for external clients) - networks: - - wavekit - volumes: - - wavekit-config:/app/config - - wavekit-logs:/var/log/wavekit - - recordings:/recordings - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - deploy: - resources: - limits: - cpus: "2" - memory: 1G - reservations: - cpus: "1" - memory: 512M - cap_add: - - SYS_NICE - - NET_RAW - security_opt: - - no-new-privileges:true - - # Optional: Multi-host setup (distributed mode) - wavekit-sdrpp: - # Dedicated SDR++ server on Pi or other host - image: wavekit:latest-sdrpp - container_name: wavekit-sdrpp - restart: unless-stopped - environment: - LOG_LEVEL: info - ports: - - "5259:5259" - networks: - - wavekit - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5259/"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 20s - deploy: - resources: - limits: - cpus: "1" - memory: 512M - reservations: - cpus: "0.5" - memory: 256M - - wavekit-core: - # Core mode: API + decoders (connect to external SDR++) - image: wavekit:latest-core - container_name: wavekit-core - restart: unless-stopped - depends_on: - - wavekit-sdrpp - environment: - WAVEKIT_LOG_LEVEL: info - WAVEKIT_CONFIG_PATH: /app/config - NODE_ENV: production - SDR_SOURCE: "tcp://wavekit-sdrpp:5259" - ports: - - "9000:9000" - - "8080:8080" - - "4713:4713" - networks: - - wavekit - volumes: - - wavekit-config:/app/config - - wavekit-logs:/var/log/wavekit - - recordings:/recordings - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - deploy: - resources: - limits: - cpus: "1.5" - memory: 768M - reservations: - cpus: "0.75" - memory: 384M - - # Monitoring & Logs (optional) - prometheus: - image: prom/prometheus:latest - container_name: wavekit-prometheus - restart: unless-stopped - volumes: - - ./docker/prometheus.yml:/etc/prometheus/prometheus.yml:ro - - prometheus-data:/prometheus - ports: - - "9090:9090" - networks: - - wavekit - command: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - grafana: - image: grafana/grafana:latest - container_name: wavekit-grafana - restart: unless-stopped - environment: - GF_SECURITY_ADMIN_PASSWORD: admin - GF_SECURITY_ADMIN_USER: admin - volumes: - - grafana-data:/var/lib/grafana - - ./docker/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro - ports: - - "3000:3000" - depends_on: - - prometheus - networks: - - wavekit - -networks: - wavekit: - driver: bridge - ipam: - config: - - subnet: 172.28.0.0/16 - -volumes: - wavekit-config: - wavekit-logs: - recordings: - prometheus-data: - grafana-data: diff --git a/docker/Dockerfile.demod-test b/docker/Dockerfile.demod-test deleted file mode 100644 index 8b16528..0000000 --- a/docker/Dockerfile.demod-test +++ /dev/null @@ -1,108 +0,0 @@ -# Demodulation Testing Container -# Pre-installed with all SDR/demodulation tools for rapid testing -FROM ubuntu:22.04 - -ENV DEBIAN_FRONTEND=noninteractive - -# Base tools and Python -RUN apt-get update && apt-get install -y \ - build-essential \ - cmake \ - git \ - pkg-config \ - wget \ - curl \ - vim \ - netcat-openbsd \ - # Audio tools - sox \ - libsox-fmt-all \ - ffmpeg \ - # Python stack (NOT numpy/scipy from apt - we install via pip for version control) - python3 \ - python3-pip \ - python3-matplotlib \ - librtlsdr-dev \ - libsndfile1-dev \ - libpulse-dev \ - libasound2-dev \ - libusb-1.0-0-dev \ - libfftw3-dev \ - libitpp-dev \ - libncurses-dev \ - # Build deps for csdr - libsamplerate0-dev \ - pulseaudio \ - && rm -rf /var/lib/apt/lists/* - -# Install numpy 1.x FIRST for GNU Radio compatibility -RUN pip3 install --no-cache-dir "numpy>=1.24,<2" "scipy>=1.10,<1.14" - -# Install mbelib (dependency for dsd-fme) -RUN git clone https://github.com/szechyjs/mbelib.git /tmp/mbelib \ - && cd /tmp/mbelib \ - && mkdir build && cd build \ - && cmake -DCMAKE_BUILD_TYPE=Release .. \ - && make -j$(nproc) \ - && make install \ - && ldconfig \ - && rm -rf /tmp/mbelib - -# Install dsd-fme -RUN git clone https://github.com/lwvmobile/dsd-fme.git /tmp/dsd-fme \ - && cd /tmp/dsd-fme \ - && mkdir build && cd build \ - && cmake -DCMAKE_BUILD_TYPE=Release .. \ - && make -j$(nproc) \ - && make install \ - && rm -rf /tmp/dsd-fme - -# Install multimon-ng from official repo -RUN git clone https://github.com/EliasOenal/multimon-ng.git /tmp/multimon-ng \ - && cd /tmp/multimon-ng \ - && mkdir build && cd build \ - && cmake .. \ - && make -j$(nproc) \ - && make install \ - && rm -rf /tmp/multimon-ng - -# Install csdr (sdr-util version with better CLI) -RUN git clone https://github.com/jketterl/csdr.git /tmp/csdr \ - && cd /tmp/csdr \ - && mkdir build && cd build \ - && cmake .. \ - && make -j$(nproc) \ - && make install \ - && ldconfig \ - && rm -rf /tmp/csdr - -# Install rtl-sdr tools -RUN git clone https://github.com/osmocom/rtl-sdr.git /tmp/rtl-sdr \ - && cd /tmp/rtl-sdr \ - && mkdir build && cd build \ - && cmake .. -DINSTALL_UDEV_RULES=ON \ - && make -j$(nproc) \ - && make install \ - && ldconfig \ - && rm -rf /tmp/rtl-sdr - -# Install GNU Radio (after numpy < 2 is installed) -RUN apt-get update && apt-get install -y \ - gnuradio \ - gr-osmosdr \ - && rm -rf /var/lib/apt/lists/* - -# Python packages for signal processing -RUN pip3 install --no-cache-dir \ - commpy \ - soundfile \ - resampy \ - scikit-dsp-comm - -# Create working directories -RUN mkdir -p /data/debug_audio /scripts /output - -WORKDIR /workspace - -# Default to bash for interactive use -CMD ["/bin/bash"] diff --git a/docker/bake.hcl b/docker/bake.hcl new file mode 100644 index 0000000..2d8d7d5 --- /dev/null +++ b/docker/bake.hcl @@ -0,0 +1,157 @@ +# WaveKit buildx bake configuration. +# +# Canonical entry point for every image build (local + CI). Replaces the +# per-mode docker/build.sh wrapper. The Makefile and CI workflow invoke +# `docker buildx bake --file docker/bake.hcl `; no plain +# `docker`+`build` (without `buildx`) survives anywhere — see Req 5.1. +# +# Cache strategy (design.md §4): +# - `cache-from` pulls per-stage refs from GHCR. Misses fall through +# silently per Req 4.3 (no auth error if the contributor isn't logged +# in to ghcr.io). +# - `cache-to` writes are gated by CACHE_FROM_ONLY. Defaults to "true" +# so local builds never push cache. CI on main flips it to "false" to +# publish `mode=max` cache for cold-build hits on every other machine. +# +# Variables can be overridden via `--set` on the CLI: +# docker buildx bake --file docker/bake.hcl default --set "REGISTRY=..." + +variable "REGISTRY" { + default = "ghcr.io/coriou/wavekit" +} + +variable "TAG" { + default = "latest" +} + +variable "CACHE_FROM_ONLY" { + default = "true" +} + +# Per-stage cache ref helper. Returns the single-element cache-from list +# for a named Dockerfile stage. Used inside concat() in each target. +function "cache" { + params = [stage] + result = [ + "type=registry,ref=${REGISTRY}:cache-${stage}", + ] +} + +# Abstract base inherited by every concrete target. Centralises context, +# Dockerfile path, and multi-arch platform list. +target "_base" { + context = "." + dockerfile = "Dockerfile" + platforms = ["linux/amd64", "linux/arm64"] +} + +# Full image: every decoder including SDR++. Maps to Dockerfile stage `final`. +target "final" { + inherits = ["_base"] + target = "final" + tags = ["${REGISTRY}:${TAG}"] + cache-from = concat( + cache("final"), + cache("final-base"), + cache("node-build"), + cache("sdrpp-build"), + cache("dsd-fme-build"), + cache("multimon-ng-build"), + cache("rtl433-build"), + cache("acarsdec-build"), + cache("ais-catcher-build"), + cache("direwolf-build"), + cache("dumpvdl2-build"), + cache("readsb-build"), + cache("soapy-rtltcp-build"), + cache("csdr-build"), + cache("lora-build"), + cache("base-build"), + cache("runtime-base"), + ) + cache-to = equal(CACHE_FROM_ONLY, "true") ? [] : [ + "type=registry,ref=${REGISTRY}:cache-final,mode=max", + ] +} + +# Core image: every decoder MINUS SDR++. Maps to Dockerfile stage `final-core`. +# Property 8: no sdrpp-build in the cache chain. +target "final-core" { + inherits = ["_base"] + target = "final-core" + tags = ["${REGISTRY}:${TAG}-core"] + cache-from = concat( + cache("final-core"), + cache("final-base"), + cache("node-build"), + cache("dsd-fme-build"), + cache("multimon-ng-build"), + cache("rtl433-build"), + cache("acarsdec-build"), + cache("ais-catcher-build"), + cache("direwolf-build"), + cache("dumpvdl2-build"), + cache("readsb-build"), + cache("soapy-rtltcp-build"), + cache("csdr-build"), + cache("lora-build"), + cache("base-build"), + cache("runtime-base"), + ) + cache-to = equal(CACHE_FROM_ONLY, "true") ? [] : [ + "type=registry,ref=${REGISTRY}:cache-final-core,mode=max", + ] +} + +# SDR++ standalone image: SDR++ binaries + minimal s6 overlay. +# Maps to Dockerfile stage `final-sdrpp`. +target "final-sdrpp" { + inherits = ["_base"] + target = "final-sdrpp" + tags = ["${REGISTRY}:${TAG}-sdrpp"] + cache-from = concat( + cache("final-sdrpp"), + cache("sdrpp-build"), + cache("base-build"), + cache("runtime-base"), + ) + cache-to = equal(CACHE_FROM_ONLY, "true") ? [] : [ + "type=registry,ref=${REGISTRY}:cache-final-sdrpp,mode=max", + ] +} + +# Interactive demod tooling image: dsd-fme, multimon-ng, csdr, rtl-sdr. +# Maps to Dockerfile stage `final-demod`. No SDR++ in this chain. +target "final-demod" { + inherits = ["_base"] + target = "final-demod" + tags = ["${REGISTRY}:${TAG}-demod"] + cache-from = concat( + cache("final-demod"), + cache("dsd-fme-build"), + cache("multimon-ng-build"), + cache("csdr-build"), + cache("base-build"), + cache("runtime-base"), + ) + cache-to = equal(CACHE_FROM_ONLY, "true") ? [] : [ + "type=registry,ref=${REGISTRY}:cache-final-demod,mode=max", + ] +} + +# CI helper: PR sanity build. Currently aliases final-core; CI uses it via +# `--target ci-core` so we can later cheapen the PR build (e.g. amd64-only, +# smaller cache chain) without touching the CI workflow. +target "ci-core" { + inherits = ["final-core"] +} + +# Default group: built locally by `make docker-build` and pushed by CI on +# main. Demod tooling is opt-in via the separate demod group. +group "default" { + targets = ["final", "final-core", "final-sdrpp"] +} + +group "demod" { + targets = ["final-demod"] +} diff --git a/docker/build.sh b/docker/build.sh deleted file mode 100755 index 78ed349..0000000 --- a/docker/build.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -# Build script for WaveKit Docker images -# Features: Multi-platform support, BuildKit caching, layer optimization -# Usage: ./docker/build.sh [mode] [tag] -# Modes: full (default), core, sdrpp - -set -e - -MODE="${1:-full}" -TAG="${2:-latest}" -REGISTRY="${REGISTRY:-}" -BUILDKIT="${BUILDKIT:-1}" - -# Color output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -log() { - echo -e "${GREEN}[build]${NC} $1" -} - -warn() { - echo -e "${YELLOW}[warn]${NC} $1" -} - -error() { - echo -e "${RED}[error]${NC} $1" - exit 1 -} - -# Validate mode -case "$MODE" in - full|core|sdrpp) - log "Building mode: $MODE" - ;; - *) - error "Invalid mode: $MODE. Must be: full, core, or sdrpp" - ;; -esac - -# Set target -TARGET="final" -[ "$MODE" = "core" ] && TARGET="final-core" -[ "$MODE" = "sdrpp" ] && TARGET="final-sdrpp" - -# Image name -IMAGE="${REGISTRY}wavekit:${TAG}-${MODE}" -[ "$MODE" = "full" ] && IMAGE="${REGISTRY}wavekit:${TAG}" - -log "Image: $IMAGE" -log "Target: $TARGET" - -# Enable BuildKit -export DOCKER_BUILDKIT=$BUILDKIT - -# Build command -BUILD_ARGS=( - "--target=$TARGET" - "--tag=$IMAGE" - "--progress=plain" -) - -# Multi-platform if specified -if [ -n "$PLATFORMS" ]; then - log "Building for platforms: $PLATFORMS" - BUILD_ARGS+=("--platform=$PLATFORMS") - BUILD_ARGS+=("--push") -else - log "Building for local platform" -fi - -# Build -log "Starting build..." -docker build "${BUILD_ARGS[@]}" -f Dockerfile . - -log "✅ Build complete: $IMAGE" -log "Size: $(docker image inspect $IMAGE --format='{{.Size}}' | numfmt --to=iec)" diff --git a/docker/init.sh b/docker/init.sh index 25e9359..5e9cb35 100755 --- a/docker/init.sh +++ b/docker/init.sh @@ -44,12 +44,6 @@ fi docker buildx inspect wavekit-builder --bootstrap >/dev/null log "Using buildx builder: wavekit-builder" -# Create build cache directory -log "Creating build cache directory..." -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -mkdir -p "${ROOT_DIR}/.docker-cache" -chmod 777 "${ROOT_DIR}/.docker-cache" - # Enable experimental features log "Enabling Docker experimental features..." export DOCKER_BUILDKIT=1 @@ -70,5 +64,5 @@ log "✅ Environment initialized successfully!" log "" log "Next steps:" log " 1. Build images: make docker-build" -log " 2. Start dev env: make docker-dev" -log " 3. View logs: make docker-logs" +log " 2. Start dev env: make dev-stack" +log " 3. View logs: make dev-stack-logs" diff --git a/docker/platform-utils.sh b/docker/platform-utils.sh deleted file mode 100755 index 68e272a..0000000 --- a/docker/platform-utils.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -# Platform detection and multi-arch build helper -# Automatically selects correct architecture for builds - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -BUILDER="${BUILDER:-wavekit-builder}" - -detect_platform() { - local kernel=$(uname -s) - local machine=$(uname -m) - - case "${kernel}/${machine}" in - Linux/x86_64) - echo "linux/amd64" - ;; - Linux/aarch64) - echo "linux/arm64" - ;; - Linux/armv7l) - echo "linux/arm/v7" - ;; - Darwin/x86_64) - echo "linux/amd64" # Docker Desktop - ;; - Darwin/arm64) - echo "linux/arm64" # Apple Silicon - ;; - *) - echo "linux/amd64" # Fallback - ;; - esac -} - -ensure_builder() { - local driver - if ! docker buildx inspect "$BUILDER" >/dev/null 2>&1; then - docker buildx create --name "$BUILDER" --driver docker-container --config "${SCRIPT_DIR}/buildkit.toml" --use >/dev/null - else - driver="$(docker buildx inspect "$BUILDER" | awk -F': ' '/Driver:/ {print $2}')" - if [ "$driver" = "docker" ]; then - docker buildx rm "$BUILDER" >/dev/null - docker buildx create --name "$BUILDER" --driver docker-container --config "${SCRIPT_DIR}/buildkit.toml" --use >/dev/null - else - docker buildx use "$BUILDER" >/dev/null - fi - fi - docker buildx inspect "$BUILDER" --bootstrap >/dev/null -} - -# Multi-platform build -build_multiarch() { - local tag=$1 - local platforms="${2:-linux/amd64,linux/arm64,linux/arm/v7}" - - echo "Building for platforms: $platforms" - - ensure_builder - - DOCKER_BUILDKIT=1 docker buildx build \ - --builder "$BUILDER" \ - --platform "$platforms" \ - --tag "wavekit:${tag}" \ - --push \ - . -} - -# Export functions -export -f detect_platform -export -f build_multiarch diff --git a/docker/push.sh b/docker/push.sh index 34c77c1..b9e8bdc 100755 --- a/docker/push.sh +++ b/docker/push.sh @@ -1,97 +1,33 @@ #!/bin/bash -# Push script for WaveKit Docker images -# Handles multi-platform builds and registry management -# Usage: ./docker/push.sh [tag] [registries...] - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -BUILDER="${BUILDER:-wavekit-builder}" -PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64,linux/arm/v7}" - +# Push WaveKit images to GHCR via docker buildx bake. +# Delegates the entire build matrix to docker/bake.hcl; this script only +# wires up env-var overrides (registry owner, tag, group, platforms) and +# flips CACHE_FROM_ONLY=false so the push populates mode=max cache. +# +# Usage: +# ./docker/push.sh [tag] [group] +# +# Environment overrides: +# WAVEKIT_GH_OWNER GHCR org/user (default: coriou) +# PLATFORMS comma-separated buildx platforms (default: linux/amd64,linux/arm64) + +set -euo pipefail + +OWNER="${WAVEKIT_GH_OWNER:-coriou}" +REGISTRY="ghcr.io/${OWNER}/wavekit" TAG="${1:-latest}" -REGISTRIES=("${@:2}") - -# Default registries -if [ ${#REGISTRIES[@]} -eq 0 ]; then - REGISTRIES=("docker.io" "ghcr.io") -fi - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' +GROUP="${2:-default}" +PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" -log() { - echo -e "${GREEN}[push]${NC} $1" -} - -warn() { - echo -e "${YELLOW}[warn]${NC} $1" -} - -error() { - echo -e "${RED}[error]${NC} $1" - exit 1 -} - -ensure_builder() { - local driver - if ! docker buildx inspect "$BUILDER" >/dev/null 2>&1; then - docker buildx create --name "$BUILDER" --driver docker-container --config "${SCRIPT_DIR}/buildkit.toml" --use >/dev/null - else - driver="$(docker buildx inspect "$BUILDER" | awk -F': ' '/Driver:/ {print $2}')" - if [ "$driver" = "docker" ]; then - docker buildx rm "$BUILDER" >/dev/null - docker buildx create --name "$BUILDER" --driver docker-container --config "${SCRIPT_DIR}/buildkit.toml" --use >/dev/null - else - docker buildx use "$BUILDER" >/dev/null - fi - fi - docker buildx inspect "$BUILDER" --bootstrap >/dev/null -} - -# Modes to build -MODES=("full" "core" "sdrpp") - -log "Pushing WaveKit images (tag: $TAG)" -log "Registries: ${REGISTRIES[*]}" -log "Platforms: ${PLATFORMS}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ensure_builder +echo "[push] registry=${REGISTRY} tag=${TAG} group=${GROUP} platforms=${PLATFORMS}" -for MODE in "${MODES[@]}"; do - log "" - log "Building and pushing mode: $MODE" - TARGET="final" - [ "$MODE" = "core" ] && TARGET="final-core" - [ "$MODE" = "sdrpp" ] && TARGET="final-sdrpp" - - for REGISTRY in "${REGISTRIES[@]}"; do - IMAGE_NAME="${REGISTRY}/wavekit" - - if [ "$MODE" = "full" ]; then - IMAGE="${IMAGE_NAME}:${TAG}" - IMAGE_LATEST="${IMAGE_NAME}:latest" - else - IMAGE="${IMAGE_NAME}:${TAG}-${MODE}" - IMAGE_LATEST="${IMAGE_NAME}:latest-${MODE}" - fi - - log "Pushing: $IMAGE" - - DOCKER_BUILDKIT=1 docker buildx build \ - --builder "$BUILDER" \ - --target="$TARGET" \ - --tag="$IMAGE" \ - --tag="$IMAGE_LATEST" \ - --platform="$PLATFORMS" \ - --push \ - -f Dockerfile . - - log "✅ Pushed: $IMAGE" - done -done +export REGISTRY TAG +export CACHE_FROM_ONLY=false -log "" -log "✅ All images pushed successfully" +exec docker buildx bake \ + --file "${SCRIPT_DIR}/bake.hcl" \ + --set "*.platform=${PLATFORMS}" \ + --push \ + "${GROUP}" diff --git a/docker/scripts/start-sdrpp.sh b/docker/scripts/start-sdrpp.sh index facc4ca..d15aae9 100644 --- a/docker/scripts/start-sdrpp.sh +++ b/docker/scripts/start-sdrpp.sh @@ -9,8 +9,9 @@ echo "[sdrpp-server] Starting SDR++ server..." >> /var/log/wavekit/system.log exec 2>&1 # Execute SDR++ server +# Note: SDR++ v1.1.0 does not accept `--log-level`; passing it triggers a +# basic_string-from-null std::logic_error crash. Verbosity is left at default. exec sdrpp \ --server \ --port 5259 \ - --addr 0.0.0.0 \ - --log-level info + --addr 0.0.0.0