MOBUX_PORT ?= 5151
MOBUX_SMOKE_PORT ?= 8281
MOBUX_USER ?= $(USER)
MOBUX_PIN ?= 30879
CARGO := $(HOME)/.cargo/bin/cargo
MOBUX_PACKAGE_ID ?= io.github.mvhenten.mobux
MOBUX_APP_NAME ?= Mobux
TWA_INSTALL_DIR ?= web/static/install
TWA_WELLKNOWN_DIR ?= web/static/.well-known
MOBUX_DEV_DOMAIN ?= sandbox:5152
PID := $(shell lsof -ti :$(MOBUX_PORT) 2>/dev/null)
SMOKE_PID := $(shell lsof -ti :$(MOBUX_SMOKE_PORT) 2>/dev/null)
.PHONY: build run clean start stop restart status logs test web setup setup-twa twa twa-dev \
transcribe setup-transcribe \
smoke-start smoke-stop smoke-logs smoke-status \
podman-build podman-run podman-stop podman-test
PODMAN_IMAGE ?= localhost/mobux:dev
PODMAN_PORT ?= 8381
PODMAN_NAME ?= mobux-podman
WHISPER_DIR ?= $(HOME)/.local/whisper.cpp
WHISPER_MODEL_NAME ?= base.en
setup:
./bin/setup
setup-twa:
./bin/setup-twa
transcribe:
@if [ -z "$(FILE)" ]; then echo "usage: make transcribe FILE=<audio-or-video-file>" >&2; exit 2; fi
@WHISPER_DIR="$(WHISPER_DIR)" ./bin/transcribe "$(FILE)"
setup-transcribe:
@command -v ffmpeg >/dev/null 2>&1 || { echo "ffmpeg is required (apt install ffmpeg)"; exit 1; }
@if [ ! -d "$(WHISPER_DIR)" ]; then \
git clone --depth 1 https://github.com/ggerganov/whisper.cpp "$(WHISPER_DIR)"; \
fi
cmake -B "$(WHISPER_DIR)/build" -S "$(WHISPER_DIR)" -DCMAKE_BUILD_TYPE=Release
cmake --build "$(WHISPER_DIR)/build" -j --config Release
bash "$(WHISPER_DIR)/models/download-ggml-model.sh" "$(WHISPER_MODEL_NAME)" "$(WHISPER_DIR)/models"
@echo "whisper.cpp ready at $(WHISPER_DIR) (model: $(WHISPER_MODEL_NAME))"
web:
node web/build.js
clean:
$(CARGO) clean -p mobux
build: web
$(CARGO) build
run: build
env MOBUX_AUTH_USER=$(MOBUX_USER) MOBUX_PIN=$(MOBUX_PIN) PORT=$(MOBUX_PORT) \
$(CARGO) run
start: build
@if [ -n "$(PID)" ]; then echo "already running (pid $(PID))"; exit 1; fi
nohup env MOBUX_AUTH_USER=$(MOBUX_USER) MOBUX_PIN=$(MOBUX_PIN) PORT=$(MOBUX_PORT) \
./target/debug/mobux > /tmp/mobux.log 2>&1 &
@sleep 2 && lsof -i :$(MOBUX_PORT) >/dev/null 2>&1 && echo "mobux running on port $(MOBUX_PORT)" || echo "FAILED to start"
stop:
@if [ -z "$(PID)" ]; then echo "not running"; exit 0; fi
kill $(PID) && echo "stopped (pid $(PID))"
restart: stop
@sleep 2
@$(MAKE) start
status:
@if [ -n "$(PID)" ]; then echo "running (pid $(PID)) on port $(MOBUX_PORT)"; else echo "not running"; fi
logs:
@tail -f /tmp/mobux.log
smoke-start: build
@if [ -n "$(SMOKE_PID)" ]; then echo "smoke already running (pid $(SMOKE_PID)) on $(MOBUX_SMOKE_PORT)"; exit 1; fi
@if [ "$(MOBUX_SMOKE_PORT)" = "$(MOBUX_PORT)" ]; then echo "MOBUX_SMOKE_PORT must differ from MOBUX_PORT"; exit 1; fi
@mkdir -p /tmp/mobux-smoke/home
@nohup env MOBUX_DATA_DIR=/tmp/mobux-smoke MOBUX_TLS=0 \
HOME=/tmp/mobux-smoke/home HISTFILE=/dev/null \
MOBUX_TMUX_SOCKET=mobux-test \
PORT=$(MOBUX_SMOKE_PORT) MOBUX_AUTH_USER=smoke MOBUX_PIN=00000 \
./target/debug/mobux > /tmp/mobux-smoke/mobux.log 2>&1 < /dev/null &
@sleep 2 && lsof -i :$(MOBUX_SMOKE_PORT) >/dev/null 2>&1 \
&& echo "smoke mobux running on port $(MOBUX_SMOKE_PORT) (data /tmp/mobux-smoke)" \
|| { echo "smoke FAILED to start"; tail /tmp/mobux-smoke/mobux.log; exit 1; }
smoke-stop:
@if [ -n "$(SMOKE_PID)" ]; then kill $(SMOKE_PID) && echo "smoke stopped (pid $(SMOKE_PID))"; else echo "smoke not running"; fi
@tmux -L mobux-test kill-server 2>/dev/null || true
smoke-logs:
@tail -f /tmp/mobux-smoke/mobux.log
smoke-status:
@if [ -n "$(SMOKE_PID)" ]; then echo "smoke running (pid $(SMOKE_PID)) on port $(MOBUX_SMOKE_PORT)"; else echo "smoke not running"; fi
test:
MOBUX_USER=$(MOBUX_USER) MOBUX_PASS=$(MOBUX_PIN) npx playwright test
.PHONY: test-smoke
test-smoke:
@$(MAKE) smoke-start
@trap '$(MAKE) smoke-stop' EXIT; \
MOBUX_URL=http://localhost:$(MOBUX_SMOKE_PORT) \
MOBUX_USER=smoke MOBUX_PASS=00000 \
npx playwright test test/smoke.spec.cjs
.PHONY: test-critical-path
test-critical-path:
@$(MAKE) smoke-start
@trap '$(MAKE) smoke-stop' EXIT; \
MOBUX_URL=http://localhost:$(MOBUX_SMOKE_PORT) \
MOBUX_USER=smoke MOBUX_PASS=00000 \
npx playwright test test/critical-path.spec.cjs
.PHONY: test-mesh
test-mesh:
@$(MAKE) smoke-start
@trap '$(MAKE) smoke-stop' EXIT; \
MOBUX_URL=http://localhost:$(MOBUX_SMOKE_PORT) \
MOBUX_USER=smoke MOBUX_PASS=00000 \
npx playwright test test/mesh-relay.spec.cjs
.PHONY: test-e2e
test-e2e:
@$(MAKE) smoke-start
@trap '$(MAKE) smoke-stop' EXIT; \
MOBUX_URL=http://localhost:$(MOBUX_SMOKE_PORT) \
MOBUX_USER=smoke MOBUX_PASS=00000 \
npx playwright test
podman-build:
podman build -t $(PODMAN_IMAGE) -f Containerfile .
podman-run: podman-build
-@podman rm -f $(PODMAN_NAME) >/dev/null 2>&1
@podman run -d --name $(PODMAN_NAME) -p $(PODMAN_PORT):8080 \
-e MOBUX_AUTH_USER=test -e MOBUX_PIN=00000 \
$(PODMAN_IMAGE) >/dev/null
@echo "mobux running in container on http://localhost:$(PODMAN_PORT) (test/00000)"
podman-stop:
-@podman rm -f $(PODMAN_NAME) >/dev/null 2>&1 && echo "stopped $(PODMAN_NAME)" || echo "not running"
podman-test: podman-build
-@podman rm -f $(PODMAN_NAME) >/dev/null 2>&1
@podman run -d --name $(PODMAN_NAME) -p $(PODMAN_PORT):8080 \
-e MOBUX_AUTH_USER=test -e MOBUX_PIN=00000 \
$(PODMAN_IMAGE) >/dev/null
@trap 'podman rm -f $(PODMAN_NAME) >/dev/null 2>&1' EXIT; \
for i in $$(seq 1 30); do \
if curl -fsS -u test:00000 -o /dev/null http://localhost:$(PODMAN_PORT)/ 2>/dev/null; then break; fi; \
sleep 0.5; \
done; \
MOBUX_URL=http://localhost:$(PODMAN_PORT) \
MOBUX_USER=test MOBUX_PASS=00000 \
MOBUX_TEST_TMUX="podman exec $(PODMAN_NAME) tmux" \
npx playwright test
twa:
@if [ -z "$$MOBUX_DOMAIN" ]; then \
echo "MOBUX_DOMAIN is required, e.g. make twa MOBUX_DOMAIN=mine.example.com" >&2; \
exit 1; \
fi
@CONFIG_DIR="$${MOBUX_CONFIG_DIR:-$$HOME/.config/mobux}"; \
KEYSTORE="$$CONFIG_DIR/twa-signing.keystore"; \
PASSFILE="$$CONFIG_DIR/twa-signing.password"; \
mkdir -p "$$CONFIG_DIR"; \
chmod 700 "$$CONFIG_DIR" 2>/dev/null || true; \
FRESH_KEY=0; \
if [ ! -f "$$KEYSTORE" ]; then \
FRESH_KEY=1; \
if [ -n "$$MOBUX_TWA_KEYSTORE_PASSWORD" ]; then \
KEYSTORE_PASSWORD="$$MOBUX_TWA_KEYSTORE_PASSWORD"; \
else \
KEYSTORE_PASSWORD="$$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)"; \
umask 077; \
printf '%s' "$$KEYSTORE_PASSWORD" > "$$PASSFILE"; \
chmod 600 "$$PASSFILE"; \
fi; \
echo "Generating signing keystore at $$KEYSTORE"; \
keytool -genkeypair -v \
-keystore "$$KEYSTORE" \
-alias mobux \
-keyalg RSA -keysize 2048 \
-validity 10000 \
-storepass "$$KEYSTORE_PASSWORD" \
-keypass "$$KEYSTORE_PASSWORD" \
-dname "CN=Mobux, OU=mobux, O=mobux, L=Unknown, ST=Unknown, C=XX" >/dev/null; \
echo ""; \
echo "============================================================"; \
echo " BACK THIS UP: $$KEYSTORE"; \
echo " Losing this key prevents APK upgrades for existing installs."; \
if [ -z "$$MOBUX_TWA_KEYSTORE_PASSWORD" ]; then \
echo " Password written to: $$PASSFILE (mode 0600)"; \
fi; \
echo "============================================================"; \
echo ""; \
else \
if [ -n "$$MOBUX_TWA_KEYSTORE_PASSWORD" ]; then \
KEYSTORE_PASSWORD="$$MOBUX_TWA_KEYSTORE_PASSWORD"; \
elif [ -f "$$PASSFILE" ]; then \
KEYSTORE_PASSWORD="$$(cat "$$PASSFILE")"; \
else \
echo "Keystore exists at $$KEYSTORE but neither MOBUX_TWA_KEYSTORE_PASSWORD nor $$PASSFILE is set." >&2; \
exit 1; \
fi; \
fi; \
CA_CERT="$$CONFIG_DIR/ca.crt"; \
if [ -f "$$CA_CERT" ] && [ -z "$${NODE_EXTRA_CA_CERTS:-}" ]; then \
export NODE_EXTRA_CA_CERTS="$$CA_CERT"; \
fi; \
mkdir -p "$$HOME/.bubblewrap"; \
if [ ! -f "$$HOME/.bubblewrap/config.json" ]; then \
printf '{\n "jdkPath": "%s",\n "androidSdkPath": "%s"\n}\n' \
"$${JAVA_HOME:-$$HOME/.sdkman/candidates/java/current}" \
"$${ANDROID_HOME:-$$HOME/.android}" \
> "$$HOME/.bubblewrap/config.json"; \
fi; \
echo "Rendering twa/twa-manifest.json (MOBUX_DOMAIN=$$MOBUX_DOMAIN, packageId=$(MOBUX_PACKAGE_ID), name=$(MOBUX_APP_NAME))"; \
sed -e "s|__MOBUX_DOMAIN__|$$MOBUX_DOMAIN|g" \
-e "s|__MOBUX_KEYSTORE_PATH__|$$KEYSTORE|g" \
-e "s|__MOBUX_PACKAGE_ID__|$(MOBUX_PACKAGE_ID)|g" \
-e "s|__MOBUX_APP_NAME__|$(MOBUX_APP_NAME)|g" \
twa/twa-manifest.json.template > twa/twa-manifest.json; \
if [ -d twa/app ]; then \
echo "Regenerating TWA project from manifest (twa/app/)"; \
rm -rf twa/app; \
fi; \
echo "Initializing TWA project (twa/app/)"; \
node twa/init.js; \
echo "Building signed APK"; \
( cd twa/app && BUBBLEWRAP_KEYSTORE_PASSWORD="$$KEYSTORE_PASSWORD" \
BUBBLEWRAP_KEY_PASSWORD="$$KEYSTORE_PASSWORD" \
bubblewrap build ); \
APK_SRC="twa/app/app-release-signed.apk"; \
if [ ! -f "$$APK_SRC" ]; then \
echo "Expected signed APK at $$APK_SRC but it is missing." >&2; \
exit 1; \
fi; \
mkdir -p $(TWA_INSTALL_DIR); \
cp "$$APK_SRC" $(TWA_INSTALL_DIR)/mobux.apk; \
echo "Wrote $(TWA_INSTALL_DIR)/mobux.apk"; \
FINGERPRINT="$$(keytool -list -v \
-keystore "$$KEYSTORE" \
-alias mobux \
-storepass "$$KEYSTORE_PASSWORD" 2>/dev/null \
| awk '/SHA256:/ {print $$2; exit}')"; \
if [ -z "$$FINGERPRINT" ]; then \
echo "Could not extract SHA-256 fingerprint from keystore." >&2; \
exit 1; \
fi; \
mkdir -p $(TWA_WELLKNOWN_DIR); \
printf '[{\n "relation": ["delegate_permission/common.handle_all_urls"],\n "target": {\n "namespace": "android_app",\n "package_name": "$(MOBUX_PACKAGE_ID)",\n "sha256_cert_fingerprints": ["%s"]\n }\n}]\n' "$$FINGERPRINT" > $(TWA_WELLKNOWN_DIR)/assetlinks.json; \
echo "Wrote $(TWA_WELLKNOWN_DIR)/assetlinks.json (fingerprint $$FINGERPRINT)"; \
if [ "$$FRESH_KEY" = "1" ]; then \
echo ""; \
echo "Reminder: back up $$KEYSTORE and $$PASSFILE before you forget."; \
fi
twa-dev:
@$(MAKE) twa \
MOBUX_DOMAIN=$(MOBUX_DEV_DOMAIN) \
MOBUX_PACKAGE_ID=io.github.mvhenten.mobux.dev \
MOBUX_APP_NAME="Mobux Dev" \
TWA_INSTALL_DIR=twa/dist-dev/install \
TWA_WELLKNOWN_DIR=twa/dist-dev/.well-known