Skip to content

stale_project example

Upgrading a project an old just-makeit generated. tree/ is a real project, generated by just-makeit 0.33.14 and never touched since except by its author, who wrote a kernel into it. This example upgrades it to the jm you are running, one step at a time, and proves the result: jm status --check clean, built and tested on Linux, macOS and Windows, the author's code intact, and a bug the old version shipped now fixed.

Use it as the recipe for your own project. Every step below is the same command you would run, and the order matters: first make the project build with today's jm, then take the fixes its generated code is missing.

TL;DR — see it work first

just-makeit example stale_project
# stale_project: OK

What you start with

A project with a standalone object (gain), a module dsp holding an object fir with a variable-output method, an array method, a writable property and a module function — the shapes a real library has. FREEZE.toml records the exact commands that made it.

Its author changed one thing jm generated: the body of fir_step in the sacred header native/inc/fir/fir_core.h.

return x * state->scale;

An upgrade must leave that alone, and this one does.

1. Read before you write

jm status

It exits 1 and sorts what is behind into kinds, each with what to do about it: files the manifest implies that are missing or stale, a binding fragment that is unreconciled, create-only files that are outdated, C cores that are unwired from the C library, and fixes to the root CMakeLists.txt template that yours lacks (root cmake). Keep this output — every step below answers one of its sections.

2. jm apply

jm apply

It writes everything the manifest implies that is missing and reconciles the wiring jm owns, then stamps just-makeit.toml with the version you ran. It also warns, loudly, about a fragment it will not rewrite:

warning ~: native/src/dsp/dsp_ext_fir.c: binding no longer matches the
manifest [...; steps: jm's out= guard tests C-contiguity and this fragment
does not, so a strided out= is filled through a copy and silently ignored]

Hold that thought — step 5 shows what it means, and step 6 fixes it.

3. The root CMakeLists.txt, which jm maintains only in part

apply keeps the root CMakeLists.txt's marked blocks current (the components, modules and external dependencies) and never touches the rest, because the rest is where your own targets go. So no fix jm has made to that template since your project was generated has arrived. jm status names each one under ROOT CMAKE, with what breaks without it:

ROOT CMAKE (8) — fixes jm's root CMakeLists.txt template carries that yours lacks:
  ↑ libm (gh-1452): the combined library does not link libm PUBLIC, so a C consumer built through find_package() fails with undefined math symbols
  ↑ static-name (gh-1368) [Windows]: the static library shares the shared library's OUTPUT_NAME, so both produce <name>.lib: `ninja: error: multiple rules generate`
  ...

Most of them are Windows fixes, and without them this project does not build under clang-cl. jm status --diff prints your file against today's render, so you can see what to merge. This project's file was never edited, so take today's whole, from a scratch project of the same name, and let apply put the managed blocks back:

jm status --diff                                 # what to merge
(cd /tmp && jm new stale)
cp /tmp/stale/CMakeLists.txt CMakeLists.txt      # here: never edited, so whole
jm apply

A fix you have decided against goes in [project] status_allow as CMakeLists.txt:<fix>, e.g. CMakeLists.txt:complex-range.

If you added your own targets to it, merge by hand instead of copying — or move them to a native/src/<dir>/<dir>_extra.cmake, which jm includes and never writes.

4. jm upgrade, and the create-only files jm ships newer

These go together. jm upgrade runs the migrations the schema carries — for this project, respelling complex as _Complex in four files (the same type and ABI). Only the new clib_common.h reads that spelling on every compiler, so adopt it in the same step: both or neither (see Upgrading). clang-cl rejects the old spelling outright.

jm upgrade       # also renames jb.toml -> bootstrap.toml, edits and all
jm status        # lists the create-only files jm ships newer as OUTDATED

jb.toml is what jm used to call bootstrap.toml. apply does not create a fresh bootstrap.toml beside it, because a default there would silently stand in for anything you added to yours; it says so on every run until upgrade renames it.

apply never rewrites a create-only file, because your edits may be in it. Diff each first. None was edited here, so each takes jm's render the same way — delete it, and apply writes today's:

