2026-05-11 01:14:43 +00:00
LOCUST_PORT ?= 10502
LOCUST_WEB_PORT ?= 10503
LOCUST_DB_DIR ?= /tmp/devplace_locust
LOCUST_DB ?= $( LOCUST_DB_DIR) /datastore.db
LOCUST_USERS ?= 20
LOCUST_SPAWN_RATE ?= 5
LOCUST_RUN_TIME ?= 120s
2026-06-14 14:46:36 +00:00
LOCUST_WEB_WORKERS ?= 4
WEB_WORKERS ?= $( shell nproc 2>/dev/null || echo 2)
2026-05-23 03:57:05 +00:00
DEVPLACE_RATE_LIMIT ?= 1000000
2026-09-03 08:16:05 +00:00
DEVPLACE_PORT ?= 10500
2026-05-11 01:14:43 +00:00
2026-08-29 23:28:49 +02:00
VENV ?= $( CURDIR) /.venv
PYTHON := $( VENV) /bin/python
VENV_STAMP := $( VENV) /.installed
BOOTSTRAP_PYTHON := $( shell command -v python3 2>/dev/null || command -v python 2>/dev/null)
2026-06-13 16:27:41 +00:00
PYTHONDONTWRITEBYTECODE := 1
export PYTHONDONTWRITEBYTECODE
2026-09-08 03:43:49 +02:00
.PHONY : venv install dev prod clean tree tree -loc zip test test -headed test -unit test -api test -e 2e test -fast test -failed test -first -failure test -slowest test -cache -clean coverage coverage -headed coverage -html locust locust -headless prune prune -dry -run
2026-05-10 07:08:12 +00:00
2026-08-29 23:28:49 +02:00
$(PYTHON) :
@test -n " $( BOOTSTRAP_PYTHON) " || { echo "python3 is required to create $( VENV) " ; exit 1; }
$( BOOTSTRAP_PYTHON) -m venv $( VENV)
2026-05-10 07:08:12 +00:00
2026-08-29 23:28:49 +02:00
$(VENV_STAMP) : $( PYTHON ) pyproject .toml
$( PYTHON) -m pip install -U pip
$( PYTHON) -m pip install -e ".[dev]"
$( PYTHON) -m playwright install chromium
touch $( VENV_STAMP)
2026-05-11 03:30:51 +00:00
2026-08-29 23:28:49 +02:00
venv : $( VENV_STAMP )
install : $( PYTHON )
$( PYTHON) -m pip install -U pip
$( PYTHON) -m pip install -e ".[dev]"
$( PYTHON) -m playwright install chromium
2026-09-07 15:22:23 +02:00
git config core.hooksPath .githooks
2026-08-29 23:28:49 +02:00
touch $( VENV_STAMP)
dev : $( VENV_STAMP )
2026-09-03 08:16:05 +00:00
DEVPLACE_PORT = $( DEVPLACE_PORT) $( PYTHON) -m uvicorn devplacepy.main:app --reload --reload-dir devplacepy --host 0.0.0.0 --port $( DEVPLACE_PORT) --backlog 4096
2026-08-29 23:28:49 +02:00
prod : $( VENV_STAMP )
2026-09-03 08:16:05 +00:00
DEVPLACE_STATIC_VERSION = $$ ( date +%s) DEVPLACE_TEMPLATE_AUTO_RELOAD = 0 DEVPLACE_WEB_WORKERS = $( WEB_WORKERS) DEVPLACE_PORT = $( DEVPLACE_PORT) $( PYTHON) -m uvicorn devplacepy.main:app --host 0.0.0.0 --port $( DEVPLACE_PORT) --workers $( WEB_WORKERS) --backlog 8192 --proxy-headers --forwarded-allow-ips '*'
2026-05-10 07:08:12 +00:00
2026-06-14 07:48:10 +00:00
delete-pyc :
find . -name "__pycache__" -type d -prune -exec rm -rf {} + 2>/dev/null || true
find . -name "*.pyc" -delete
2026-06-13 16:27:41 +00:00
tree :
2026-06-13 18:34:47 +00:00
git ls-files | tree --fromfile --noreport
tree-loc :
@git ls-files | while IFS = read -r f; do \
loc = $$ ( wc -l < " $$ f" 2>/dev/null || echo 0) ; \
printf '%s [%s LOC]\n' " $$ f" " $$ loc" ; \
done | tree --fromfile --noreport
zip :
@rm -f $( notdir $( CURDIR)) .zip
@git ls-files -z | xargs -0 zip -q $( notdir $( CURDIR)) .zip
@printf 'Wrote %s (%s files)\n' " $( notdir $( CURDIR)) .zip" " $$ (git ls-files | wc -l)"
2026-06-13 16:27:41 +00:00
2026-08-29 23:28:49 +02:00
test : $( VENV_STAMP )
PLAYWRIGHT_HEADLESS = 1 $( PYTHON) -m pytest tests/
2026-05-10 07:08:12 +00:00
2026-08-29 23:28:49 +02:00
test-headed : $( VENV_STAMP )
PLAYWRIGHT_HEADLESS = 0 $( PYTHON) -m pytest tests/
2026-05-10 07:08:12 +00:00
2026-08-29 23:28:49 +02:00
test-unit : $( VENV_STAMP )
$( PYTHON) -m pytest tests/unit
2026-06-13 14:32:33 +00:00
2026-08-29 23:28:49 +02:00
test-api : $( VENV_STAMP )
$( PYTHON) -m pytest tests/api
2026-06-13 14:32:33 +00:00
2026-08-29 23:28:49 +02:00
test-e2e : $( VENV_STAMP )
PLAYWRIGHT_HEADLESS = 1 $( PYTHON) -m pytest tests/e2e
2026-07-26 16:02:36 +02:00
2026-08-29 23:28:49 +02:00
test-fast : $( VENV_STAMP )
$( PYTHON) -m pytest tests/unit tests/api
2026-07-26 16:02:36 +02:00
2026-08-29 23:28:49 +02:00
test-failed : $( VENV_STAMP )
PLAYWRIGHT_HEADLESS = 1 $( PYTHON) -m pytest tests/ --last-failed --last-failed-no-failures none
2026-07-26 16:02:36 +02:00
2026-08-29 23:28:49 +02:00
test-first-failure : $( VENV_STAMP )
PLAYWRIGHT_HEADLESS = 1 $( PYTHON) -m pytest tests/ -x
2026-07-26 16:02:36 +02:00
2026-08-29 23:28:49 +02:00
test-slowest : $( VENV_STAMP )
PLAYWRIGHT_HEADLESS = 1 $( PYTHON) -m pytest tests/ --durations= 40
2026-06-13 14:32:33 +00:00
2026-08-29 23:28:49 +02:00
coverage : $( VENV_STAMP )
2026-06-05 18:33:35 +00:00
rm -f .coverage .coverage.*
COVERAGE_PROCESS_START = $( CURDIR) /.coveragerc PLAYWRIGHT_HEADLESS = 1 \
2026-08-29 23:28:49 +02:00
$( PYTHON) -m coverage run -m pytest tests/
$( PYTHON) -m coverage combine
$( PYTHON) -m coverage report
2026-06-05 18:33:35 +00:00
2026-08-29 23:28:49 +02:00
coverage-headed : $( VENV_STAMP )
2026-06-05 18:33:35 +00:00
rm -f .coverage .coverage.*
COVERAGE_PROCESS_START = $( CURDIR) /.coveragerc PLAYWRIGHT_HEADLESS = 0 \
2026-08-29 23:28:49 +02:00
$( PYTHON) -m coverage run -m pytest tests/
$( PYTHON) -m coverage combine
$( PYTHON) -m coverage report
2026-06-05 18:33:35 +00:00
coverage-html : coverage
2026-08-29 23:28:49 +02:00
$( PYTHON) -m coverage html
2026-06-05 18:33:35 +00:00
@echo "Report written to htmlcov/index.html"
2026-08-29 23:28:49 +02:00
locust : $( VENV_STAMP )
2026-05-11 01:14:43 +00:00
export DEVPLACE_DATABASE_URL = "sqlite:/// $( LOCUST_DB) " ; \
2026-05-23 03:57:05 +00:00
export DEVPLACE_RATE_LIMIT = $( DEVPLACE_RATE_LIMIT) ; \
2026-06-05 15:44:12 +00:00
fuser -k $( LOCUST_PORT) /tcp 2>/dev/null || true; \
sleep 1; \
2026-05-11 01:14:43 +00:00
mkdir -p $( LOCUST_DB_DIR) ; \
rm -f $( LOCUST_DB) ; \
2026-08-29 23:28:49 +02:00
DEVPLACE_TEMPLATE_AUTO_RELOAD = 0 DEVPLACE_WEB_WORKERS = $( LOCUST_WEB_WORKERS) $( PYTHON) -m uvicorn devplacepy.main:app --host 127.0.0.1 --port $( LOCUST_PORT) --workers $( LOCUST_WEB_WORKERS) --backlog 8192 > /tmp/devplace_locust_server.log 2>& 1 & \
2026-05-11 01:14:43 +00:00
PID = $$ !; \
2026-06-05 15:44:12 +00:00
while kill -0 $$ PID 2>/dev/null && ! curl -s http://127.0.0.1:$( LOCUST_PORT) / > /dev/null 2>& 1; do sleep 0.5; done ; \
if ! kill -0 $$ PID 2>/dev/null; then echo "Server failed to start (port $( LOCUST_PORT) busy?). See /tmp/devplace_locust_server.log" ; exit 1; fi ; \
2026-08-29 23:28:49 +02:00
$( PYTHON) -m locust --host http://127.0.0.1:$( LOCUST_PORT) --web-port $( LOCUST_WEB_PORT) ; \
2026-05-11 01:14:43 +00:00
kill $$ PID 2>/dev/null || true; \
rm -rf $( LOCUST_DB_DIR)
2026-08-29 23:28:49 +02:00
locust-headless : $( VENV_STAMP )
2026-05-11 01:14:43 +00:00
export DEVPLACE_DATABASE_URL = "sqlite:/// $( LOCUST_DB) " ; \
2026-05-23 03:57:05 +00:00
export DEVPLACE_RATE_LIMIT = $( DEVPLACE_RATE_LIMIT) ; \
2026-06-05 15:44:12 +00:00
fuser -k $( LOCUST_PORT) /tcp 2>/dev/null || true; \
sleep 1; \
2026-05-11 01:14:43 +00:00
mkdir -p $( LOCUST_DB_DIR) ; \
rm -f $( LOCUST_DB) ; \
2026-08-29 23:28:49 +02:00
DEVPLACE_TEMPLATE_AUTO_RELOAD = 0 DEVPLACE_WEB_WORKERS = $( LOCUST_WEB_WORKERS) $( PYTHON) -m uvicorn devplacepy.main:app --host 127.0.0.1 --port $( LOCUST_PORT) --workers $( LOCUST_WEB_WORKERS) --backlog 8192 > /tmp/devplace_locust_server.log 2>& 1 & \
2026-05-11 01:14:43 +00:00
PID = $$ !; \
2026-06-05 15:44:12 +00:00
while kill -0 $$ PID 2>/dev/null && ! curl -s http://127.0.0.1:$( LOCUST_PORT) / > /dev/null 2>& 1; do sleep 0.5; done ; \
if ! kill -0 $$ PID 2>/dev/null; then echo "Server failed to start (port $( LOCUST_PORT) busy?). See /tmp/devplace_locust_server.log" ; exit 1; fi ; \
2026-08-29 23:28:49 +02:00
$( PYTHON) -m locust --host http://127.0.0.1:$( LOCUST_PORT) --headless -u $( LOCUST_USERS) -r $( LOCUST_SPAWN_RATE) --run-time $( LOCUST_RUN_TIME) --html= $( LOCUST_DB_DIR) /report.html; \
2026-05-11 01:14:43 +00:00
kill $$ PID 2>/dev/null || true; \
rm -rf $( LOCUST_DB_DIR)
2026-05-10 07:08:12 +00:00
clean :
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name '*.pyc' -delete
rm -rf devplacepy.egg-info
2026-07-26 16:02:36 +02:00
rm -rf .pytest_cache
2026-05-10 07:08:12 +00:00
rm -rf .venv
2026-05-12 13:07:34 +00:00
2026-07-26 16:02:36 +02:00
test-cache-clean :
rm -rf .pytest_cache
2026-10-16 07:20:44 +02:00
# Builds the donor clients (Go binaries for five platforms and the browser
# extension zip) into the directory the /nodes/download route serves.
NODE_CLIENT_OUT ?= data/node_clients
.PHONY : node -client
node-client :
sh nodes/client/build.sh $( NODE_CLIENT_OUT)
sh nodes/extension/build.sh $( NODE_CLIENT_OUT)
2026-09-08 03:43:49 +02:00
# Safe but aggressive disk-space cleanup, acting on the REAL data/devplace.db
# and data/ tree (never the test database) - see CLAUDE.md "devplace system
# prune". prune-dry-run reports what would be removed without deleting.
prune : $( VENV_STAMP )
$( PYTHON) -m devplacepy.cli system prune
prune-dry-run : $( VENV_STAMP )
$( PYTHON) -m devplacepy.cli system prune --dry-run
2026-06-09 04:41:27 +00:00
# Container Manager works out of the box: the overlay installs the docker CLI in
# the image and mounts the host socket. DOCKER_GID is read straight from the
2026-06-13 19:06:43 +00:00
# socket so the UID-1000 app can use it; the data dir is the project's own data/
2026-06-09 04:41:27 +00:00
# at its real host path, so the DooD bind-mount (host == container path) holds
# with no /srv dir and no sudo.
COMPOSE := docker compose -f docker-compose.yml -f docker-compose.containers.yml
2026-06-13 19:06:43 +00:00
DEVPLACE_DATA_DIR ?= $( CURDIR) /data
2026-06-09 04:41:27 +00:00
DOCKER_GID ?= $( shell stat -c '%g' /var/run/docker.sock 2>/dev/null)
2026-08-11 20:03:15 +02:00
DEVPLACE_CONTAINER_NETWORK ?= bridge
2026-06-09 04:41:27 +00:00
export DEVPLACE_DATA_DIR
export DOCKER_GID
2026-05-12 13:07:34 +00:00
2026-08-11 20:03:15 +02:00
.PHONY : docker -build docker -up docker -attach docker -reload docker -down docker -logs docker -clean docker -prep ppy
2026-06-09 14:06:02 +00:00
# Build the single shared container image every instance runs. Build once;
# rebuild only when ppy.Dockerfile, the sudo shim, or pagent change.
ppy :
docker build --network= host -f ppy.Dockerfile -t ppy:latest devplacepy/services/containers/files
2026-05-12 13:07:34 +00:00
2026-06-09 04:41:27 +00:00
docker-prep :
mkdir -p $( DEVPLACE_DATA_DIR)
docker-build : docker -prep
$( COMPOSE) build
docker-up : docker -prep
$( COMPOSE) up -d
2026-08-11 20:03:15 +02:00
$( MAKE) docker-attach
# Workspace tunnels reach a container port that was never published on the host,
# so the app must sit on the same docker network as the instances it runs. The
# default bridge rejects the network-scoped aliases compose always sends, so
# this cannot live in docker-compose.containers.yml and is wired here instead.
docker-attach :
@app= $$ ( $( COMPOSE) ps -q app) ; \
test -n " $$ app" || { echo "app container is not running" ; exit 1; } ; \
docker network connect $( DEVPLACE_CONTAINER_NETWORK) $$ app 2>/dev/null \
&& echo "attached app to the $( DEVPLACE_CONTAINER_NETWORK) network" \
|| echo "app is already on the $( DEVPLACE_CONTAINER_NETWORK) network"
2026-05-12 13:07:34 +00:00
2026-07-27 11:17:48 +02:00
docker-reload :
$( COMPOSE) restart app
$( COMPOSE) up -d --wait
2026-08-11 20:03:15 +02:00
$( MAKE) docker-attach
2026-07-27 11:17:48 +02:00
2026-05-12 13:07:34 +00:00
docker-down :
2026-06-09 04:41:27 +00:00
$( COMPOSE) down
2026-05-12 13:07:34 +00:00
docker-logs :
2026-06-09 04:41:27 +00:00
$( COMPOSE) logs -f
2026-05-12 13:07:34 +00:00
docker-clean :
2026-06-09 04:41:27 +00:00
$( COMPOSE) down -v
2026-05-23 06:27:04 +00:00
2026-06-09 14:06:02 +00:00
docker-bup : docker -build docker -up
2026-10-14 07:13:12 +02:00
# Rootless-podman hosts have no docker daemon: the podman overlay runs the app
# as container root (== invoking host user under rootless, see
# docker-compose.podman.yml) so the bind-mounted checkout stays writable.
# Always update through these targets - a bare `up` without the overlay file
# recreates the app with the base `user: 1000:1000` and it crashes on startup
# with PermissionError (container uid 1000 maps to a subordinate host uid).
PCOMPOSE := podman-compose -f docker-compose.yml -f docker-compose.podman.yml
.PHONY : podman -build podman -up podman -reload podman -down podman -logs podman -ps
podman-build : docker -prep
$( PCOMPOSE) build
podman-up : docker -prep
$( PCOMPOSE) up -d
@loginctl show-user " $$ (id -un)" 2>/dev/null | grep -q 'Linger=yes' || echo "WARNING: user linger is off - containers will NOT restart after a reboot until someone logs in (fix: sudo loginctl enable-linger $$ (id -un))"
podman-reload :
$( PCOMPOSE) restart app
$( PCOMPOSE) up -d
podman-down :
$( PCOMPOSE) down
podman-logs :
$( PCOMPOSE) logs -f
podman-ps :
$( PCOMPOSE) ps
2026-05-23 06:27:04 +00:00
deploy :
git checkout production
git merge master
git push origin production
2026-06-11 23:35:31 +00:00