Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ jobs:
- name: Every module is documented
run: python3 tools/check_module_docs.py

# The counts quoted in Cargo.toml, pyproject.toml and the READMEs are
# written by hand from the two generated files above. This pins them
# to those files, so a library that grows fails the build instead of
# shipping a stale figure to a registry, where the description of a
# published version cannot be edited.
Comment on lines +93 to +97
- name: Quoted counts match the generated figures
run: python3 tools/check_counts.py

coverage:
name: Coverage
runs-on: ubuntu-latest
Expand Down
49 changes: 3 additions & 46 deletions .github/workflows/crates-release.yml
Original file line number Diff line number Diff line change
@@ -1,44 +1,15 @@
name: Crates.io release

# Publishes the library crate to crates.io. Its sibling python-release.yml
# publishes the bindings to PyPI, and both answer to `push: tags: v*`, so one
# tag ships both registries.
#
# A file of its own rather than a job in python-release.yml, for the same
# reason that file gives for existing: a trusted publisher is registered
# against a workflow *filename*. One file per registry means either
# publisher can be reconfigured without disturbing the other, and a failure
# on one registry does not strand the other mid-run.
#
# ── How the upload is authorised ─────────────────────────────────────
#
# crates.io Trusted Publishing, the same OIDC exchange PyPI uses, so there
# is no registry token in this repository's secrets. The publisher
# registered on crates.io for `rust_physics_engine` names:
#
# repository Magic-Man-us/RustPhysicsEngine
# workflow crates-release.yml <- this file's name
# environment crates-io <- the job's environment, below
#
# crates-io-auth-action trades the workflow's OIDC token for a crates.io
# token scoped to this run, and revokes it in its post step. Renaming this
# file or changing that environment breaks the release until crates.io is
# told.
#
# ── Why workflow_dispatch ────────────────────────────────────────────
#
# v0.2.0 was tagged before this file existed, so no tag push can ever fire
# it for that version. Dispatch runs from the default branch and checks out
# whichever existing tag it is given, which is the only way to publish a
# version whose tag predates this workflow. From v0.3.0 on the tag push is
# enough and this input should go unused.
# The crates.io trusted publisher is keyed to this file's name and the
# environment below. Renaming either silently breaks releases.

on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
# For versions tagged before this file existed; no tag push can reach them.
description: "An existing tag to publish, e.g. v0.2.0"
required: true
type: string
Expand All @@ -52,20 +23,13 @@ jobs:
publish:
name: "Publish to crates.io"
runs-on: ubuntu-latest
# Named so crates.io can be told to trust exactly this job, and so a
# required reviewer on the environment gates the one step here that
# cannot be undone: a published version can be yanked, never removed.
environment:
name: crates-io
url: "https://crates.io/crates/rust_physics_engine"
permissions:
contents: read
# The OIDC token Trusted Publishing exchanges for an upload. The only
# elevated permission in the file, scoped to this job.
id-token: write
steps:
# A tag push carries the tag in ref_name; a dispatch carries it in the
# input. Both paths continue as one value from here.
- name: The tag being published
id: ref
run: 'echo "tag=${{ inputs.tag || github.ref_name }}" >> "$GITHUB_OUTPUT"'
Expand All @@ -75,19 +39,12 @@ jobs:
ref: "${{ steps.ref.outputs.tag }}"
persist-credentials: false

# The same gate python-release.yml runs, and the reason a malformed or
# mismatched tag stops here: check_version.py rejects anything that is
# not vX.Y.Z, and refuses a tag whose version disagrees with the
# manifests. crates.io will not re-use a version number either.
- name: The tag, the crate and the bindings agree
run: "python3 bindings/python/check_version.py '${{ steps.ref.outputs.tag }}'"

- uses: rust-lang/crates-io-auth-action@v1
id: auth

# Verification is left on: the crate is dependency-free and builds in
# seconds, so the packaged artifact is compiled before upload rather
# than trusted.
- name: Publish
run: "cargo publish"
env:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "rust_physics_engine"
version = "0.2.0"
edition = "2021"
description = "A zero-dependency Rust library for physics, mathematics and engineering computation — 6,365 public functions across 71 modules"
description = "A zero-dependency Rust library for physics, mathematics and engineering computation — 4,124 public functions across 295 modules"
license = "MIT"
repository = "https://github.com/Magic-Man-us/RustPhysicsEngine"
homepage = "https://github.com/Magic-Man-us/RustPhysicsEngine"
Expand Down
2 changes: 1 addition & 1 deletion bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ extension.
## What is not bound

4,086 of the library's 4,149 free functions, 2,254 of its 2,277 methods,
416 of its 426 types and all 106 of its constants, across 296 modules.
416 of its 426 types and all 106 of its constants, across 295 modules.

