Current section
Files
Jump to
Current section
Files
docs/development.md
# Working on the package
For people changing ExMujoco itself. Using it needs none of this — `README.md` and the API
reference cover that.
## The shape of the repository
| | |
| --------------------------------- | ------------------------------------------------------------------------- |
| `lib/ex_mujoco.ex` | the curated API, hand-written |
| `lib/ex_mujoco/` | `Model`, `Data`, `Api`, `Native` and `Types/*` — **generated**, committed |
| `c_src/ex_mujoco_nif.cpp` | the hand-written half of the NIF: loading, state, contacts, errors |
| `c_src/manifest.exs` | which C functions are bound 1:1, and which are bound by hand |
| `c_src/generated/` | `fields.inc` and `functions.inc` — **generated**, committed |
| `scripts/prepare_mujoco.exs` | fills one MuJoCo directory, from a release, source, or yours |
| `scripts/tag_and_push_version.sh` | tags the version and lets the release workflow publish |
## How the bindings are made
MuJoCo generates its own Python bindings by reading its headers. This package does the same:
`mix ex_mujoco.gen` parses `MUJOCO_DIR/include/mujoco/*.h` — functions, struct members with the
extents from `mjxmacro.h`, and enums — and writes the C++ that Fine registers and the Elixir
that calls it. The output is committed, so a package build needs neither the generator nor a
network, and `mix ex_mujoco.gen --check` (which CI runs) fails when the two disagree.
`c_src/manifest.exs` is the one hand-edited decision: which C functions are bound 1:1, and
which are bound by hand because each call needs an answer no signature gives.
A change to a template in `lib/ex_mujoco/gen/` can emit C++ the compiler rejects, and
`mix ex_mujoco.gen` compiles the project before it runs — a deadlock. Make the edit to
`c_src/generated/*.inc` by hand first, compile, then generate.
## Tests
`mix test` runs the suite against the pinned MuJoCo: behaviour (`step`, state round-trips,
errors), the generated fields, the tensor shape rules, the generator's own checks, and two
contracts worth knowing about —
- **the package ships what the build reads** (`test/ex_mujoco/package_test.exs`), because a
file left out of `package: files` is a dependency that cannot compile;
- **the pin is the version the library answers**, so a build against `main` fails rather than
reporting physics it cannot name.
`mix precommit` — when it is present — is compile with `--warnings-as-errors`, format check,
`credo --strict`, `dialyzer`, the generator check, and the suite.
## CI
`ci.yml`, on every push that touches something other than prose:
| job | what it proves |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| `lint` | no warnings, formatted, credo, and that `VERSION`, `mix.exs` and the README agree |
| `gen` | `mix ex_mujoco.gen --check` — the committed bindings are the pinned headers |
| `types` | `mix dialyzer` |
| `test (ubuntu, windows)` | the suite on both, with MuJoCo fetched fresh on every run |
| `package (ubuntu, windows)` | the tarball Hex would ship is unpacked and installed with `Mix.install/2` in an empty directory |
There is no macOS job: this account has no macOS runner, and a job that cannot start is
cancelled rather than failed, which reddens a run for something the build did not do. macOS is
the machine the package is developed on, and `mix hex.build` → `Mix.install` is how a release is
verified there.
`mujoco-main.yml` runs once a day against upstream `main` and is information, not a gate: it
builds MuJoCo from source at `heads/main` and steps a model, so a pin that can no longer be
moved is known before somebody tries to move it.
## Docs
`mix docs` has to print nothing, and `mix hex.publish` builds the docs from the package, so the
documentation is part of a release rather than something added afterwards.
- The front page is `README.md`, and it is written for a caller: install, the pin, usage, where
to look. Anything about working on the package belongs here, not there.
- `mix.exs` groups the modules with patterns: `Types` is a regular expression over
`ExMujoco.Types.*` against the module name, because the alternative is eighty module names in
a list. `Physics`, `The C API` and `Errors` are short lists; `Generating the bindings` is the
other pattern, so the generator's modules do not sit among the API.
- Prose names an internal module by what it does ("the NIF's load hook") or by its file
(`priv/ex_mujoco_preload.dll`). A backticked `` `ExMujoco.Native` `` in a rendered document is
an ex_doc warning, because that module is hidden on purpose — and warnings in a docs build are
how a stale link survives a release.
- The generated field functions carry the generator's description of the field, taken from
MuJoCo's own header comments, so `ExMujoco.Model.geom_size/1` is documented with MuJoCo's
documentation for `geom_size`.
- A field belongs to a family named by MuJoCo's own prefix — `geom_size`, `geom_pos`,
`geom_rbound` are `geom` — and the generator puts `@doc group: "geom"` on the function once ten
fields share it. Fewer than ten stay in the plain function list: a heading over two functions
is worse than no heading. The rule lives in `@family_size` in `lib/ex_mujoco/gen/template.ex`,
so it is changed in one place and `mix ex_mujoco.gen` decides the rest.
## Releasing
`VERSION` is the one place the version lives; `mix.exs` and the README's dependency line are
updated from it.
```console
$ ./scripts/tag_and_push_version.sh --dry-run # what it would do
$ ./scripts/tag_and_push_version.sh # v0.1.3, pushed
```
It refuses to tag a commit whose CI is not green, and a commit CI never saw — a docs-only push,
since the workflow ignores prose — is compared against the last run that did succeed.
The tag is what `.github/workflows/release.yml` publishes from. It checks that the tag, `VERSION`
and `mix.exs` agree; **that the build came from the pinned MuJoCo** (no override in the
environment, and the prepared library's `.provider` says `release=<pin>`);
`mix ex_mujoco.gen --check`; then `mix hex.publish` and the GitHub release.
Hex stages every publish on this account for approval — package and docs separately — so
`mix hex.publish` succeeding is not the same as being live. Check
`https://hex.pm/api/packages/ex_mujoco/releases/<version>` and the dashboard link the publish
prints.
A patch bump is the default between releases: nothing a caller wrote has to change.