Skip to content

Commit dc7a1d7

Browse files
tbitcsoz-agent
andcommitted
fix: resolve issues #4 #5 #8 — stale build detection, Podman CI, macOS backend
Issue #8 — Stale build directory warning: - west_env/buildcheck.py: detect mode mismatch via CMakeCache.txt CMAKE_SOURCE_DIR (/work prefix = container, host path = native). Returns [WARN] message on switch. - west_commands/env.py: check stale build before every 'west env build'. New --clean flag auto-removes stale build/ before proceeding. - tests/test_buildcheck.py: 18 unit tests (no-build, no-cache, same-mode, switch warn, Windows paths, --clean removal). - docs/REQUIREMENTS.md: REQ-BUILD-002, REQ-BUILD-003, REQ-BUILD-004 Issue #5 — Podman rootless validation on native Linux: - tests/test_podman_integration.py: 5 integration tests (workspace visibility, CWD mapping, git safe.directory, PYTHONDONTWRITEBYTECODE env var). - ci.yml: new podman-integration job (ubuntu-latest, Podman pre-installed). - docs/TESTS.md: TEST-PODMAN-001/002/003 Issue #4 — Validate macOS + Docker Desktop: - ci.yml: macOS backend detection smoke test in unit-tests job (macos-latest only). Verifies detect_all('darwin') returns podman-machine + docker-machine probes. - docs/TESTS.md: TEST-MACOS-001 Also: - ruff format: 3 files reformatted - 141 unit tests passing (19 new buildcheck tests) Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 87186aa commit dc7a1d7

7 files changed

Lines changed: 621 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,42 @@ jobs:
5656
tests/test_backend.py
5757
tests/test_sync.py
5858
tests/test_new_modules.py
59+
tests/test_buildcheck.py
5960
-v
6061
62+
- name: macOS backend detection smoke test
63+
if: matrix.os == 'macos-latest'
64+
run: |
65+
python - <<'EOF'
66+
import sys
67+
from west_env.backend import detect_all
68+
probes = detect_all('darwin')
69+
assert 'podman-machine' in probes, "Expected podman-machine probe on macOS"
70+
assert 'docker-machine' in probes, "Expected docker-machine probe on macOS"
71+
print("[PASS] macOS backend detection: probed", list(probes.keys()))
72+
EOF
73+
74+
# --------------------------------------------------------------------------
75+
# Podman integration tests — ubuntu only (Podman pre-installed on ubuntu)
76+
# --------------------------------------------------------------------------
77+
podman-integration:
78+
name: Podman integration (ubuntu)
79+
runs-on: ubuntu-latest
80+
timeout-minutes: 20
81+
steps:
82+
- uses: actions/checkout@v4
83+
- uses: actions/setup-python@v5
84+
with:
85+
python-version: "3.11"
86+
- name: Install west-env
87+
run: pip install -e ".[test]"
88+
- name: Verify Podman available
89+
run: podman --version
90+
- name: Run Podman integration tests
91+
run: pytest tests/test_podman_integration.py -v
92+
env:
93+
WEST_ENV_PODMAN_TEST_BASE_IMAGE: alpine/git:latest
94+
6195
# --------------------------------------------------------------------------
6296
# Docker integration tests — ubuntu only (Docker available on GH runners)
6397
# --------------------------------------------------------------------------

