Importing Targets

Share targets across projects using the //gomake:import comment.

Overview

The //gomake:import comment tag lets you pull target functions from any Go package into your makefile. This is the primary mechanism for sharing build logic across repositories.


Syntax

import _ "github.com/myorg/build-targets/docker"   //gomake:import
import _ "github.com/myorg/build-targets/release"  //gomake:import deploy
  • Without a namespace argument: targets are merged into the root namespace.
  • With a namespace argument (deploy): targets are prefixed with deploy:.

The import uses the blank identifier (_) for the ordinary Go reason: the package is never referenced in your code, so a named import would fail to compile as “imported and not used”. GoMake reads the target functions from the package during parsing.


Example

//go:build gomake

package main

import (
    "context"
    "os/exec"

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

    _ "github.com/myorg/gomake-targets/docker"   //gomake:import
    _ "github.com/myorg/gomake-targets/infra"    //gomake:import infra
)

// Build compiles the project.
func Build(ctx context.Context, rng *ring.Ring) error {
    cmd := exec.CommandContext(ctx, "go", "build", "./...")
    cmd.Stdout = rng.Stdout()
    cmd.Stderr = rng.Stderr()
    return cmd.Run()
}
$ gomake --list
build           compiles the project
docker:build    builds the Docker image        # from docker package
docker:push     pushes the image               # from docker package
infra:plan      plans infrastructure changes   # from infra package (namespaced)
infra:apply     applies infrastructure changes # from infra package (namespaced)

Writing a shareable target package

A shareable package is a regular Go package with functions matching the target signature. It does not need package main:

// Package docker provides reusable Docker build targets.
package docker

import (
    "context"
    "os/exec"

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

// Build builds the Docker image for the current project.
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()
}

Place the package in its own module and go get it from the project that uses it.


Import resolution

GoMake resolves //gomake:import packages by running go list in the project directory. This means the imported package must be available in the project’s module graph (i.e. present in go.mod / go.sum).

Multiple imports are resolved concurrently. An error in any import aborts the build. Packages inside the current module are resolved in-process without go list, and results for external, versioned modules are cached on disk, so repeat runs stay fast.


Namespaced imports

The second word after //gomake:import becomes the namespace prefix. It is converted to lowercase regardless of how it is written in the source:

import _ "github.com/myorg/targets/db"  //gomake:import DB
$ gomake --list
db:migrate   ...
db:rollback  ...

What counts as a valid import comment

CommentValid?Namespace
//gomake:importYesroot
//gomake:import nsYesns
//gomake:import NSYesns (lowercased)
// gomake:importYesroot — a space after // is tolerated
//gomake:No — missing import keyword
//notgomake:importNo — wrong prefix
//gomake:import ns extraNo — too many words

Unlike the // gomake:hidden tag, //gomake:import is read straight from the source comment, so a space after // makes no difference here.