Code generation › Plugins

Code generation

Plugins

A plugin brings a format or a framework to archec in one folder. It holds three kinds of declarations:

  • a generator adds a code target (archec gen --target <id>, see generators);
  • an importer reads a file of another tool into arche-lang (archec import <file>);
  • an exporter writes the model as a file of another tool (archec export <main.arche> --format <id>).
plugins/mccoffee/
  library.arche     library mccoffee "McCOFFEE" { version "0.1.0"; kind plugin }
  plugin.arche      its generators, importers and exporters
  melody/import.ts
  melody/export.ts
  cpp/generate.ts
  README.md
importer melody "Melody CCM" {
  script "melody/import.ts"
  extensions ".ccmproject"
  reads "../.."
}

exporter melody "Melody CCM" {
  script "melody/export.ts"
  extension ".ccmproject"
}

generator mccoffee-cpp "McCOFFEE C++" {
  script "cpp/generate.ts"
  option framework "The runtime" { type string; values mccoffee, stl; default mccoffee }
}
archec import TU_Timers.ccmproject -o timers.arche
archec export main.arche --format melody -o timers.ccmproject
archec gen main.arche --target mccoffee-cpp --option framework=stl
archec plugin list main.arche

These are not the Arche app's plugins (plugin records, plugins.yaml): those run in the app.

Where archec finds plugins#

Where Source Runs its code
plugins/<name>/plugin.arche beside the main file the project's own at once
a library of kind plugin that the project uses (from the Store, git or a folder) library, store once approved
ARCHE_PLUGINS, a path list of plugin folders (or folders of them) the user's at once
lib/plugins/<name>/ beside archec built in at once

When two declare the same id, the nearer wins: the project's own declarations first, then the order of the table. A project needs no include: uses mccoffee "^1" is enough. A plugin's declarations are read on their own. Only the commands that run them see them, and a project's generator x from <a plugin's>.

A built-in plugin's hooks (after c) do not run: what archec ships does not change every project's files.

archec plugin list [main.arche] lists the plugins, where each comes from, and what each brings, with their options. It also says what is wrong in a plugin's own declarations. Without a project, it lists the built-in plugins and those of ./plugins.

Importers#

archec import <file> picks the importer whose extensions end the file's name, the longest ending first. A built-in importer keeps its own endings (.dbc, .sysml, .urdf, …); a plugin's importer takes them only when it is named: archec import bus.dbc --importer mydbc.

Without --project, archec import sees the plugins of main.arche in the current folder, else only the built-in ones and those of ./plugins. A library's plugin needs its project: --project main.arche.

A script#

export default function importFile(input, ctx) {
  // input: { api: 1, importer, file, name, text, options }
  const names = [...input.text.matchAll(/identifier="(\w+)"/g)].map((m) => m[1]);
  if (!names.length) ctx.warn({ line: 1 }, 'no identifier');
  return { files: { 'main.arche': names.map((n) => `component ${n}`).join('\n') + '\n' } };
}

export function importFile and module.exports = function … work too. It returns { files, notes, warnings, errors }:

  • files: { "path": "text" }, or [{ path, text }]. Paths are relative to the output, and never leave it.
  • notes: plain strings.
  • warnings, errors: strings, or { where, message }. where is a line of the imported file ({ line: 12 }) or of another one ({ file, line }).

ctx gives:

  • options: the importer's options, with --option k=v for a run;
  • readFile(rel), exists(rel), listFiles(rel): files beside the imported file, relative to its folder;
  • readOwn(rel): a file of the plugin's folder (its tables, its library models);
  • render(template, data), filters: the template language;
  • note(msg), warn(where, msg), error(where, msg).

What a script may read#

A script runs in the generators' sandbox: no process, no network, no module but the plugin's own (import … from './core/view'). It reads, and only reads:

  • the imported file's folder and what is under it;
  • the plugin's own folder;
  • the folders the importer names with reads, relative to the imported file's folder. A Melody model names the projects two folders up, so the McCOFFEE importer says reads "../..";
  • the folders given with --allow-read <dir> for one run.

A read outside them, or through a link that leads out, is an error.

A command#

importer kv "Key-value files" { command "python3 kv.py"; extensions ".kv" }

The command runs in the plugin's folder. It reads the input as JSON on its stdin — { "api": 1, "importer", "file", "name", "text", "options" } — and writes the result as JSON on its stdout. What it writes on stderr is shown as notes. A command is not sandboxed: it is code, approved like a script.

What archec does with the result#

  • No -o: one file goes to stdout; several are an error.
  • -o x.arche: the one file.
  • -o folder: the files, under it.

Then archec checks what it wrote, as archec build would: the main.arche written, else the first .arche. An error there fails the import (exit 1).

Exporters#

An exporter is a generator whose files are another tool's: it reads the same generator model (version 1), and comes as templates, a script (function generate(model, ctx)) or a command. Its options, typed options, from, needs deployment and errors at the elements' at work as a generator's.

archec export <main.arche> --format <id> runs it. Without --format, -o picks the exporter whose extension ends its name; -o model.json stays the model's JSON.

  • One file: -o is that file, whatever the exporter calls it. Without -o, it goes to stdout.
  • Several files: -o is a folder.

Plugins as packages#

A Store package of kind plugin is a plugin: its library.arche, its plugin.arche, and the files they need. Scripts are TypeScript (.ts): a package holds no .js. Templates may end in .tpl.

archec store check ./mccoffee        # kind plugin needs a plugin.arche
archec add main.arche arche/mccoffee@^0.1
archec plugin approve main.arche mccoffee

archec store seed publishes the built-in plugins as arche/<name>.

Approval#

A library's templates run as they are. Its scripts and commands run on the machine of whoever builds the project, so they run only once approved:

archec plugin approve main.arche mccoffee            # lists its code, then records it
archec plugin approve main.arche mccoffee --revoke

Each script or command is recorded in arche.lock at the library's fingerprint: mccoffee/importer:melody, mccoffee/exporter:melody, and mccoffee/mccoffee-cpp for a generator (archec generator approve approves one generator). When the library changes, it is refused until it is approved again. The project's own plugins, the user's and the built-in ones need no approval.

What is checked#

The codes are the generators': generator and generator-option.

  • An importer is a script or a command, not templates.
  • An importer without extensions runs only with --importer (a warning).
  • An importer or an exporter has no after, and an importer no builtins.
  • Options are held as a generator's.

The McCOFFEE plugin#

lib/plugins/mccoffee is built in. Its README says what it brings now and what comes next: Melody CCM in and out, C++ for McCOFFEE and the STL, Rust for threads, tokio and embassy, and the signal-processing graphs.

Its importer melody reads a Melody CCM project with the projects it refers to, and its exporter melody writes one that Melody and PYCOGEN read (importing). The library models a .ccmproject refers to are in melody/MM/; the importer reads them with ctx.readOwn.

Its C++ generator, mccoffee-cpp, writes the files PYCOGEN writes for McCOFFEE, by PYCOGEN's names, its profile as the generator's options; --option framework=stl writes the same shape on std:: only. lib/plugins/mccoffee/tools/mccoffee-run.sh builds and runs what it wrote against McCOFFEE, in a container. Its scripts share one view of the model, core/view.ts: they import it.

--target mccoffee-rust is its Rust: a Cargo workspace with a crate per component and a binary per process, for the runtime the user picks (--option runtime=threads|tokio|embassy). Its script reads the model in rust/view.ts and imports its own modules.