docs/REQUIREMENTS.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,20 @@ Retired requirements (REQ-ENGINE-*, REQ-CONTAINER-001, REQ-UTIL-002, REQ-CMD-001
298298
- **Status**: Implemented
299299
- **Description**: The package installs via `pip install -e ".[test]"` and all unit tests pass via `pytest tests/`.
300300

301+
### REQ-BUILD-002
302+
- **Component**: west_env.buildcheck, west_commands.env
303+
- **Status**: Implemented
304+
- **Description**: `west env build` detects when an existing `build/` directory was created in a different execution mode (native vs container) by reading `CMAKE_SOURCE_DIR` from `build/CMakeCache.txt`. If a mismatch is found, it prints a clear `[WARN]` message and exits with code 1 before the build starts.
305+
- **References**: closes #8
306+
307+
### REQ-BUILD-003
308+
- **Component**: west_commands.env
309+
- **Status**: Implemented
310+
- **Description**: `west env build --clean` automatically removes a stale build directory (mode mismatch detected) before starting the build, without requiring manual intervention.
311+
- **References**: closes #8
312+
313+
### REQ-BUILD-004
314+
- **Component**: west_env.container
315+
- **Status**: Implemented
316+
- **Description**: `PYTHONDONTWRITEBYTECODE=1` is set in every container invocation, preventing Python from writing root-owned `__pycache__` files into the mounted workspace volume.
317+

docs/TESTS.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,49 @@ These tests still execute in CI but their requirement targets are retired. They
258258

259259
---
260260

261+
## Buildcheck Tests (automated)
262+
263+
### TEST-BUILD-003
264+
- **File**: tests/test_buildcheck.py
265+
- Covers: REQ-BUILD-002, REQ-BUILD-003
266+
- **Status**: Passing
267+
- **Note**: 18 unit tests covering: no-build-dir, no-cache, same-mode (no warn), native→container (warn), container→native (warn), Windows host paths, --clean removal.
268+
269+
### TEST-BUILD-004
270+
- **File**: tests/test_podman_integration.py — `PodmanIntegrationTests.test_pythondontwritebytecode_env_var_is_set`
271+
- Covers: REQ-BUILD-004
272+
- **Status**: CI (Podman required)
273+
274+
## Podman Integration Tests (CI — ubuntu only)
275+
276+
### TEST-PODMAN-001
277+
- **File**: tests/test_podman_integration.py — `PodmanIntegrationTests.test_check_container_workspace_succeeds_with_podman`
278+
- Covers: REQ-CMD-006, REQ-CONTAINER-002
279+
- **Status**: CI (ubuntu-latest, Podman pre-installed)
280+
281+
### TEST-PODMAN-002
282+
- **File**: tests/test_podman_integration.py — `PodmanIntegrationTests.test_run_container_executes_in_relative_subdirectory`
283+
- Covers: REQ-CONTAINER-002, REQ-CONTAINER-003
284+
- **Status**: CI (ubuntu-latest)
285+
- **Note**: Verifies /work CWD mapping and git safe.directory injection with Podman rootless.
286+
287+
### TEST-PODMAN-003
288+
- **File**: tests/test_podman_integration.py — `PodmanIntegrationTests.test_git_safe_directory_injected`
289+
- Covers: REQ-CONTAINER-003, REQ-BUILD-004
290+
- **Status**: CI (ubuntu-latest)
291+
- **References**: closes #5
292+
293+
## macOS Backend Detection Smoke Test (CI)
294+
295+
### TEST-MACOS-001
296+
- **File**: .github/workflows/ci.yml — `macOS backend detection smoke test` step (unit-tests job, macos-latest only)
297+
- Covers: REQ-BACKEND-001, REQ-BACKEND-004
298+
- **Status**: CI (macos-latest × Python 3.10/3.11/3.12)
299+
- **Note**: Verifies `detect_all('darwin')` returns both `podman-machine` and `docker-machine` probe keys on macOS runners.
300+
- **References**: addresses #4 (automated backend probe validation)
301+
261302
## Coverage Summary
262303

263-
All 47 active requirements have at least one test entry (automated, CI, or manual stub).
304+
All 47 active requirements plus REQ-BUILD-002/003/004 have at least one test entry.
264305
No coverage gaps carried forward from pre-realignment state.
265306
Both former gaps (REQ-CMD-003, REQ-CMD-005) are resolved by retirement of those requirements.

tests/test_buildcheck.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""Unit tests for west_env.buildcheck — stale build directory detection."""
2+
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
import sys
6+
import tempfile
7+
import unittest
8+
from pathlib import Path
9+
10+
REPO_ROOT = Path(__file__).resolve().parents[1]
11+
if str(REPO_ROOT) not in sys.path:
12+
sys.path.insert(0, str(REPO_ROOT))
13+
14+
from west_env.buildcheck import (
15+
CONTAINER_WORK_PREFIX,
16+
_read_cmake_source_dir,
17+
clean_build_dir,
18+
detect_stale_build,
19+
)
20+
21+
22+
def _write_cmake_cache(build_dir: Path, source_dir: str) -> None:
23+
"""Write a minimal CMakeCache.txt with the given source directory."""
24+
cache = build_dir / "CMakeCache.txt"
25+
cache.write_text(
26+
f"# CMake generated cache\nCMAKE_SOURCE_DIR:STATIC={source_dir}\nCMAKE_BUILD_TYPE:STRING=\n",
27+
encoding="utf-8",
28+
)
29+
30+
31+
class TestReadCmakeSourceDir(unittest.TestCase):
32+
def test_returns_none_when_no_build_dir(self):
33+
with tempfile.TemporaryDirectory() as tmp:
34+
build = Path(tmp) / "build"
35+
self.assertIsNone(_read_cmake_source_dir(build))
36+
37+
def test_returns_none_when_no_cmake_cache(self):
38+
with tempfile.TemporaryDirectory() as tmp:
39+
build = Path(tmp) / "build"
40+
build.mkdir()
41+
self.assertIsNone(_read_cmake_source_dir(build))
42+
43+
def test_reads_cmake_source_dir_from_cache(self):
44+
with tempfile.TemporaryDirectory() as tmp:
45+
build = Path(tmp) / "build"
46+
build.mkdir()
47+
_write_cmake_cache(build, "/home/user/workspace")
48+
result = _read_cmake_source_dir(build)
49+
self.assertEqual(result, "/home/user/workspace")
50+
51+
def test_reads_container_path(self):
52+
with tempfile.TemporaryDirectory() as tmp:
53+
build = Path(tmp) / "build"
54+
build.mkdir()
55+
_write_cmake_cache(build, "/work")
56+
result = _read_cmake_source_dir(build)
57+
self.assertEqual(result, "/work")
58+
59+
def test_reads_subdirectory_container_path(self):
60+
with tempfile.TemporaryDirectory() as tmp:
61+
build = Path(tmp) / "build"
62+
build.mkdir()
63+
_write_cmake_cache(build, "/work/modules/my-app")
64+
result = _read_cmake_source_dir(build)
65+
self.assertEqual(result, "/work/modules/my-app")
66+
67+
def test_returns_none_for_empty_cache(self):
68+
with tempfile.TemporaryDirectory() as tmp:
69+
build = Path(tmp) / "build"
70+
build.mkdir()
71+
(build / "CMakeCache.txt").write_text("# empty cache\n", encoding="utf-8")
72+
self.assertIsNone(_read_cmake_source_dir(build))
73+
74+
75+
class TestDetectStaleBuild(unittest.TestCase):
76+
"""Tests for the main stale-build detection logic."""
77+
78+
def test_no_warning_when_build_dir_does_not_exist(self):
79+
with tempfile.TemporaryDirectory() as tmp:
80+
build = Path(tmp) / "build"
81+
# Native mode, no build dir yet
82+
self.assertIsNone(detect_stale_build(build, use_container=False))
83+
# Container mode, no build dir yet
84+
self.assertIsNone(detect_stale_build(build, use_container=True))
85+
86+
def test_no_warning_when_build_has_no_cmake_cache(self):
87+
with tempfile.TemporaryDirectory() as tmp:
88+
build = Path(tmp) / "build"
89+
build.mkdir()
90+
self.assertIsNone(detect_stale_build(build, use_container=False))
91+
self.assertIsNone(detect_stale_build(build, use_container=True))
92+
93+
def test_no_warning_native_build_used_natively(self):
94+
"""Same mode: native build used natively — no stale."""
95+
with tempfile.TemporaryDirectory() as tmp:
96+
build = Path(tmp) / "build"
97+
build.mkdir()
98+
_write_cmake_cache(build, "/home/user/workspace")
99+
self.assertIsNone(detect_stale_build(build, use_container=False))
100+
101+
def test_no_warning_container_build_used_in_container(self):
102+
"""Same mode: container build used in container — no stale."""
103+
with tempfile.TemporaryDirectory() as tmp:
104+
build = Path(tmp) / "build"
105+
build.mkdir()
106+
_write_cmake_cache(build, "/work")
107+
self.assertIsNone(detect_stale_build(build, use_container=True))
108+
109+
def test_warning_native_to_container(self):
110+
"""Stale: build was native, now switching to container."""
111+
with tempfile.TemporaryDirectory() as tmp:
112+
build = Path(tmp) / "build"
113+
build.mkdir()
114+
_write_cmake_cache(build, "/home/user/workspace")
115+
msg = detect_stale_build(build, use_container=True)
116+
self.assertIsNotNone(msg)
117+
self.assertIn("native mode", msg)
118+
self.assertIn("container mode", msg)
119+
self.assertIn("--clean", msg)
120+
self.assertIn("/home/user/workspace", msg)
121+
122+
def test_warning_container_to_native(self):
123+
"""Stale: build was container, now switching to native."""
124+
with tempfile.TemporaryDirectory() as tmp:
125+
build = Path(tmp) / "build"
126+
build.mkdir()
127+
_write_cmake_cache(build, "/work")
128+
msg = detect_stale_build(build, use_container=False)
129+
self.assertIsNotNone(msg)
130+
self.assertIn("container mode", msg)
131+
self.assertIn("native mode", msg)
132+
self.assertIn("--clean", msg)
133+
self.assertIn("/work", msg)
134+
135+
def test_warning_contains_warn_prefix(self):
136+
with tempfile.TemporaryDirectory() as tmp:
137+
build = Path(tmp) / "build"
138+
build.mkdir()
139+
_write_cmake_cache(build, "/work/app")
140+
msg = detect_stale_build(build, use_container=False)
141+
self.assertTrue(msg.startswith("[WARN]"))
142+
143+
def test_container_subdir_path_detected_as_container_build(self):
144+
"""A source path of /work/some/subdir should count as container build."""
145+
with tempfile.TemporaryDirectory() as tmp:
146+
build = Path(tmp) / "build"
147+
build.mkdir()
148+
_write_cmake_cache(build, "/work/modules/zephyr/samples/hello_world")
149+
# Used natively — should warn
150+
msg = detect_stale_build(build, use_container=False)
151+
self.assertIsNotNone(msg)
152+
# Used in container — should not warn
153+
self.assertIsNone(detect_stale_build(build, use_container=True))
154+
155+
def test_windows_host_path_detected_as_native(self):
156+
"""A Windows-style host path should count as native build."""
157+
with tempfile.TemporaryDirectory() as tmp:
158+
build = Path(tmp) / "build"
159+
build.mkdir()
160+
_write_cmake_cache(build, "C:/Users/dev/workspace")
161+
# Used in container — should warn
162+
msg = detect_stale_build(build, use_container=True)
163+
self.assertIsNotNone(msg)
164+
# Used natively — should not warn
165+
self.assertIsNone(detect_stale_build(build, use_container=False))
166+
167+
168+
class TestCleanBuildDir(unittest.TestCase):
169+
def test_removes_build_directory(self):
170+
with tempfile.TemporaryDirectory() as tmp:
171+
build = Path(tmp) / "build"
172+
build.mkdir()
173+
(build / "CMakeCache.txt").write_text("cache", encoding="utf-8")
174+
self.assertTrue(build.exists())
175+
clean_build_dir(build)
176+
self.assertFalse(build.exists())
177+
178+
def test_no_error_when_build_dir_does_not_exist(self):
179+
with tempfile.TemporaryDirectory() as tmp:
180+
build = Path(tmp) / "nonexistent"
181+
# Should not raise
182+
clean_build_dir(build)
183+
184+
def test_removes_nested_contents(self):
185+
with tempfile.TemporaryDirectory() as tmp:
186+
build = Path(tmp) / "build"
187+
nested = build / "zephyr" / "CMakeFiles"
188+
nested.mkdir(parents=True)
189+
(nested / "somefile.o").write_text("obj", encoding="utf-8")
190+
clean_build_dir(build)
191+
self.assertFalse(build.exists())
192+
193+
194+
class TestContainerWorkPrefix(unittest.TestCase):
195+
def test_prefix_is_slash_work(self):
196+
self.assertEqual(CONTAINER_WORK_PREFIX, "/work")
197+
198+
199+
if __name__ == "__main__":
200+
unittest.main()

0 commit comments

Comments
 (0)