Builtin Targets

Compile targets permanently into the gomake binary at install time.

Overview

targets.yaml lets you compile target packages permanently into the gomake binary. These targets become builtin — they are available in every project, even those without a makefile.go, and they appear alongside any user-defined targets when you run --list.

This is different from //gomake:import, which declares per-project imports inside a project’s own makefile.go:

MechanismWhere declaredCompiled whenAvailable in
targets.yamlgomake repo rootcmd/install runsEvery project, always
//gomake:importproject’s makefile.goproject makefile buildsThat project only

The targets.yaml file

targets.yaml lives at the root of the gomake source repository and is committed to version control. An empty config is valid:

imports:

Each entry in imports has a required import field and two optional fields:

imports:
  - import: example.com/myorg/gomake-targets/docker
  - import: example.com/myorg/gomake-targets/release
    namespace: release
  - import: example.com/myorg/gomake-targets/db
    namespace: db
    config:
      host: db.internal
      port: 5432
FieldRequiredDescription
importYesGo import path of the target package
namespaceNoCLI prefix for all targets from this package, lowercased (e.g. release)
configNoArbitrary YAML surfaced as ring.Ring metadata (see below)

The import path may pin a version, e.g. example.com/myorg/gomake-targets/release@v1.3.0. Duplicate import paths are rejected with an error, and the file is parsed strictly — an unknown field fails the load rather than being ignored.


How it works

Passing --targets to go run github.com/ctx42/gomake/cmd/install@latest runs the target-preparation step, which:

  1. Reads each supplied targets.yaml and, when --targets is repeated, combines their imports (see Combining several files).
  2. Runs go get <import-path> for each listed package. The exception is a local --targets file inside a Go module (see Developing targets locally): each such module is resolved from disk through a temporary Go workspace, so go get is skipped for its packages.
  3. Calls builtin.GenMain, which parses each package for target functions and generates internal/builtin/targets.go.
  4. Compiles the generated file into the gomake binary.

After installation, the new targets are part of the binary — no makefile.go in a project is needed to make them available.

During installation, a line is printed to stderr for each import:

adding external target example.com/myorg/gomake-targets/docker
adding external target example.com/myorg/gomake-targets/release

Nothing is printed when imports is empty. Passing --targets= with an empty value — for example a shell variable that expanded to nothing — instead prints no external targets provided and continues the install with no external targets, so the empty expansion is not mistaken for an omitted flag.


Adding an external target

  1. Edit your targets.yaml and add the entry.
  2. Reinstall gomake with the updated file:
go run github.com/ctx42/gomake/cmd/install@latest --targets=./targets.yaml

Or fetch the file from a URL:

go run github.com/ctx42/gomake/cmd/install@latest --targets=https://example.com/targets.yaml

To regenerate internal/builtin/targets.go without a full reinstall:

go generate ./internal/builtin/

Combining several files

Repeat --targets to compile the imports of several files into one binary — for example a public and a private set of targets kept in separate repositories. Each value is a local path or a URL, and the forms can be mixed:

go run github.com/ctx42/gomake/cmd/install@latest \
    --targets=../gmtask/targets.yaml \
    --targets=../gmtool/targets.yaml

The install script forwards repeated flags the same way:

curl -fsSL https://raw.githubusercontent.com/ctx42/gomake/master/install.sh | sh -s -- --targets=a/targets.yaml --targets=b/targets.yaml

The files are read in the order given and their imports combined:

  • An import listed identically in more than one file — same path, version, namespace, and config — is compiled in once.
  • The same package listed with a different version, namespace, or config fails the install with a conflicting import error naming the package and both files.

Every file must exist; a missing one fails the install. An empty value such as --targets= is dropped, so it does not disturb the other files.


Developing targets locally

While developing a target package you can build a gomake binary from its unpublished source, without pushing to a module proxy. Point --targets at a local targets.yaml that lives inside the target’s Go module:

GOBIN=$PWD/dist go run ./cmd/install --targets=/path/to/module/targets.yaml

When the --targets file sits inside a Go module, cmd/install resolves that module from disk through a temporary Go workspace instead of fetching it with go get. Your local, uncommitted edits are compiled straight into the binary, and go.mod is left untouched — no replace or require is added. Re-run the command after each edit to rebuild.

An install from a clone builds from a temporary copy of the files the build reads, so the targets.yaml and internal/builtin glue it regenerates never touches your working tree, and the binary reports the clone’s version without +dirty. Outside a repository the installer regenerates that glue in place and restores the prior contents on a normal exit (success or error). A hard interrupt (for example SIGINT / SIGKILL) can skip that restore and leave targets.yaml, internal/builtin/targets.go, and internal/builtin/data/targets_main.go_ modified.

With --targets repeated, every local file inside a Go module adds its module to the same workspace, so unpublished edits in each of them compile in. Only the modules containing those targets.yaml files are resolved locally; imports from any other module still come from the proxy via go get.


Namespaces

The optional namespace field places all targets from a package under a CLI prefix. Without it, targets merge into the root namespace.

imports:
  - import: example.com/myorg/targets/infra
    namespace: infra
$ gomake --list
infra:plan    plans infrastructure changes
infra:apply   applies infrastructure changes

The config field

The optional config field carries arbitrary YAML that a target can read as ring.Ring metadata. The metadata key is the namespace value, or — when namespace is absent — the last path segment of the import path (version suffixes like @v2 and /v2 are stripped).

imports:
  - import: example.com/myorg/targets/db
    namespace: db
    config:
      host: db.internal
      port: 5432

There is an important detail about where this config is read. Only the target code is compiled into the binary — the config values are not. At runtime GoMake loads config from a targets.yaml in the project’s source directory (the --src directory, which defaults to the current directory). So to feed configuration to a built-in target in a given project, place a targets.yaml carrying the config block in that project. The config from the gomake repo’s install-time targets.yaml is not embedded in the binary.

A target reads its config via the Ring; each entry’s config arrives as a JSON string under its metadata key:

func Migrate(ctx context.Context, rng *ring.Ring) error {
    raw := rng.MetaGet("db") // key == namespace == "db"
    cfg, ok := raw.(string)  // config is passed as a JSON string
    if !ok {
        return errors.New("db: missing config")
    }
    // unmarshal cfg as needed
    return nil
}

Writing a package for targets.yaml

A target package for targets.yaml uses the same function signatures as any other target, but lives in a regular (non-main) package:

// Package docker provides Docker build and push targets.
package docker

import (
    "context"
    "os/exec"

    "github.com/ctx42/ring/pkg/ring"
)

// Build builds the Docker image.
func Build(ctx context.Context, rng *ring.Ring) error {
    cmd := exec.CommandContext(ctx, "docker", "build", ".")
    cmd.Stdout = rng.Stdout()
    cmd.Stderr = rng.Stderr()
    return cmd.Run()
}

// Push pushes the most recently built image to the registry.
func Push(ctx context.Context, rng *ring.Ring) error {
    cmd := exec.CommandContext(ctx, "docker", "push", "myorg/myimage")
    cmd.Stdout = rng.Stdout()
    cmd.Stderr = rng.Stderr()
    return cmd.Run()
}

Publish the package as a normal Go module, then reference its import path in targets.yaml.


Comparison: targets.yaml vs //gomake:import

Use targets.yaml when:

  • The targets should be available in every project without any makefile.go.
  • You are maintaining a shared organisation-wide toolset and want it compiled into the standard gomake binary.
  • The targets require no per-project configuration (or configuration is supplied via the config field).

Use //gomake:import when:

  • The targets are specific to one project.
  • Different projects import different versions of the package.
  • You want the import to be visible in the project’s own makefile.go.