genspec-wasi

command
v0.36.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

genspec-wasi

A headless spec generator: point it at annotated Go source, get a Swagger 2.0 document on standard output. It is the non-interactive counterpart to genspec-tui, and the form codescan takes when it is built for WebAssembly.

It depends on nothing beyond the library itself, which is the point: it cross-compiles to wasip1/wasm and runs under any WASI runtime, with no Go toolchain present and no subprocess.

Audience: codescan/go-swagger maintainers and contributors.

Install and run

genspec-wasi lives in the main module, so it carries no dependencies the library does not already have.

go install github.com/go-openapi/codescan/cmd/genspec-wasi@latest

# scan the module in the current directory
genspec-wasi

# or point it somewhere, and narrow the scope
genspec-wasi -workdir ../my-api ./internal/models/... ./internal/api/...

From a checkout:

go run ./cmd/genspec-wasi -workdir ./testdata ./goparsing/petstore/...
Flag Default Meaning
-workdir . directory the scan runs from; patterns are relative to it
-scan-models true also emit definitions for swagger:model types
(one flag per boolean option) every boolean knob on codescan.Options has a flag, named after the field in kebab-case — -prune-unused-models, -ref-aliases, -clean-go-doc … Run genspec-wasi -h for the list. The one exception is ToolchainFreeLoader, which -loader states more fully.
-build-tags comma-separated build tags to apply while loading
-goos / -goarch this machine's the platform the scanned code is built for
-loader auto here auto and own are the same thing: a WebAssembly guest can start no subprocess, so go list can never run. go is refused up front for that reason, rather than failing deep inside the go command
-export-data directory or .zip of precomputed dependency types (see below)
-stub-stdlib false synthesize standard-library types instead of reading GOROOT
-compiled-dependencies false take dependency types from the compiler's export data rather than reading every dependency from source. The spec is the same either way; this is much faster on a warm build cache and much slower on a cold one, since export data has to be compiled before it exists. Native builds only — it needs -loader=go, which WebAssembly cannot run
-format spec spec writes the document alone; json wraps it with diagnostics and provenance (see below)
-output - where to write the specification
-indent true indent the emitted JSON
-quiet false suppress scan diagnostics on standard error (moot under -format=json)

-loader=auto is why the same source builds for both worlds: WebAssembly has no process model, so go list can never run there and the choice makes itself.

Writing for a program: -format=json

The default shape — document on stdout, diagnostics as prose on stderr — is what a person or a shell pipeline wants. It is not enough for a program. Prose carries a position only in the sense that one is printed in it, and provenance, which says which Go construct produced this spec node, has nowhere to go at all.

-format=json writes one object instead:

{
  "spec": { "swagger": "2.0", "definitions": { "doc": {} } },
  "diagnostics": [
    {"severity": "hint", "code": "validate.dropped-ref-sibling",
     "message": "field \"for\": description dropped …",
     "file": "models/m.go", "line": 12, "col": 5}
  ],
  "provenance": [
    {"pointer": "/definitions/doc", "file": "models/m.go", "line": 8, "col": 6},
    {"pointer": "/definitions/doc/properties/at", "file": "models/m.go", "line": 10, "col": 2}
  ],
  "runtime": {
    "sys": 390070272, "heapAlloc": 369098752,
    "totalAlloc": 372244480, "collections": 2
  }
}

runtime is the Go runtime's own account of the scan, read just after it finished and without forcing a collection first — collecting would report a tidier heap than the one the scan actually ran with. It is carried because it cannot be recovered afterwards: once the process has exited, nobody can ask. Everything else here can be. Definition counts come from the document and elapsed time is the caller's own clock, which is why neither is included.

sys is memory obtained from the host; under wasip1 the linear memory never shrinks, so it is a high-water mark rather than a reading at one moment. heapAlloc against totalAlloc says which kind of heavy a scan is: close together means it is holding memory, far apart with many collections means it is churning through it.

Paths are relative to -workdir. The scan reports where it read a file from, which under a WASI guest is whichever mount point the host chose. A caller holds models/pet.go and should not have to know that the guest called it /src/models/pet.go. A position outside the module — a dependency in the module cache, a type in GOROOT — stays absolute rather than becoming a chain of .., and that is how a consumer tells the two apart.

Provenance covers anchors only: type declarations, fields, values, and route or meta blocks. A finer node resolves to its nearest anchored ancestor at the consumer, which is the contract genspec-tui's cross-references already work to. Anchors are sorted by pointer, so the same scan produces the same bytes.

Diagnostics keep the order they were reported in, and do not also go to stderr: under -format=json stderr stays clean, which is what makes the envelope safe to read from a pipe.

Errors are sentinels, so a caller can ask which refusal it met rather than matching on prose: errBadFlag for a flag value the command does not accept, errBadExportData for a -export-data path that is neither a directory nor a .zip.

Experimental, inheriting the status of Options.OnProvenance.

Running it under a WASI runtime

Build the artifact, then hand it to a runtime along with the directories it is allowed to read:

GOOS=wasip1 GOARCH=wasm go build -o genspec-wasi.wasm ./cmd/genspec-wasi

Verified against wasmtime 41 and wazero 1.11. Their mount syntax differs, which is the first thing to get wrong:

# wasmtime — <host>::<guest>
wasmtime run --dir "$PWD::$PWD" genspec-wasi.wasm -workdir "$PWD" ./...

# wazero — <host>:<guest>, no separator
wazero run -mount="$PWD:$PWD" genspec-wasi.wasm -workdir "$PWD" ./...

