App command¶
just-makeit app¶
just-makeit app
[--target c|console|pep723]
[--object name | --function name]
[--module name]
[--name name]
[--flag name:type[:default[:help]] ...]
[--command name[:help] ...]
[--argc-argv]
Scaffold a shippable, runnable application from an existing component —
the "one C core, three faces" pattern. Run it after just-makeit object (or
just-makeit function) to turn a C extension into a CLI you can hand to a
user.
# C executable
jm app --target c --object engine --name dsp_tool
# Python console script (installed via [project.scripts])
jm app --target console --object engine --name dsp_tool_py
# PEP 723 inline script (run with `uv run`, no install)
jm app --target pep723 --object engine --name dsp_tool_script
As of 0.15.0 the generated app is complete, not a stub, for the four
object shapes listed below: each target emits a real argument parser and
a working read → process → write loop, generated from the object model. The
C argv parser and the Python argparse setup are produced from the same
spec, so the C binary and the Python CLI accept the same flags and behave the
same way. There is nothing to hand-edit before it runs.
Every run appends an [[app]] row to just-makeit.toml — the app's
target, its source, and any [[app.flags]] / [[app.commands]] — so a
project holds as many apps as you declare, and jm apply recreates each one
(gh-2074). An app is keyed by its --name, which is unique in the project:
- a name already taken is refused, naming
jm remove app <name>; - so is a second app whose default name is taken (the project's name, or
the function's), naming
--name— which is why the three faces above are named apart; - and so is a second console app over the same package, whose
cli.pyit would overwrite (see Targets below).
jm remove app <name> takes one out.
Arguments
| Argument | Description |
|---|---|
--target c\|console\|pep723 |
Output target. Default: c. Short form: -t. One app per run; run jm app again, under another --name, for another face. |
--object name |
Component to wrap. Defaults to the first component. |
--function name |
Wrap a module-level function instead of an object (see Function CLIs). |
--module name |
Module the --object or --function lives in; it decides the import path and where cli.py goes. |
--name name |
Name of the generated app/script, unique in the project. Defaults to the project name (the function name with --function). |
--flag name:type[:default[:help]] |
Extra control flag, added to both parsers and persisted as [[app.flags]]. Repeatable. |
--command name[:help] |
Declare a subcommand (multi-command CLI). Repeatable. See Subcommands. |
--argc-argv |
For an object shape jm app doesn't generate a full loop for (e.g. a --no-step reader): emit an argv-parsing skeleton in the C target's stub instead of a plain (void)argc; (void)argv; no-op. |
Object shapes¶
jm app reads the object's step/steps signature and generates the I/O
loop that fits it — no flag needed:
| Shape | Signature | Generated I/O |
|---|---|---|
| scalar | step(x) -> y |
read one sample → step() → write one, in a loop |
| blockwise | T[] -> U[] (--preset blockwise) |
read a block → steps(in, n, out) → write the block |
| consumer | T -> void |
read → step(); no output side |
| generator | void -> T |
synthetic --count N drives step() → write; no input side |
Constructor state vars become --<name> flags wired into create(), each
defaulting to its --state default. Extra --flag controls are appended to
both the C and Python parsers.
An object outside these four shapes — a --no-step object such as a
reader, or one with an unsupported --arg-type/
--return-type combination — still scaffolds, but jm app has no I/O loop
to generate for it: the C target gets an <<IMPLEMENT: parse argv>> stub
(or --argc-argv's skeleton) and the Python targets get an
<<IMPLEMENT: open input/output, call obj.step(), write>> stub. --function
apps (see below) are unaffected — they always call the function once and
print the result.
Output axes (complex-float streams)¶
When the output element type is complex float32 (a generator or
blockwise shape returning float _Complex), jm app adds a set of built-in
output flags — no declaration needed. They are generated byte-identically
across all three faces (the C face converts inline; the Python faces use
numpy), so a capture is reproducible regardless of which face produced it.
| Flag | Values | Effect |
|---|---|---|
--sample_type |
cf32 cf64 ci32 ci16 ci8 |
wire type, converted on write (full-scale ±1.0 for the integer types) |
--file_type |
raw csv |
raw interleaved I/Q (default) or text I,Q lines (%.9g cf32, %.17g cf64: each reads back exact; %d integer) |
--endian |
le be |
byte order; be reverses each element (raw only — csv is text) |
--record FILE |
path | write a JSON record of the fully-resolved run (every flag after defaulting; choice flags as their chosen string) for reproducible captures |
--sample_type shipped in 0.16.0; --file_type / --endian / --record in
0.17.0.
Richer containers stay application-side. Formats that need context a generic generator can't know — sample rate (BLUE
xdelta), per-segment annotations (SigMF), or a transport (zmq) — are not generated. Provide them in a hand-written tool over your C cores (see doppler'swfmgencomposer, which adds BLUE / SigMF /zmq://alongside the generatedwavegen).
Targets¶
--target c¶
Generates native/src/app/<name>.c — a main() with an argv parser
and the shape-appropriate create → loop → destroy. A numeric flag is read
whole at its declared type, or refused: an argument that is not a number,
overflows, or is outside the type's range (--gain 300 for an int8_t, -1
for an unsigned) prints <prog>: --<flag>: <reason> and exits 2 rather than
running with a wrapped value (gh-2225). jm's App block in CMakeLists.txt holds an
add_executable / target_link_libraries / install group for every C app
the manifest declares, and is rewritten from it on each run (idempotent;
printed for manual addition if there is no CMakeLists.txt).
--target console¶
Generates src/<pkg>/cli.py (or cli.py in the module's package directory
for a --function/--object inside a module: src/<pkg>/<module>/, or the
module's package when it declares one) — an argparse CLI over the Python
bindings, one --<param> per constructor scalar plus any --flags, with the
matching process loop. Adds <name> to [project.scripts] in pyproject.toml
(snippet printed if tomlkit/pyproject.toml is unavailable). The module is
the package's cli.py, not a file named after the app, so one package holds
one console app: a second over the same package is refused, naming the first
app's removal.
--target pep723¶
Generates <name>.py in the project root — a self-contained
PEP 723 script with an inline
# /// script dependency block, the same parser/loop as console.
The # /// script block names your package as a dependency, so uv run
needs it published (or on a local index). Use --target console during
development.
Function CLIs (--function)¶
jm app --function <name> [--module m] generates a CLI over a module-level
function instead of an object: each scalar parameter becomes a flag, the
function is called once, and the result is printed.
jm app --target console --function kaiser_beta --module resample --name kaiser
# kaiser --atten 60 -> prints the computed beta
Subcommands (--command / [[app.commands]])¶
Pass --command name[:help] (repeatable) to scaffold a multi-command CLI:
jm app --target c --object engine --name dsp_tool \
--command encode:"encode a stream" --command decode:"decode a stream"
The C target generates an argv[1] dispatch with a per-command flag-parsing
handler; the Python targets generate an argparse subparsers tree. Each
command body is an <<IMPLEMENT>> marker. The file is regenerated by every
jm app and jm apply, command bodies included, so implement each command in
a component (jm method / jm function) and call it from the body rather
than writing the logic into the generated file. Commands persist as
[[app.commands]].
TOML record¶
One [[app]] row per app. TOML attaches each [[app.flags]] and
[[app.commands]] row to the [[app]] row above it:
[[app]]
target = "c"
name = "dsp_tool"
object = "engine" # or: function = "kaiser_beta"
[[app.flags]]
name = "gain"
type = "float"
default = "1.0"
help = "output gain"
[[app]]
target = "console"
name = "dsp_cmds"
[[app.commands]]
name = "encode"
help = "encode a stream"
jm apply reads every [[app]] row (with its [[app.flags]] /
[[app.commands]]) and regenerates each app from it on every run, as
jm app does. The app source is jm's: edits to it are discarded. Put custom
logic in a component (jm method) and call it from the generated main().
Schema 8 and earlier held one app, as a single [app] table. This jm reads
the [[app]] spelling alone and refuses the old one, naming jm upgrade,
which rewrites it in place as a one-row [[app]] — see
Upgrading.
Removing an app¶
Removes the [[app]] row named dsp_tool and the app's file —
native/src/app/<name>.c, the package's cli.py, or <name>.py — with its
wiring: the C app's lines in the App block of CMakeLists.txt, the console
app's [project.scripts] entry. A file you edited is kept, with a note to
delete it by hand, the way jm remove method leaves an authored body in
_core.c.
An object, module or function an app is built from cannot be removed while
the app is there: jm remove refuses, naming each app and
jm remove app <name>, and changes nothing (gh-2075).
See the bundled three_face and app_shapes examples
(jm example three_face, jm example app_shapes) for end-to-end runs.