rm .gitignore Makefile Doxyfile bootstrap.toml \
   native/benchmarks/jm_bench.h native/inc/clib_common.h
jm apply

A file you did edit: merge jm's version into yours by hand instead.

The packaging templates are handed to jm rather than deleted. cmake/stale.pc.in and cmake/stale-config.cmake.in are what a C consumer reads through pkg-config and find_package, and today's jm owns them: status lists the two under PACKAGING, and once adopted apply keeps them current by itself. Every line in them is one a released jm rendered -- 0.33.14's -- so adopting drops nothing of yours, and --check says so. (A root CMakeLists.txt you did not replace whole in step 4 is adopted the same way: its install section becomes jm's managed block.)

jm adopt --packaging --check                # each diff, and anything of yours it would drop
jm adopt --packaging

Your C benchmarks too, if you never touched them. Once scaffolded, native/benchmarks/bench_<obj>_core.c is yours, so status cannot call it behind. But the ones 0.33.14 wrote time with POSIX clock_gettime, which Windows does not have, and today's time through jm_bench.h on every platform. If yours are still the scaffold, take today's the same way:

rm native/benchmarks/bench_fir_core.c native/benchmarks/bench_gain_core.c
jm apply

5. See the bug the warning named

The project builds everywhere now. Build it and pass a strided out= buffer — every other element of a longer array:

strided = np.zeros(16, np.float32)[::2]
f.steps(np.ones(8, np.float32), out=strided)   # returns normally...
strided                                        # ...and it is still all zeros

0.33.14's binding fills a temporary copy and hands your buffer back untouched. That is what step 2's warning meant, and apply alone cannot fix it.

6. Receive the fix the binding fragment is missing

A module object's binding fragment (<mod>_ext_<obj>.c) is sacred by default: apply only ever adds members to it, so a fix to a member it already has never arrives. Ask which parts differ from today's render:

jm adopt --check
#   needs acknowledgement fir
#       native/src/dsp/dsp_ext_fir.c
#         differs:   fn:Fir_steps
#         ...

jm cannot tell a hand-written body from a render that predates a fix, so it asks. Look at the diff. Here nobody wrote code in that file, so take today's render — delete it and apply:

rm native/src/dsp/dsp_ext_fir.c
jm apply
jm adopt --check
#   would flip           fir

Then make it jm's for good, so the next fix to a wrapper arrives on the next apply by itself. Under the [fir] table in objects/fir.toml:

[fir]
fragment = "generated"

Put it under [fir], not at the end of the file: appended, it lands in the last [[fir.properties]] table, and jm tells you it has no effect there. Run jm apply once more. If you had written code in the fragment, move it to dsp_ext_fir_extra.c beside it first — jm includes that file and never writes it. See Who owns a module's binding fragment.

7. Prove it

jm status --check      # exit 0
cmake -S . -B ../build && cmake --build ../build && ctest --test-dir ../build

and from Python:

from stale.dsp import Fir
import numpy as np

f = Fir()
f.scale = 2.0
f.steps(np.ones(8, np.float32))          # [2.0, ...]: your kernel, intact
strided = np.zeros(16, np.float32)[::2]
f.steps(np.ones(8, np.float32), out=strided)
# TypeError: out must be a writable, C-contiguous ndarray of the output dtype

The strided out= that 0.33.14's binding silently ignored is now refused.

For jm maintainers: it is also a regression gate

A fresh scaffold cannot show what goes wrong when a file an older jm rendered meets a guard added later — it is rendered by the jm under test. This project can. Everything each step prints is compared with upgrade.golden, and the run first proves tree/ is still the frozen tree (its manifest's jm_version and every file's hash in tree.sha256).

A release that changes what upgrading prints on purpose regenerates the golden in the same PR, and the diff is what gets reviewed:

JM_UPDATE_STALE_GOLDEN=1 make test-examples EXAMPLES_K=stale_project

Never regenerate tree/. The golden growing is the record of every guard added since 0.33.14; if jm ever stops applying this project on purpose, freeze a second one beside it and keep both (gh-1443).

It has already found one: gh-1471, the root-CMakeLists.txt gap in step 3, on its first Windows run.