forked from Urban-Meteorology-Reading/SUEWS
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathMakefile
More file actions
307 lines (287 loc) · 11.7 KB
/
Copy pathMakefile
File metadata and controls
307 lines (287 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# SUEWS Simplified Makefile - Essential recipes only
.PHONY: help setup submodules dev docs-setup reinstall test test-smoke test-all audit-deps docs livehtml clean format bridge plugin agent-plugin
# Default Python
PYTHON := python
help:
@echo "SUEWS Development - Essential Commands"
@echo ""
@echo " setup - Create virtual environment (if using uv)"
@echo " dev - Build and install in editable mode"
@echo " docs-setup - Install documentation dependencies and strip legacy .pth hooks"
@echo " test - Run standard tests (excludes slow, ~2-3 min)"
@echo " test-smoke - Run smoke tests only (fast CI validation, ~30-60 sec)"
@echo " test-all - Run ALL tests including slow (~4-5 min)"
@echo " audit-deps - Audit Python dependencies and startup hooks"
@echo " docs - Build documentation"
@echo " clean - Smart clean (keeps .venv if active)"
@echo " bridge - Build the Rust bridge CLI (suews_bridge)"
@echo " format - Format Python and Fortran code"
@echo ""
@echo "Quick start:"
@echo " With uv: make setup && source .venv/bin/activate && make dev"
@echo ""
@echo "Common workflows:"
@echo " Fresh start: make clean && make dev"
@echo " Update code: git pull && make dev"
@echo " Test changes: make dev && make test"
@echo " CI validation: make test-smoke"
# Setup virtual environment (for uv users)
setup:
@if command -v uv >/dev/null 2>&1; then \
if [ ! -d ".venv" ]; then \
echo "Creating uv virtual environment..."; \
uv venv; \
echo "Created .venv"; \
echo "-> Now run: source .venv/bin/activate"; \
else \
echo "Virtual environment already exists at .venv"; \
fi \
else \
echo "uv not found. Install with: brew install uv"; \
echo "Or use conda/mamba instead"; \
exit 1; \
fi
# Initialise git submodules (idempotent - safe to run multiple times)
submodules:
@git submodule update --init --recursive
# Build and install in editable mode
dev:
@# Check for activated virtual environment
@if [ -z "$$VIRTUAL_ENV" ]; then \
echo ""; \
echo "ERROR: No virtual environment activated."; \
echo ""; \
echo "Please activate a virtual environment first:"; \
echo " make setup && source .venv/bin/activate && make dev"; \
echo ""; \
echo "Or with conda:"; \
echo " mamba activate suews-dev && make dev"; \
echo ""; \
exit 1; \
fi
@$(MAKE) submodules
@echo "Installing SUEWS in editable mode..."
@# Get current Python version tag
@PYVER=$$($(PYTHON) -c "import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')") || { \
echo "ERROR: Failed to get Python version"; \
exit 1; \
}; \
echo "Python version: $$PYVER"; \
# Stale build detection: check for builds from different Python versions \
if [ -d "build" ]; then \
STALE_BUILDS=$$(ls -d build/cp* 2>/dev/null | grep -v "build/$$PYVER" || true); \
if [ -n "$$STALE_BUILDS" ]; then \
echo ""; \
echo "WARNING: Found build directories for different Python versions:"; \
echo "$$STALE_BUILDS"; \
echo ""; \
echo "Cleaning stale builds to avoid import errors..."; \
for dir in $$STALE_BUILDS; do \
rm -rf "$$dir"; \
echo " Removed: $$dir"; \
done; \
echo ""; \
fi \
fi; \
# Uninstall first if build directory is missing (post-clean state) \
if [ ! -d "build/$$PYVER" ]; then \
echo "Build directory missing for $$PYVER - performing clean reinstall..."; \
if command -v uv >/dev/null 2>&1; then \
uv pip uninstall supy 2>/dev/null || true; \
else \
$(PYTHON) -m pip uninstall supy -y 2>/dev/null || true; \
fi \
fi
@# Install build dependencies first (required for --no-build-isolation)
@if command -v uv >/dev/null 2>&1; then \
echo "Using uv for fast installation..."; \
uv pip install wheel pytest "numpy>=2.0" "meson-python>=0.12.0"; \
if [ -x "/opt/homebrew/bin/gfortran" ]; then \
echo "Using Homebrew gfortran for macOS compatibility"; \
bash -c 'PATH="$$HOME/.cargo/bin:$$PATH" FC=/opt/homebrew/bin/gfortran uv pip install --no-build-isolation -e ".[dev]"'; \
else \
uv pip install --no-build-isolation -e ".[dev]"; \
fi \
else \
$(PYTHON) -m pip install wheel pytest "numpy>=2.0" "meson-python>=0.12.0"; \
if [ -x "/opt/homebrew/bin/gfortran" ]; then \
FC=/opt/homebrew/bin/gfortran $(PYTHON) -m pip install --no-build-isolation -e ".[dev]"; \
else \
$(PYTHON) -m pip install --no-build-isolation -e ".[dev]"; \
fi \
fi
@# Ensure meson build directory is initialized (fixes post-clean state)
@$(MAKE) rebuild-meson
@echo "Build complete"
# Install docs-only dependencies without re-installing the package
docs-setup:
@# Check for activated virtual environment
@if [ -z "$$VIRTUAL_ENV" ]; then \
echo ""; \
echo "ERROR: No virtual environment activated."; \
echo ""; \
echo "Please activate a virtual environment first:"; \
echo " make setup && source .venv/bin/activate && make dev && make docs-setup"; \
echo ""; \
exit 1; \
fi
@$(PYTHON) -c "import sys; sys.exit(sys.version_info < (3, 11))" || { \
echo "ERROR: Documentation tooling requires Python 3.11 or newer."; \
echo "Runtime support remains Python 3.9+."; \
exit 1; \
}
@TMP_REQ=$$(mktemp "$${TMPDIR:-/tmp}/suews-docs.XXXXXX" 2>/dev/null || mktemp -t suews-docs); \
trap 'rm -f "$$TMP_REQ"' EXIT; \
$(PYTHON) scripts/security/export_pyproject_requirements.py --extra docs > "$$TMP_REQ"; \
if command -v uv >/dev/null 2>&1; then \
echo "Installing documentation dependencies with uv..."; \
uv pip install -r "$$TMP_REQ"; \
else \
echo "Installing documentation dependencies with pip..."; \
$(PYTHON) -m pip install -r "$$TMP_REQ"; \
fi; \
$(PYTHON) scripts/security/remove_legacy_namespace_pth.py
# Deprecated: use 'make clean && make dev' instead (kept for backwards compatibility)
reinstall:
@echo "Note: 'make reinstall' is deprecated. Use 'make clean && make dev' instead."
@echo "Forcing clean reinstall..."
@rm -rf build
@$(MAKE) dev
# Initialize meson build after clean OR rebuild if sources changed
rebuild-meson:
@# Get Python version for build directory naming
@PYVER=$$($(PYTHON) -c "import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')") || { \
echo "ERROR: Failed to get Python version. Is Python available?"; \
exit 1; \
}; \
if [ -z "$$PYVER" ]; then \
echo "ERROR: Could not determine Python version"; \
exit 1; \
fi; \
if [ ! -d "build/$$PYVER" ]; then \
echo "Initializing meson build directory for $$PYVER..."; \
mkdir -p "build/$$PYVER"; \
if cd "build/$$PYVER" && meson setup ../.. --prefix=$$VIRTUAL_ENV; then \
echo "[OK] Build directory initialized"; \
else \
echo "ERROR: meson setup failed"; \
exit 1; \
fi; \
else \
echo "Rebuilding changed Fortran sources..."; \
BRIDGE_SO=$$(ls build/$$PYVER/src/supy/suews_bridge.* 2>/dev/null | head -1); \
if [ -n "$$BRIDGE_SO" ] && [ -d "src/suews_bridge/src" ]; then \
STALE=$$(find src/suews_bridge/src src/suews_bridge/c_api \
src/suews_bridge/build.rs src/suews_bridge/Cargo.toml \
-newer "$$BRIDGE_SO" 2>/dev/null | head -1); \
if [ -n "$$STALE" ]; then \
echo "Rust bridge sources changed - forcing rebuild..."; \
rm -f build/$$PYVER/src/supy/suews_bridge.* build/$$PYVER/src/supy/bin/suews*; \
fi; \
fi; \
if cd "build/$$PYVER" && ninja; then \
echo "[OK] Fortran extension rebuilt"; \
else \
echo "ERROR: ninja build failed"; \
exit 1; \
fi; \
fi
# Run tests - Four local entry points available:
# - test-smoke: Fast critical tests (~30-60 sec) - used in CI wheel validation
# - test: Standard tests excluding slow (~2-3 min) - default for development
# - test-all: All tests including slow (~4-5 min) - comprehensive validation
# - test-qgis: UMEP/QGIS compatibility tests (Windows + Python 3.12 target)
test:
@echo "Running standard tests (excluding slow tests)..."
@echo "NOTE: Long-running regression tests are skipped."
@echo " Run 'make test-all' for comprehensive testing."
@echo ""
$(PYTHON) -m pytest test -m "not slow and not qgis" -v --tb=short --durations=10
# Smoke tests - fast critical path tests for CI
test-smoke:
@echo "Running smoke tests (critical path only)..."
@echo "This is the fastest test tier for CI wheel validation."
@echo ""
$(PYTHON) -m pytest test -m "smoke and not slow and not qgis" -v --tb=short --durations=10
# All tests including slow tests
test-all:
@echo "Running ALL tests including slow tests..."
@echo "This may take 4-5 minutes."
@echo ""
$(PYTHON) -m pytest test -v --tb=short --durations=10
# UMEP/QGIS compatibility tests
test-qgis:
@echo "Running UMEP/QGIS compatibility tests..."
@echo "Target runtime is Windows + Python 3.12 (current QGIS 3 LTR / QGIS 4)."
@echo "Other platforms/interpreters collect these tests as skipped."
@echo ""
$(PYTHON) -m pytest test/umep -m "qgis and api" -v --tb=short --durations=10
# Audit installed dependency hooks and declared dependency advisories
audit-deps:
@TMP_REQ=$$(mktemp "$${TMPDIR:-/tmp}/suews-audit.XXXXXX" 2>/dev/null || mktemp -t suews-audit); \
TMP_AUDIT_ENV=$$(mktemp -d "$${TMPDIR:-/tmp}/suews-audit-env.XXXXXX" 2>/dev/null || mktemp -d -t suews-audit-env); \
trap 'rm -f "$$TMP_REQ"; rm -rf "$$TMP_AUDIT_ENV"' EXIT; \
$(PYTHON) scripts/security/export_pyproject_requirements.py --extra dev --extra docs > "$$TMP_REQ"; \
echo "Auditing Python startup hooks in the active environment..."; \
$(PYTHON) scripts/security/audit_python_startup.py; \
echo ""; \
echo "Auditing declared dependencies against PyPI advisories..."; \
if command -v uv >/dev/null 2>&1; then \
uv venv "$$TMP_AUDIT_ENV"; \
uv pip install --python "$$TMP_AUDIT_ENV/bin/python" -r "$$TMP_REQ" pip-audit; \
"$$TMP_AUDIT_ENV/bin/python" scripts/security/remove_legacy_namespace_pth.py; \
"$$TMP_AUDIT_ENV/bin/python" -m pip_audit; \
elif $(PYTHON) -c "import pip_audit" >/dev/null 2>&1; then \
$(PYTHON) scripts/security/remove_legacy_namespace_pth.py --dry-run; \
$(PYTHON) -m pip_audit -r "$$TMP_REQ"; \
else \
echo "ERROR: pip-audit is required. Install uv or run: $(PYTHON) -m pip install pip-audit"; \
exit 1; \
fi
# Build documentation
docs:
$(MAKE) -C docs html
livehtml:
$(MAKE) -C docs livehtml
# Smart clean - preserves .venv if you're in it
clean:
@echo "Cleaning build artifacts..."
@rm -rf build dist *.egg-info .pytest_cache
@find . -name "*.pyc" -delete
@find . -name "__pycache__" -type d -exec rm -rf {} +
@$(MAKE) -C src/suews clean 2>/dev/null || true
@$(MAKE) -C docs clean 2>/dev/null || true
@if [ -n "$$VIRTUAL_ENV" ] && [ -d ".venv" ]; then \
echo "[OK] Cleaned (keeping .venv - you're using it)"; \
echo "-> Run 'make dev' to rebuild"; \
elif [ -d ".venv" ]; then \
echo "Removing .venv (not active)..."; \
rm -rf .venv; \
echo "[OK] Cleaned everything including .venv"; \
else \
echo "[OK] Cleaned"; \
fi
# Build the Rust bridge CLI binary
bridge:
@if ! command -v cargo >/dev/null 2>&1; then \
echo "ERROR: cargo not found. Install Rust: https://rustup.rs"; \
exit 1; \
fi
@echo "Building Rust bridge CLI..."
cd src/suews_bridge && cargo build --release
@echo "Binary at: src/suews_bridge/target/release/suews-engine"
@echo "Run: src/suews_bridge/target/release/suews-engine --help"
# Format code
format:
ruff format src test
fprettify --config .fprettify.rc src/suews/src/*.f95 2>/dev/null || true
# Regenerate the distributable plugin bundle (plugins/suews/skills/) from the
# single source of truth (.claude/skills/). Run after editing a skill; the
# parity test in test/mcp/test_packaging_manifests.py guards against drift.
plugin:
$(PYTHON) scripts/build_plugin.py
# Generate the standalone SUEWS agent marketplace repository. Override
# AGENT_PLUGIN_OUT to point at an existing checkout of UMEP-dev/suews-agent.
AGENT_PLUGIN_OUT ?= ../suews-agent
agent-plugin:
$(PYTHON) scripts/build_agent_plugin.py --output $(AGENT_PLUGIN_OUT)