Two things a guest cannot work out for itself:

  • -goos / -goarch must be passed explicitly. Left alone they default to the platform the scanner is running on, which inside a guest is wasip1. That silently drops every _linux.go file and produces a different specification than the same scan run natively. Pass the platform of the code under scan.
  • GOROOT and the module cache are found by path. Nothing in a WASI environment can ask the go command where they live, so if the scan needs them they have to be mounted and named through the environment (--env GOROOT=…, --env GOMODCACHE=…).

wazero is a pure-Go runtime and convenient to embed in tests; wasmtime is several times faster on this workload. Both produce identical output.

How much of the host to expose

The real choice is how much of the host the guest may see. Measured on the petstore fixture under wasmtime:

mounted mode time peak RSS result
GOROOT + module cache default 7.3 s 681 MB identical to a go list scan
module cache -export-data 1.0 s 138 MB identical
module cache -stub-stdlib 1.0 s 147 MB degraded — see below
project tree only -stub-stdlib 0.1 s 123 MB degraded

Memory is usually the binding constraint rather than time: 681 MB for a fixture this small is more than a browser tab can host.

Precomputed dependency types

-export-data reads a scan's dependencies from the export data the compiler already produced, instead of parsing and type-checking them. That is where nearly all the time goes, so it costs a fraction — with no loss of fidelity, because the types are the compiler's own. The module being scanned is always read from source: its comments are the annotations.

It takes a directory or a .zip, so a host with somewhere to put a file but no tree to build hands over one blob.

go run ./hack/genexportdata -out /tmp/exportdata std

wasmtime run --dir "$PWD::$PWD" --dir /tmp/exportdata::/tmp/exportdata \
  genspec-wasi.wasm -export-data /tmp/exportdata -workdir "$PWD" ./...

The data is valid only for the toolchain that generated it, since the export format is tied to the Go release. Regenerate it when the toolchain moves.

A package whose meaning lives in comments cannot go in. strfmt declares its formats with swagger:strfmt, and export data holds types, not comments — such a package comes back structurally intact and semantically empty, with nothing erroring. genexportdata detects and skips them, saying which; they have to be read from source.

A build that needs nothing mounted but the project

The exportdata tag embeds that same data in the binary, so the artifact is self-contained:

go run ./hack/genexportdata -out internal/exportdata/exportdata.zip std
GOOS=wasip1 GOARCH=wasm go build -tags exportdata -o genspec-wasi.wasm ./cmd/genspec-wasi

# no GOROOT, no toolchain, nothing but the sources being scanned
wasmtime run --dir "$PWD::$PWD" genspec-wasi.wasm -workdir "$PWD" ./...

That costs about 5 MB of artifact (20 MB, 8.5 MB compressed, against 15 MB) and runs the petstore in 1.1 s. The archive is generated rather than committed.

Synthesizing the standard library instead

-stub-stdlib fabricates standard-library types from the names the scanned code selects through them. It needs no GOROOT and no module cache at all, and it is the smallest footprint on offer — but it is not failsafe, and its failure mode is quiet: the specification comes out slightly thinner rather than erroring.

Recognition by type identity survives, so time.Time is still a date-time. Structure does not: a synthesized type has no fields and no method set, so json.RawMessage stops rendering as a byte array, time.Duration as an integer, and a type is no longer seen to implement encoding.TextMarshaler. Across codescan's fixture corpus 138 of 143 scans stay byte-identical.

Prefer a full graph, or the export data above, wherever GOROOT is available.

Whatever the mode, every import that had to be synthesized raises a scan.synthesized-import diagnostic on standard error naming the import and where it came from, so the loss is never silent. Drop -quiet to see them.

Tests

The integration tests build the artifact and run it under whichever runtime is on PATH, comparing the result against an in-process scan:

go test ./internal/integration/ -run TestWASIArtifact -v

They skip when no runtime is installed, when there is no go command to build with, and under -short. The self-contained case additionally skips unless internal/exportdata/exportdata.zip has been generated.

Documentation

Overview

Command genspec-wasi scans annotated Go source and writes the Swagger specification it describes.

It is a headless counterpart to genspec-tui, and the form codescan takes when it is built for WebAssembly: it carries no dependency beyond the library itself, so it cross-compiles to wasip1/wasm and runs under any WASI runtime.

go build -o genspec-wasi ./cmd/genspec-wasi
GOOS=wasip1 GOARCH=wasm go build -o genspec-wasi.wasm ./cmd/genspec-wasi
wazero run -mount /path/to/project:/src genspec-wasi.wasm -workdir /src ./...

Under a WASI runtime the scan reads only what the host mounted, and runs no subprocess: use -loader=own, which needs neither the go command nor a toolchain. See -h for the full flag set.

Mounting, and what it costs

A full scan resolves the standard library and the module cache by path, so both have to be mounted and named through GOROOT and GOMODCACHE. -stub-stdlib removes the first requirement, and an unresolvable import is synthesized rather than fatal, so a scan will complete with nothing but the project tree mounted. Measured on codescan's petstore fixture under wasmtime:

everything mounted, full graph      8.1 s   848 MB   byte-identical
module cache only, -stub-stdlib     1.0 s   143 MB   byte-identical
project tree only, -stub-stdlib     0.1 s   123 MB   one format lost

The last row is the shape of the trade: it does not fail, it quietly emits slightly less. Prefer a full graph wherever GOROOT is available.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL