Skip to content

Commit 650b672

Browse files
committed
feat: Add info script to display project status and version information
1 parent cde2d84 commit 650b672

2 files changed

Lines changed: 193 additions & 1 deletion

File tree

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ PYTHON ?= python3
66
SPHINXBUILD ?= $(CURDIR)/$(VENV)/bin/sphinx-build
77
DOCS_STAMP ?= $(VENV)/.docs-installed
88

9-
.PHONY: build flash monitor run clean setup docs docs-clean docs-env erase web build-web release
9+
.PHONY: build flash monitor run clean setup docs docs-clean docs-env erase web build-web release info
1010

1111
all: build
1212

@@ -18,6 +18,9 @@ build-web:
1818

1919
web: build-web
2020

21+
info:
22+
@$(PYTHON) tools/info.py
23+
2124
build: build-web
2225
@echo "Injecting version from VERSION file..."
2326
@$(PYTHON) scripts/inject_version.py

tools/info.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env python3
2+
"""Print a quick project status snapshot for MultiGeiger."""
3+
4+
from __future__ import annotations
5+
6+
import re
7+
import subprocess
8+
from pathlib import Path
9+
from typing import Iterable, List, Optional, Tuple
10+
11+
12+
ROOT = Path(__file__).resolve().parent.parent
13+
14+
15+
def run(cmd: List[str]) -> Optional[str]:
16+
"""Run a command and return stdout (stripped) or None on error."""
17+
try:
18+
result = subprocess.run(
19+
cmd, cwd=ROOT, check=False, capture_output=True, text=True, timeout=5
20+
)
21+
if result.returncode == 0:
22+
return result.stdout.strip()
23+
except Exception:
24+
return None
25+
return None
26+
27+
28+
def read_version_file() -> Optional[str]:
29+
path = ROOT / "VERSION"
30+
if not path.exists():
31+
return None
32+
return path.read_text().strip()
33+
34+
35+
def read_core_version() -> Optional[str]:
36+
path = ROOT / "src" / "core" / "core.hpp"
37+
if not path.exists():
38+
return None
39+
match = re.search(r'#define\s+VERSION_STR\s+"([^"]+)"', path.read_text())
40+
return match.group(1) if match else None
41+
42+
43+
def git_branch() -> Optional[str]:
44+
return run(["git", "rev-parse", "--abbrev-ref", "HEAD"])
45+
46+
47+
def git_head() -> Optional[str]:
48+
return run(["git", "log", "-1", "--format=%h %cs %s"])
49+
50+
51+
def git_status_summary() -> str:
52+
out = run(["git", "status", "--porcelain"])
53+
if out is None:
54+
return "git unavailable"
55+
lines = [line for line in out.splitlines() if line.strip()]
56+
if not lines:
57+
return "clean"
58+
59+
staged = sum(1 for line in lines if not line.startswith("??") and not line.startswith(" "))
60+
unstaged = sum(1 for line in lines if line.startswith(" ") and not line.startswith("??"))
61+
untracked = sum(1 for line in lines if line.startswith("??"))
62+
63+
parts = []
64+
if staged:
65+
parts.append(f"{staged} staged")
66+
if unstaged:
67+
parts.append(f"{unstaged} unstaged")
68+
if untracked:
69+
parts.append(f"{untracked} untracked")
70+
return "dirty (" + ", ".join(parts) + ")"
71+
72+
73+
def git_tags(limit: int = 5) -> List[Tuple[str, Optional[str]]]:
74+
out = run(["git", "tag", "--sort=-creatordate"])
75+
if out is None:
76+
return []
77+
tags = [line.strip() for line in out.splitlines() if line.strip()][:limit]
78+
result: List[Tuple[str, Optional[str]]] = []
79+
for tag in tags:
80+
date = run(["git", "log", "-1", "--format=%cs", tag])
81+
result.append((tag, date))
82+
return result
83+
84+
85+
def commits_since_last_tag() -> Optional[Tuple[str, str]]:
86+
last_tag = run(["git", "describe", "--tags", "--abbrev=0"])
87+
if not last_tag:
88+
return None
89+
count = run(["git", "rev-list", f"{last_tag}..HEAD", "--count"])
90+
if count is None:
91+
return None
92+
return last_tag, count
93+
94+
95+
def default_env() -> Optional[str]:
96+
makefile = ROOT / "Makefile"
97+
if not makefile.exists():
98+
return None
99+
match = re.search(r"^ENV\s*\?=\s*(\S+)", makefile.read_text(), re.MULTILINE)
100+
return match.group(1) if match else None
101+
102+
103+
def pio_envs() -> List[str]:
104+
ini = ROOT / "platformio.ini"
105+
if not ini.exists():
106+
return []
107+
return re.findall(r"^\[env:([^\]]+)\]", ini.read_text(), re.MULTILINE)
108+
109+
110+
def tool_version(cmd: List[str]) -> str:
111+
out = run(cmd)
112+
if not out:
113+
return "not available"
114+
return out.splitlines()[0]
115+
116+
117+
def artifact_present(path: Path) -> str:
118+
return "present" if path.exists() else "absent"
119+
120+
121+
def latest_changelog_heading() -> Optional[str]:
122+
path = ROOT / "docs" / "source" / "changes.rst"
123+
if not path.exists():
124+
return None
125+
for line in path.read_text().splitlines():
126+
if line.strip().startswith("V"):
127+
return line.strip()
128+
return None
129+
130+
131+
def print_section(title: str, lines: Iterable[str]) -> None:
132+
print(title)
133+
for line in lines:
134+
print(f" {line}")
135+
print()
136+
137+
138+
def main() -> None:
139+
version_file = read_version_file()
140+
core_version = read_core_version()
141+
tags = git_tags()
142+
last_tag_info = commits_since_last_tag()
143+
changelog_head = latest_changelog_heading()
144+
env_default = default_env()
145+
pio_env_list = pio_envs()
146+
147+
print("MultiGeiger info\n-----------------")
148+
149+
version_lines = []
150+
version_lines.append(f"VERSION file: {version_file or 'missing'}")
151+
version_lines.append(f"core.hpp VERSION_STR: {core_version or 'missing'}")
152+
if version_file and core_version:
153+
version_lines.append("Version match: " + ("yes" if version_file.replace("//", "").strip() == core_version else "no"))
154+
if tags:
155+
tag_lines = ", ".join(f"{t} ({d or 'n/a'})" for t, d in tags)
156+
version_lines.append(f"Recent tags: {tag_lines}")
157+
if changelog_head:
158+
version_lines.append(f"Changelog head: {changelog_head}")
159+
print_section("Versions", version_lines)
160+
161+
repo_lines = []
162+
branch = git_branch()
163+
if branch:
164+
repo_lines.append(f"Branch: {branch}")
165+
head = git_head()
166+
if head:
167+
repo_lines.append(f"HEAD: {head}")
168+
repo_lines.append(f"Status: {git_status_summary()}")
169+
if last_tag_info:
170+
repo_lines.append(f"Since last tag {last_tag_info[0]}: {last_tag_info[1]} commits")
171+
print_section("Repo", repo_lines)
172+
173+
build_lines = []
174+
build_lines.append(f"Default ENV: {env_default or 'n/a'}")
175+
build_lines.append(f"PIO envs: {', '.join(pio_env_list) if pio_env_list else 'none'}")
176+
build_lines.append(f"pio version: {tool_version(['pio', '--version'])}")
177+
build_lines.append(f"python3 version: {tool_version(['python3', '--version'])}")
178+
build_lines.append(f"node version: {tool_version(['node', '--version'])}")
179+
build_lines.append(f"npm version: {tool_version(['npm', '--version'])}")
180+
print_section("Build/Tools", build_lines)
181+
182+
artifact_lines = []
183+
artifact_lines.append(f"web/dist: {artifact_present(ROOT / 'web' / 'dist')}")
184+
artifact_lines.append(f"docs/build/html: {artifact_present(ROOT / 'docs' / 'build' / 'html')}")
185+
print_section("Artifacts", artifact_lines)
186+
187+
188+
if __name__ == "__main__":
189+
main()

0 commit comments

Comments
 (0)