The rest is mostly three things: functions generic over a type parameter,
which cannot be monomorphised without knowing what to monomorphise to;
Expand Down
2 changes: 1 addition & 1 deletion bindings/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "numeria"
description = "Physics, mathematics and engineering computation: 4,000+ validated functions across 71 domains, in Rust, callable from Python"
description = "Physics, mathematics and engineering computation: 4,086 functions across 71 domains, in Rust, callable from Python"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.9"
Expand Down
125 changes: 125 additions & 0 deletions tools/check_counts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Check counts quoted in prose against the files that derive them.

MODULE_MAP.md and COVERAGE.md disagree by design -- the map counts a
macro-generated item once, the generator counts what it emitted -- so each
claim names which of them owns it.
"""

from __future__ import annotations

import os
import re
import sys

ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
MODULE_MAP = "docs/MODULE_MAP.md"
COVERAGE = "bindings/python/COVERAGE.md"


def read(rel: str) -> str:
with open(os.path.join(ROOT, rel), encoding="utf-8") as fh:
return fh.read()
Comment on lines +20 to +22


def plain(n: str) -> int:
return int(n.replace(",", ""))


def module_map_facts() -> dict[str, int]:
text = read(MODULE_MAP)
pairs = {
"modules": r"\*\*([\d,]+) modules\*\*",
"top_level": r"\*\*([\d,]+) public top-level modules\*\*",
"files": r"\*\*([\d,]+) files\*\*",
"map_functions": r"\*\*([\d,]+) public functions\*\*",
"map_methods": r"\*\*([\d,]+) public methods\*\*",
"map_types": r"\*\*([\d,]+) public types\*\*",
}
out = {}
for key, pat in pairs.items():
m = re.search(pat, text)
if not m:
sys.exit(f"{MODULE_MAP}: could not find the {key} figure -- regenerate it")
out[key] = plain(m.group(1))
return out


def coverage_facts() -> dict[str, int]:
text = read(COVERAGE)
rows = {
"functions": "Free functions",
"methods": "Methods",
"classes": "Classes",
"constants": "Constants",
}
out = {}
for key, label in rows.items():
m = re.search(rf"^\|\s*{label}\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|", text, re.M)
if not m:
sys.exit(f"{COVERAGE}: could not find the {label!r} row -- regenerate it")
out[f"rust_{key}"] = int(m.group(1))
out[f"bound_{key}"] = int(m.group(2))
return out


# A literal space in a pattern means any whitespace: these sentences are
# hard wrapped and a reflow must not read as a missing claim.
CLAIMS: list[tuple[str, str, str]] = [
("Cargo.toml", r"([\d,]+) public functions", "map_functions"),
("Cargo.toml", r"public functions across ([\d,]+) modules", "modules"),
("README.md", r"the ([\d,]+) modules", "modules"),
("docs/GUIDE.md", r"([\d,]+) modules", "modules"),
("bindings/python/pyproject.toml", r"([\d,]+) functions across", "bound_functions"),
("bindings/python/pyproject.toml", r"functions across ([\d,]+) domains", "top_level"),
("bindings/python/README.md", r"^([\d,]+) functions, ", "bound_functions"),
("bindings/python/README.md", r"functions, ([\d,]+) methods", "bound_methods"),
("bindings/python/README.md", r"methods, ([\d,]+) classes", "bound_classes"),
("bindings/python/README.md", r"classes and ([\d,]+) constants", "bound_constants"),
("bindings/python/README.md", r"constants across ([\d,]+) domains", "top_level"),
("bindings/python/README.md", r"^([\d,]+) of the library's", "bound_functions"),
("bindings/python/README.md", r"library's ([\d,]+) free functions", "rust_functions"),
("bindings/python/README.md", r"free functions, ([\d,]+) of its", "bound_methods"),
("bindings/python/README.md", r"of its ([\d,]+) methods", "rust_methods"),
("bindings/python/README.md", r"methods, ([\d,]+) of its", "bound_classes"),
("bindings/python/README.md", r"of its ([\d,]+) types", "rust_classes"),
("bindings/python/README.md", r"all ([\d,]+) of its constants", "bound_constants"),
("bindings/python/README.md", r"constants, across ([\d,]+) modules", "modules"),
]


def compile_claim(pattern: str) -> re.Pattern[str]:
return re.compile(pattern.replace(" ", r"\s+"), re.M)


def main() -> int:
facts = module_map_facts() | coverage_facts()
problems: list[str] = []
for rel, pattern, key in CLAIMS:
text = read(rel)
m = compile_claim(pattern).search(text)
if not m:
problems.append(f"{rel}: no text matching {pattern!r} -- the sentence moved")
continue
found, want = plain(m.group(1)), facts[key]
if found != want:
line = text[: m.start(1)].count("\n") + 1
problems.append(
f"{rel}:{line}: says {found:,} but {key} is {want:,}"
)
if problems:
print("count check failed:", file=sys.stderr)
for p in problems:
print(f" {p}", file=sys.stderr)
print(
f"\nthe figures come from {MODULE_MAP} and {COVERAGE}, both generated;"
"\nfix the prose, not those files",
file=sys.stderr,
)
return 1
print(f"{len(CLAIMS)} quoted counts agree with {MODULE_MAP} and {COVERAGE}")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading