README: the logo, what works now, and measured numbers
The README led with a wallpaper and described the system of two months ago: no desktop, no dynamic linking, a handful of drivers. It now leads with Shift and covers Xorg and Window Maker, apk and Alpine packages, SCFF, HTTPS, the storage, network and input drivers, and the machines it has booted on. "Not there yet" lists what is actually missing today: threads, udev, EHCI, touchpad gestures, an installer. The numbers -- lines of code and of comments, syscalls, drivers, packages, commits -- come from tools/python/stats.py, which rewrites them in the README and on the website between stats markers instead of somebody typing them once and letting them go stale. .gitattributes keeps the generated font and logo tables and the vendored Niveus out of the repository page's language bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
e43f56c619
commit
4265794437
4 changed files with 358 additions and 47 deletions
217
tools/python/stats.py
Normal file
217
tools/python/stats.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""The project's numbers, measured from the tree and written where they are shown.
|
||||
|
||||
python3 tools/python/stats.py rewrite README.md and the website
|
||||
python3 tools/python/stats.py --print only print them
|
||||
|
||||
Both places carry the numbers between markers -- <!-- stats:begin --> and
|
||||
<!-- stats:end --> -- and everything between them is regenerated. A number in
|
||||
the README that somebody typed is a number that was true once; these are true
|
||||
on the commit that ran this.
|
||||
|
||||
First-party code only: ports/ is other people's software, build/ is output,
|
||||
and docs/Website/niveus is a vendored design system. Comment lines are counted
|
||||
apart from code, because in this tree they are a large part of the work.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
FIRST_PARTY = ["kernel", "include", "boot", "lib", "cmd", "tools"]
|
||||
LANGS = {
|
||||
".c": "C", ".h": "C", ".S": "Assembly", ".s": "Assembly", ".asm": "Assembly",
|
||||
".py": "Python", ".sh": "Shell", ".ps1": "PowerShell", ".pl": "Perl",
|
||||
".ld": "Linker script", ".inc": "C",
|
||||
}
|
||||
GENERATED = re.compile(r"include/gfx/(logo|font)[^/]*\.h$|_data\.h$")
|
||||
|
||||
|
||||
def git(*args):
|
||||
return subprocess.run(["git", "-C", ROOT, *args], capture_output=True,
|
||||
text=True, check=True).stdout
|
||||
|
||||
|
||||
def tracked():
|
||||
return [p for p in git("ls-files").splitlines()
|
||||
if p.split("/")[0] in FIRST_PARTY and not GENERATED.search(p)]
|
||||
|
||||
|
||||
def count(path, lang):
|
||||
"""(code, comment, blank) lines. Block comments are followed for C-family
|
||||
files; for the scripting languages a line starting with # is a comment."""
|
||||
code = comment = blank = 0
|
||||
in_block = False
|
||||
try:
|
||||
with open(os.path.join(ROOT, path), encoding="utf-8", errors="replace") as f:
|
||||
lines = f.read().splitlines()
|
||||
except OSError:
|
||||
return 0, 0, 0
|
||||
hash_comments = lang in ("Python", "Shell", "PowerShell", "Perl")
|
||||
for raw in lines:
|
||||
s = raw.strip()
|
||||
if not s:
|
||||
blank += 1
|
||||
continue
|
||||
if hash_comments:
|
||||
if s.startswith("#") and not s.startswith("#!"):
|
||||
comment += 1
|
||||
else:
|
||||
code += 1
|
||||
continue
|
||||
if in_block:
|
||||
comment += 1
|
||||
if "*/" in s:
|
||||
in_block = False
|
||||
if s.split("*/", 1)[1].strip():
|
||||
comment -= 1
|
||||
code += 1
|
||||
continue
|
||||
if s.startswith("//") or (lang == "Assembly" and s.startswith("#") and
|
||||
not re.match(r"#\s*(include|define|if|endif|else|elif|undef)", s)):
|
||||
comment += 1
|
||||
elif s.startswith("/*"):
|
||||
comment += 1
|
||||
if "*/" not in s[2:]:
|
||||
in_block = True
|
||||
else:
|
||||
code += 1
|
||||
return code, comment, blank
|
||||
|
||||
|
||||
def measure():
|
||||
by_lang = {}
|
||||
kernel = [0, 0]
|
||||
files = 0
|
||||
for p in tracked():
|
||||
ext = os.path.splitext(p)[1]
|
||||
lang = LANGS.get(ext)
|
||||
if lang is None:
|
||||
continue
|
||||
c, m, _ = count(p, lang)
|
||||
files += 1
|
||||
t = by_lang.setdefault(lang, [0, 0])
|
||||
t[0] += c
|
||||
t[1] += m
|
||||
if p.startswith(("kernel/", "include/")):
|
||||
kernel[0] += c
|
||||
kernel[1] += m
|
||||
|
||||
table = open(os.path.join(ROOT, "kernel/sys/table.inc")).read()
|
||||
calls = re.findall(r"^SYSCALL\(\s*\d+,\s*(\w+),", table, re.M)
|
||||
linux = [n for n in calls if n != "NO_LINUX"]
|
||||
|
||||
drivers = set()
|
||||
for p in tracked():
|
||||
if p.startswith("kernel/drivers/") and p.endswith(".c"):
|
||||
parts = p.split("/")
|
||||
drivers.add("/".join(parts[2:4]) if len(parts) > 4 else parts[-1])
|
||||
drivers.add("i2c-hid") # kernel/acpi/i2c_hid.c, found through ACPI
|
||||
|
||||
programs = {p.split("/")[1] for p in tracked() if p.startswith("cmd/") and p.count("/") >= 2}
|
||||
|
||||
world = [l for l in open(os.path.join(ROOT, "ports/alpine/world")).read().splitlines()
|
||||
if l.strip() and not l.startswith("#")]
|
||||
lock = [l for l in open(os.path.join(ROOT, "ports/alpine/world.lock")).read().splitlines()
|
||||
if l.strip() and not l.startswith("#")]
|
||||
|
||||
commits = int(git("rev-list", "--count", "HEAD").strip())
|
||||
first = git("log", "--reverse", "--format=%ad", "--date=short").splitlines()[0]
|
||||
days = (datetime.date.today() - datetime.date.fromisoformat(first)).days
|
||||
|
||||
code = sum(v[0] for v in by_lang.values())
|
||||
comments = sum(v[1] for v in by_lang.values())
|
||||
return {
|
||||
"code": code, "comments": comments, "files": files,
|
||||
"by_lang": sorted(by_lang.items(), key=lambda kv: -kv[1][0]),
|
||||
"kernel_code": kernel[0], "kernel_comments": kernel[1],
|
||||
"syscalls": len(calls), "linux_syscalls": len(linux),
|
||||
"drivers": len(drivers), "programs": len(programs),
|
||||
"world": len(world), "packages": len(lock),
|
||||
"commits": commits, "first": first, "days": days,
|
||||
}
|
||||
|
||||
|
||||
def n(v):
|
||||
return f"{v:,}"
|
||||
|
||||
|
||||
def readme_block(s):
|
||||
ratio = 100 * s["comments"] / max(1, s["code"] + s["comments"])
|
||||
rows = "\n".join(f"| {lang} | {n(c)} | {n(m)} |" for lang, (c, m) in s["by_lang"])
|
||||
return f"""<!-- stats:begin -->
|
||||
| | |
|
||||
|---|---|
|
||||
| **First-party code** | {n(s['code'])} lines, plus {n(s['comments'])} lines of comments ({ratio:.0f}%) in {n(s['files'])} files |
|
||||
| **Kernel** (`kernel/`, `include/`) | {n(s['kernel_code'])} lines of code, {n(s['kernel_comments'])} of comments |
|
||||
| **Syscalls** | {s['syscalls']}, of which {s['linux_syscalls']} on Linux numbering |
|
||||
| **Drivers** | {s['drivers']} in the kernel |
|
||||
| **First-party programs** | {s['programs']} in `cmd/` |
|
||||
| **Alpine packages in the root** | {s['packages']}, from {s['world']} asked for in `ports/alpine/world` |
|
||||
| **History** | {n(s['commits'])} commits since {s['first']} ({s['days']} days) |
|
||||
|
||||
<details>
|
||||
<summary>Lines by language</summary>
|
||||
|
||||
| Language | Code | Comments |
|
||||
|---|--:|--:|
|
||||
{rows}
|
||||
|
||||
</details>
|
||||
|
||||
<sub>Measured by <code>tools/python/stats.py</code>; first-party code only, so
|
||||
<code>ports/</code> and generated tables are not counted.</sub>
|
||||
<!-- stats:end -->"""
|
||||
|
||||
|
||||
def site_block(s):
|
||||
items = [
|
||||
(f"lines of code in <code class=\"nv-code\">kernel/</code> and <code class=\"nv-code\">include/</code>", n(s["kernel_code"])),
|
||||
("lines of comments alongside them", n(s["kernel_comments"])),
|
||||
("first-party lines in all", n(s["code"])),
|
||||
("syscalls, Linux numbering", str(s["linux_syscalls"])),
|
||||
("drivers in the kernel", str(s["drivers"])),
|
||||
("Alpine packages in the live root", str(s["packages"])),
|
||||
("commits", n(s["commits"])),
|
||||
("days since the first one", str(s["days"])),
|
||||
("patches carried against fastfetch", "0"),
|
||||
]
|
||||
out = ["<!-- stats:begin -->", " <dl class=\"nv-stat-group\">"]
|
||||
for label, value in items:
|
||||
out += [" <div class=\"nv-stat\">",
|
||||
f" <dt class=\"nv-stat__label\">{label}</dt>",
|
||||
f" <dd class=\"nv-stat__value\">{value}</dd>",
|
||||
" </div>"]
|
||||
out += [" </dl>", " <!-- stats:end -->"]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def rewrite(path, block):
|
||||
full = os.path.join(ROOT, path)
|
||||
text = open(full, encoding="utf-8").read()
|
||||
new, k = re.subn(r"<!-- stats:begin -->.*?<!-- stats:end -->", lambda _: block,
|
||||
text, flags=re.S)
|
||||
if k != 1:
|
||||
sys.exit(f"stats: {path} needs exactly one stats:begin/stats:end pair")
|
||||
if new != text:
|
||||
open(full, "w", encoding="utf-8").write(new)
|
||||
print(f"stats: rewrote {path}")
|
||||
|
||||
|
||||
def main():
|
||||
s = measure()
|
||||
if "--print" in sys.argv:
|
||||
for k, v in s.items():
|
||||
print(f"{k}: {v}")
|
||||
return
|
||||
rewrite("README.md", readme_block(s))
|
||||
rewrite("docs/Website/index.html", site_block(s))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue