Dagger
Search

cpybkc

image as a step in somebody else's pipeline, for a caller who would rather not
write a Dockerfile.

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
with-generator --name hello --image ghcr.io/example/cpybkc-gen-hello:v1 \
generate --source . export --path .

Nothing is installed on the host and no Dockerfile is written: the published
images are pulled, composed, and run.

# This is a convenience, not a contract

The contract is the published image. docs/container/SPEC.md says what the
entrypoint is, which directory generators are discovered in, which UID the
process runs as and where the IR schema lives, and it is that document a third
party builds against. Everything here is those promises spelled as Dagger
calls, and every one of them can be written by hand as
`docker run --rm ghcr.io/zaba505/cpybkc:v0` instead.

So this module gets no SPEC.md, deliberately (docs/CONVENTIONS.md, "What
belongs here"). A specification for it would imply the contract is a property
of the module — that a caller reaching for `docker run`, a Kubernetes Job or a
Dockerfile were on a lesser path — when the module is the one thing in this
repository that could be deleted without breaking a single promise cpybkc
makes. What it needs to say, it says in this comment and in
`dagger call --help`.

It states nothing about the plugin CLI contract either. `cpybkc-gen-`,
the argument vector and the exit codes are docs/plugin/SPEC.md's, they hold
with no container anywhere in the picture, and the only thing this module will
ever know about them is the filename a generator is installed under.

# The module ref is permanent public API

This module is published as `github.com/Zaba505/cpybkc/daggerverse/cpybkc`,
and a Dagger module ref is a directory path inside a tag of this repository.
Renaming the directory would not deprecate the old ref, it would delete it:
every caller pinning `…/daggerverse/cpybkc@v0.3.1` resolves that path inside
the tag they named, so a rename breaks every unpinned caller at once and
strands every pinned one on a path that will never move again. There is no
redirect to leave behind and no deprecation period to serve.

The name is therefore chosen once and kept. `daggerverse/` is the
house layout — it is where `github.com/z5labs/devex` publishes the two modules
this repository's pipeline already depends on — and the directory is named for
what the module drives rather than for what it does, so that a second module
shipping from here later needs no rename of this one to sit beside it.

The `.dagger/` module at the repository root is a different thing entirely: it
runs this repository's own pipeline, it is published for nobody, and it knows
about a checkout of cpybkc that this module never sees. What holds the two to
one Dagger `engineVersion` is the local dependency edge the root `dagger.json`
declares on this directory — an engine bump is then one commit touching both
files, rather than two files nothing requires to agree.

# Composing a generator, and the Dockerfile it replaces

The base image carries the CLI and no generator at all, so an image that
generates anything is a composition. WithGenerator is that composition:

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
with-generator --name hello --image ghcr.io/example/cpybkc-gen-hello:v1 \
generate --source . export --path .

and the two lines it stands for are the final stage of
docs/container/SPEC.md's worked example, written by hand:

FROM ghcr.io/zaba505/cpybkc:v0
COPY --from=ghcr.io/example/cpybkc-gen-hello:v1 --chown=65532:65532 --chmod=0755 \
/usr/local/bin/cpybkc-gen-hello /usr/local/bin/cpybkc-gen-hello

They are the same two instructions and the comparison is the point: the
mechanism is `COPY --from`, this is not a second way of extending cpybkc, and
a caller who prefers the Dockerfile is not on a lesser path. What the module
saves is the build context, the registry to push the derived image to, and a
Dockerfile to keep in step with a cpybkc release — a project whose manifest
names three generators is three calls and one image that is never pushed
anywhere.

Two differences from that COPY line are deliberate rather than incidental.
The module sets no owner where the Dockerfile writes `--chown=65532:65532`:
the mode is what makes the file runnable, by the image's own UID and by any
UID a caller overrides it with, while the owner is a property of the image
this module was given rather than one it may assume. The Dockerfile can name
65532 because it also names the base image it is deriving from; a module
handed a container through --image knows no such thing. And --image may be
omitted, in which case the generator image is the one this project publishes
beside the CLI — `-gen-:`, resolved against the same
--repository and --version the CLI image came from, so a generator from one
release never lands beside a CLI from another.

WithGeneratorExecutable is the other half, and it takes a File rather than an
image: a generator that has not been published yet, most often one the caller
has just built in the same pipeline. A generator author needs it to check
their plugin against a real cpybkc run before there is anything of theirs to
pull.

# Multi-platform derived builds

A composed image is one platform, because a dagger.Container is. What this
module guarantees is that it is *one* platform: WithGenerator reads the
platform off the container it is composing into and pulls the generator image
for that platform, so an arm64 base never quietly acquires an amd64 generator
that fails at exec with the kernel's message rather than cpybkc's.

--platform on the constructor is what makes the other platform reachable at
all. Without it the base image is pulled for the engine's own platform, and an
amd64 engine could only ever compose an amd64 image. With it, a derived
multi-platform index is one composition per platform, published as variants:

vari

Installation

dagger install github.com/Zaba505/cpybkc/daggerverse/cpybkc@1d785b7223c913bd2b3ae0acd60e979995f7b0a9

Entrypoint

Return Type
Cpybkc !
Arguments
NameTypeDefault ValueDescription
versionString "v0"The tag of the published cpybkc image to run. It defaults to the moving major tag "v0", which follows every release in that major version: a caller who passes nothing keeps up with fixes and stays inside the base-image contract's compatibility guarantees. Escalate deliberately — "v0" follows releases, "v0.3.1" pins one release, and only a digest pins the bytes. A digest is not a tag, so it goes to --image rather than here.
repositoryString "ghcr.io/zaba505/cpybkc"The registry repository the image is pulled from, as `<host>/<path>` with no tag. It is an argument because where the image lives is not something the base-image contract promises — a mirror, an internal registry or an air-gapped copy serving the same digests satisfies every requirement in that document identically, and a caller behind one should not have to give up this module to use it. Companion images derive from it by the rule this project publishes them under, so redirecting this redirects the family rather than the base image alone.
imageContainer -Run in this container instead of pulling one, replacing it entirely. It has to keep the promises the base-image contract makes — cpybkc as the entrypoint, the plugin directory on PATH, a user that can execute what is in it — because that is all this module drives it through. Nothing here checks that, and nothing could: a container is not required to have come from a registry at all. This is how cpybkc's own pipeline checks the module against the image it just built rather than against the last release (#64), how a caller tries a change to cpybkc before it ships, and how a build pins the image by digest — a reference no tag argument can express, and the only one that pins bytes. It pins this image and nothing else. A generator pulled afterwards by with-generator follows --version, which defaults to the moving "v0" tag and does not track whatever was pinned here, so a build that pins the CLI by digest and wants the pairing to hold still passes --version beside it.
platformString -Pull the image for this platform, as `GOOS/GOARCH`, instead of for the engine's own. It is what makes a derived image for another architecture reachable at all: a composition is one platform, and without this an amd64 engine could only ever compose an amd64 image. Every generator composed in afterwards follows it, so the platform is stated once, here, rather than on each call that could contradict the last. A multi-platform derived image is therefore one composition per platform, published as variants of one index — see this module's comment. That loop belongs to the caller because the index does: which platforms a derived image serves is a property of who will run it. Empty is the engine's own platform, which is what makes an ordinary `dagger call` from a checkout do the obvious thing.
Example
dagger -m github.com/Zaba505/cpybkc/daggerverse/cpybkc@1d785b7223c913bd2b3ae0acd60e979995f7b0a9 call \
func (m *MyModule) Example() *dagger.Cpybkc  {
	return dag.
			Cpybkc()
}
@function
def example() -> dagger.Cpybkc:
	return (
		dag.cpybkc()
	)
@func()
example(): Cpybkc {
	return dag
		.cpybkc()
}

Types

Cpybkc 🔗

Cpybkc is one cpybkc image, plus the coordinates for resolving images related to it.

A function on it is a builder returning a new Cpybkc, a terminal that runs something, or an accessor handing back what was resolved, so a call chain reads as the image being assembled and then used; nothing here mutates.

generate() 🔗

Generate runs cpybkc over a project and hands back the project as it should now be committed:

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  generate --source . export --path .

Nothing is written to the host. The Directory that comes back is a value like any other, and exporting it is the caller’s separate, explicit step — which is also what disposes of the ownership problem a bind mount has, where generated files land owned by whichever UID the container ran as and a host user who is not that UID owns none of their own project. Dagger’s export writes as the person running it, so the image’s pinned user never reaches the host.

What comes back is the whole project directory, not only the files a generator produced. A generator’s output lands where the manifest says, which is ordinarily inside the source tree, and a run also prunes what a previous run generated and no longer would — so the generated files alone cannot express half of what a run did. generate --source . export --path . is therefore the ordinary use, and it is a full statement of the run rather than an overlay that leaves deletions behind.

Return Type
Directory !
Arguments
NameTypeDefault ValueDescription
sourceDirectory !-

The project to generate over: the directory holding the manifest, the layout it names and the copybooks that layout names.

It is mounted whole rather than filtered down to what a run reads, because which copybooks a run reads is a property of the layout — the manifest carries no input list — and a module guessing at that set would be a second, weaker answer to a question docs/cli/SPEC.md answers exactly.

manifestString -

The project manifest to read, relative to the root of source.

It defaults to nothing, which leaves cpybkc reading cpybkc.json at the root of the mounted project — the CLI’s own default, applied by the CLI, against the working directory this module puts the project at. There is no upward search and no second manifest, so a project keeping its manifest somewhere else says so here.

A relative path resolves against that project root, because that is the directory the CLI resolves a path typed on the command line against. It cannot be “-”: a manifest’s own paths are relative to the directory holding it, and a manifest arriving on a stream is in no directory.

Example
dagger -m github.com/Zaba505/cpybkc/daggerverse/cpybkc@1d785b7223c913bd2b3ae0acd60e979995f7b0a9 call \
 generate --source DIR_PATH
func (m *MyModule) Example(source *dagger.Directory) *dagger.Directory  {
	return dag.
			Cpybkc().
			Generate(source)
}
@function
def example(source: dagger.Directory) -> dagger.Directory:
	return (
		dag.cpybkc()
		.generate(source)
	)
@func()
example(source: Directory): Directory {
	return dag
		.cpybkc()
		.generate(source)
}

image() 🔗

Image is the image this module resolved and composed, for a caller who wants to do something with it other than what this module offers — run a cpybkc subcommand by hand, look at what is in it, publish a derived image holding their generators, or make it one variant of a multi-platform index:

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  image with-exec --use-entrypoint --args=--version stdout

Note the –use-entrypoint. The image’s entrypoint is the CLI and the literal value of it is not part of the contract (docs/container/SPEC.md, “The CLI’s own path is not part of the contract”), so an exec that named a path instead would be reaching for the one thing that document reserves the right to move.

Publishing the image elsewhere is a reasonable thing to do — it is the same image the registry served — but a copy is not a release of cpybkc and carries none of the signatures or attestations one does (docs/container/SPEC.md, “What a tag carries besides the image”), because those are attached to the digest this project published and not to the bytes.

Return Type
Container !
Example
dagger -m github.com/Zaba505/cpybkc/daggerverse/cpybkc@1d785b7223c913bd2b3ae0acd60e979995f7b0a9 call \
 image
func (m *MyModule) Example() *dagger.Container  {
	return dag.
			Cpybkc().
			Image()
}
@function
def example() -> dagger.Container:
	return (
		dag.cpybkc()
		.image()
	)
@func()
example(): Container {
	return dag
		.cpybkc()
		.image()
}

init() 🔗

Init scaffolds a layout from copybooks and hands back the file to edit:

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  init --source . --copybook posting.cpy export --path ledger.sexpr

It is the first thing an adopter runs, before there is a manifest, a layout or a generator to speak of, and it is curated for that reason: the one invocation somebody has nothing to copy from is a poor place to make them assemble an argument vector by hand.

What comes back is not a valid layout and is not meant to be. init writes what the copybooks decide — a record per 01-level, an alternative per REDEFINES — and leaves the half no copybook holds as commented forms for a person to answer: which field discriminates, and in what order the records come. docs/cli/SPEC.md’s init section is what that file says and what it deliberately does not.

Nothing is written to the host, exactly as with Generate: the File that comes back is a value, and exporting it is the caller’s separate step. What it is called is theirs — this module writes the scaffold to a path of its own inside the container ([scaffoldPath]) that nothing was mounted into, so the run cannot land on something the caller already has, and the name it takes in their tree is the one they give export.

Where it goes is theirs within one constraint, and it is the one thing here a caller cannot infer from the signature. The scaffold names each copybook by the path it was given, which is relative to the root of source, and a layout’s own paths are relative to the layout — so the file belongs at that root, beside the copybooks it names. Exported into a subdirectory it is a layout whose copybook paths resolve nowhere, and nothing at export time will say so. A project that keeps its layout somewhere else moves the paths as it moves the file; they are the adopter’s to edit, like the rest of what init leaves blank.

The destination this module supplies is a file rather than a stream, so --out - is not offered here. A caller who wants the scaffold on standard output still has Run, which is the same arrangement –emit-ir has:

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  run --source . --args=init,--copybook,posting.cpy,--out,- stdout
Return Type
File !
Arguments
NameTypeDefault ValueDescription
sourceDirectory !-

The project the copybooks are in: the directory their paths are relative to, mounted at /src and made the working directory.

It is required, unlike Run’s, because a scaffolding run reads files by definition. The whole directory is mounted rather than the named copybooks alone, because that is what makes the paths below resolve as the adopter typed them: –source is already how every other function on this module takes the caller’s tree, and a mount holding only the named files would be a second, weaker answer to where a relative path points.

copybook[String ! ] !-

A copybook to read, as a path relative to the root of source. Repeat it once per copybook, in the order the scaffold should hold their records in.

Paths rather than files, because the path is data: docs/cli/SPEC.md has the scaffold record each copybook’s path as it was typed, and a layout’s own paths are relative to the layout. A file handed over on its own would have cpybkc write a container path the adopter cannot find anywhere in their tree.

At least one is required, and none may be “-”: a copybook on a stream has no path for the scaffold to state. Everything else is the CLI’s to refuse — a value naming a directory is a run that failed rather than a line that was wrong, and it says so with a diagnostic naming the path.

One boundary belongs to the command line rather than to cpybkc: dagger call renders a list argument as --copybook a.cpy --copybook b.cpy or as one comma-separated --copybook a.cpy,b.cpy, so a copybook whose path contains a comma cannot be spelled here. That one is Run’s, where the vector is passed through as written.

Example
dagger -m github.com/Zaba505/cpybkc/daggerverse/cpybkc@1d785b7223c913bd2b3ae0acd60e979995f7b0a9 call \
 init --source DIR_PATH --copybook string1 --copybook string2
func (m *MyModule) Example(source *dagger.Directory, copybook []string) *dagger.File  {
	return dag.
			Cpybkc().
			Init(source, copybook)
}
@function
def example(source: dagger.Directory, copybook: List[str]) -> dagger.File:
	return (
		dag.cpybkc()
		.init(source, copybook)
	)
@func()
example(source: Directory, copybook: string[]): File {
	return dag
		.cpybkc()
		.init(source, copybook)
}

run() 🔗

Run is the escape hatch: cpybkc invoked with an argument vector this module has no opinion about, in a container handed back whole.

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  run --source . --args=--emit-ir,- stdout

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  run --args=--help stdout

It exists so that Generate does not have to grow an argument every time docs/cli/SPEC.md’s flag table does. A flag that replaces the action rather than configuring it (–emit-ir is terminal: no generator runs and nothing is merged or pruned), one that may only appear beside another (–emit-ir-format without –emit-ir is a usage error), and one that answers a question about the program rather than about a project (–version, –help) are all things a command line states plainly and a set of Dagger arguments states badly.

A container rather than a directory, because the uncurated invocations are the ones whose answer is not a tree. –emit-ir may write to standard output, and –version and –help write nothing else at all; a Directory return would make the escape hatch unable to reach exactly the flags it exists for. A caller who does want the tree takes it from the container, where the project is still mounted at /src:

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  run --source . --args=--manifest,build/cpybkc.json \
  directory --path=/src export --path .

Nothing here is validated. A vector this module checked would be a second, unversioned reading of a contract the CLI already implements, and its diagnostics are better than anything guessed at from out here: what an unrecognised flag is, whether a flag may repeat and what a usage error exits with are all docs/cli/SPEC.md’s, and the exec’s failure carries them back verbatim.

Return Type
Container !
Arguments
NameTypeDefault ValueDescription
args[String ! ] !-

The argument vector, passed to the CLI exactly as written. The entrypoint is the CLI itself, so this is everything after the command name and it never names the command.

sourceDirectory -

The project to run over, mounted at /src and made the working directory.

It is optional because half the reason this function exists is the invocations that have no project — --version and --help read nothing and contact nothing. With no source the container is the image as it was resolved, and cpybkc runs in whatever directory the image left it in.

Example
dagger -m github.com/Zaba505/cpybkc/daggerverse/cpybkc@1d785b7223c913bd2b3ae0acd60e979995f7b0a9 call \
 run --args string1 --args string2
func (m *MyModule) Example(args []string) *dagger.Container  {
	return dag.
			Cpybkc().
			Run(args)
}
@function
def example(args: List[str]) -> dagger.Container:
	return (
		dag.cpybkc()
		.run(args)
	)
@func()
example(args: string[]): Container {
	return dag
		.cpybkc()
		.run(args)
}

withGenerator() 🔗

WithGenerator adds one generator to the image by copying its executable out of a generator image:

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  with-generator --name hello --image ghcr.io/example/cpybkc-gen-hello:v1 \
  generate --source . export --path .

This is the whole of adding a generator without writing a Dockerfile. The file is taken out of the image at the path the base-image contract promises, which is what COPY --from does, and it works just as well for a generator image this project has never heard of — that is what image is for. Repeated calls compose, so a project whose manifest names three generators is three calls and one image.

With no image, the generator this project publishes for name is pulled: <repository>-gen-<name>:<version>, against the same coordinates the CLI image came from. A generator from one release beside a CLI from another is a pairing nobody tested, and defaulting to it is how somebody would end up in one without having said so.

The generator is pulled for the platform the container being composed into was resolved for, not for the engine’s. That is the whole of what this module does about multi-platform: composing an amd64 generator into an arm64 image produces an image that builds and pushes and then fails at exec with the kernel’s message rather than cpybkc’s, which is a long way from the call that caused it.

Return Type
Cpybkc !
Arguments
NameTypeDefault ValueDescription
nameString !-

The generator to add, by the <name> cpybkc.json asks for it by. Discovery is by filename, so this is the name in cpybkc-gen- and nothing else.

imageContainer -

Take the executable from this image instead of pulling the published generator image for name.

Any image carrying the generator in the plugin directory will do, including one that was never published. It has to be for the same platform as the image being composed into, which is checked, because a mismatch here is checkable and its exec-time failure is not legible.

Example
dagger -m github.com/Zaba505/cpybkc/daggerverse/cpybkc@1d785b7223c913bd2b3ae0acd60e979995f7b0a9 call \
 with-generator --name string
func (m *MyModule) Example(name string) *dagger.Cpybkc  {
	return dag.
			Cpybkc().
			Withgenerator(name)
}
@function
def example(name: str) -> dagger.Cpybkc:
	return (
		dag.cpybkc()
		.withgenerator(name)
	)
@func()
example(name: string): Cpybkc {
	return dag
		.cpybkc()
		.withGenerator(name)
}

withGeneratorExecutable() 🔗

WithGeneratorExecutable adds one generator to the image from an executable file, for a generator that ships no image — most often one the caller has just built in the same pipeline:

dagger call -m github.com/Zaba505/cpybkc/daggerverse/cpybkc \
  with-generator-executable --name hello --executable ./cpybkc-gen-hello \
  generate --source . export --path .

It is the generator author’s path, before there is anything of theirs to pull. Checking a plugin against a real cpybkc run is the first thing they need and the last thing a published image can give them.

The file lands as cpybkc-gen- whatever it was called before, because discovery is by filename and nothing inside the executable is consulted.

It has to be a statically linked native executable for the image’s platform, and that is the caller’s to meet rather than this module’s to check: it is the same requirement docs/container/SPEC.md’s worked example states as CGO_ENABLED=0, a File says nothing about what it is, and a dynamically linked or foreign-architecture generator fails at exec time with the kernel’s message rather than cpybkc’s. WithGenerator can check the platform half of that because a container states one; here there is nothing to read.

Return Type
Cpybkc !
Arguments
NameTypeDefault ValueDescription
nameString !-

The generator’s <name>, as cpybkc.json asks for it.

executableFile !-

The generator executable.

Example
dagger -m github.com/Zaba505/cpybkc/daggerverse/cpybkc@1d785b7223c913bd2b3ae0acd60e979995f7b0a9 call \
 with-generator-executable --name string --executable file:path
func (m *MyModule) Example(name string, executable *dagger.File) *dagger.Cpybkc  {
	return dag.
			Cpybkc().
			Withgeneratorexecutable(name, executable)
}
@function
def example(name: str, executable: dagger.File) -> dagger.Cpybkc:
	return (
		dag.cpybkc()
		.withgeneratorexecutable(name, executable)
	)
@func()
example(name: string, executable: File): Cpybkc {
	return dag
		.cpybkc()
		.withGeneratorExecutable(name, executable)
}