hostlaunch

package
v1.0.6 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package hostlaunch composes exec.Cmds for host-mode services and frontends, plus the small env-file helpers both call sites need.

Two CLI surfaces target the same dispatch matrix:

  • `forge run <svc>` — single-service host runner (foreground or background; backed by a per-service PID file).
  • `forge env up` host phase — N-service loop that hangs the cmds off a process registry for cascade teardown.

Both pick a runner (go-run / air / binary / delve), default the env file to `.env.<env>`, and layer the env-file values onto the child process. Before this package existed, the dispatch was duplicated across `internal/cli/run.go` (buildRunHostCmd / runHostService) and `internal/cli/up.go` (buildHostServiceCmd / upHostServices) — same runner table, two implementations.

The package intentionally does NOT own the process lifecycle:

  • foreground stream-prefix + signal handling lives in the single- service `forge env up` path because it has different semantics (one process, persistent PID file, `stop` subcommand);
  • the N-process registry that `forge env up` uses for cascade teardown stays in `internal/cli/up.go` for the same reason.

What's shared here is the pure command-construction matrix plus the minimal env-file parser. Anything that tracks PIDs / streams output / handles signals stays at the call site.

Index

Constants

View Source
const (
	// DefaultDelvePort is the dlv --listen=:<port> default when KCL
	// doesn't pin one explicitly. Matches dlv's own conventional port.
	DefaultDelvePort = 2345

	// DefaultAirConfig is the `air -c <path>` default when KCL doesn't
	// pin one explicitly. Mirrors the `air` tool's own default —
	// `.air.toml` at the project root.
	DefaultAirConfig = ".air.toml"
)

Defaults pinned by the runner dispatch. Exported so tests and CLI help text can reference them without re-deriving the magic numbers.

View Source
const ShellWinsEnvVar = "FORGE_ENV_OVERRIDE"

ShellWinsEnvVar names the parent-env variable holding the comma-separated list of keys the developer's shell may still override, e.g.

FORGE_ENV_OVERRIDE=DATABASE_URL,LOG_LEVEL forge env up dev

It exists because the declared-wins default (see envutil.MergeDeclaredWins) removes the ad-hoc override the old base-wins default provided by accident. Keeping that affordance EXPLICIT is the point: an override you typed is a decision, an override you inherited is an accident.

Variables

View Source
var AirEnvDefaults = map[string]string{"ENVIRONMENT": "development"}

AirEnvDefaults are the environment defaults forge applies to a workload run under air, at the lowest precedence (see LayerHostEnvConflicts). The scaffolded .air.toml cannot carry them: air runs its commands through PowerShell on Windows, where a POSIX `VAR=val cmd` prefix does not parse, and air hands its own environment to the server it launches.

Functions

func BuildCmd

func BuildCmd(ctx context.Context, name string, spec RunnerSpec) (*exec.Cmd, error)

BuildCmd composes the *exec.Cmd for a host workload. The argv is DERIVED, never authored per runtime (ADR 0002 §2):

explicit Command (any runner but air)  Command... Args...
go-run / ""                            go run <GoPkg> Args...
binary                                 ./bin/<OutputName|name> Args...
delve                                  dlv exec --headless --listen=:<port> --api-version=2
                                         --accept-multiclient --continue ./bin/<out> [-- Args...]
air                                    air -c <AirConfig|.air.toml>   (Args: see IgnoresArgs)

go-run, binary and delve with no Go build and no Command are an ERROR, as is an unknown runner. There is no fallback: the historical default (`go run ./cmd server <name>`) launched a command no declaration said, and every "falls through to go-run" hid a typo behind a process that started and did the wrong thing.

func ExeName added in v0.1.45

func ExeName(goos, name string) string

ExeName is the on-disk name of a Go binary built for goos: Windows executables must end in .exe (CreateProcess, dlv and air all key off it), every other OS uses the bare name. It is the single spelling `forge build` writes and BuildCmd runs, so the two cannot drift.

func IsKnownRunner

func IsKnownRunner(runner string) bool

IsKnownRunner reports whether the runner name is one of the supported dispatch keys ("" means go-run).

func LayerHostEnv

func LayerHostEnv(base []string, projectConfig, secrets, envVars map[string]string) []string

LayerHostEnv composes the env for a host-mode subprocess.

final = base ⊕ projectConfig ⊕ secrets ⊕ envVars

Order (each later layer overrides earlier on key conflict among the three map layers — base os.Environ() always wins so a developer's shell override beats them all):

  1. base — the parent process env (typically os.Environ()). Wins last.
  2. projectConfig — forge.yaml `environments[<env>].config` projected to env-var strings. Same non-secret values cluster-mode services see via the ConfigMap projection, layered here so host-mode services don't drift from cluster-mode behavior. Lowest precedence among the extra layers: secrets and envVars both override on conflict because dev-local overrides (secrets) and KCL pins (envVars) are more specific.
  3. secrets — KEY=VALUE pairs from the gitignored secrets_file (`.env.<env>`). Wins over projectConfig so a developer can override forge.yaml values locally without editing committed config.
  4. envVars — KCL-declared per-env config. Wins over secrets so reproducible per-env config can't drift across machines.

Returns a fresh []string safe to assign to cmd.Env. A nil projectConfig is treated as an empty layer so the pre-extension callers stay terse.

func LayerHostEnvConflicts added in v0.1.45

func LayerHostEnvConflicts(base []string, projectConfig, secrets, envVars map[string]string) ([]string, []envutil.EnvConflict)

LayerHostEnvConflicts is LayerHostEnv with the resolved conflicts returned so the caller can report them. A declared value that silently loses to an ambient one is the failure mode this reporting exists to end.

func LoadSecretsFile

func LoadSecretsFile(path string) (map[string]string, error)

LoadSecretsFile reads a gitignored secrets dotenv into a map. Returns (nil, nil) when path is empty so the caller can unconditionally call this. Missing-file is logged via the returned warn-only error wrapping os.ErrNotExist; permission / parse errors propagate.

Distinct from the legacy "env_file" load: the secrets-file contract is "if present, layer first; KCL env_vars override on conflict" — see LayerHostEnv for the composition.

func PIDPath

func PIDPath(name string) (string, error)

PIDPath returns the canonical per-service PID file path:

$HOME/.cache/forge/run/<service>.pid

Canonical convention. Used by `forge run <svc>` (foreground cleanup + background detach + stop subcommand). `forge env up` uses its own per-env state file under $HOME/.cache/forge/up/<env>.pids because it tracks N processes (services + frontends + port-forwards) and the per-env grouping is the unit of teardown there; the two conventions coexist deliberately.

Types

type RunnerSpec

type RunnerSpec struct {
	Runner    string // "" (= go-run) | "go-run" | "air" | "binary" | "delve"
	AirConfig string // path relative to the working dir; default DefaultAirConfig
	DelvePort int    // dlv --listen=:<port>; default DefaultDelvePort

	// GoPkg is the workload's GoBuild.cmd (e.g. "./cmd/acme"): the package
	// go-run runs and binary/delve were built from. Empty means the
	// workload has no Go build, and those runners have nothing to derive
	// an argv from.
	GoPkg string
	// OutputName is the GoBuild.output_name — the ./bin/<name> binary
	// and delve execute. Empty falls back to the workload name, which is
	// what `forge build` names a binary with no output_name.
	OutputName string

	// Command, when non-empty, is the workload's spec.command: an explicit
	// entrypoint run verbatim with Args, for any runner but air. It is how
	// a host workload runs something that is not this project's Go binary
	// — a sibling repo's `go run ./cmd/reliant`, a vendored tool. Relative
	// paths resolve against the effective cmd.Dir (WorkingDir), matching
	// shell semantics.
	Command []string
	// Args is the workload's spec.args — the same args its container gets
	// (the project binary's subcommand). Stated once for every runtime.
	Args []string

	// WorkingDir is the subprocess cwd override. Empty = inherit parent.
	// Relative paths resolve against ProjectDir; absolute paths are
	// used verbatim.
	WorkingDir string
	// ProjectDir is the forge project root used to resolve a relative
	// WorkingDir. Ignored when WorkingDir is empty or absolute. Empty
	// ProjectDir + relative WorkingDir falls through to the parent's
	// cwd interpretation (exec.Cmd default).
	ProjectDir string
}

RunnerSpec is the dispatch input: a host workload's runtime block (forge.Host{runner, air_config, delve_port, working_dir}) plus the two runtime-independent facts the argv is DERIVED from — its Go build and its args. Env composition is layered by the caller via LayerHostEnv so this package stays vendor-neutral about how config gets sourced.

WorkingDir + ProjectDir control the subprocess's working directory. When WorkingDir is empty, the subprocess inherits the parent's cwd (the project root, where forge was invoked). When WorkingDir is set:

  • absolute paths are used as-is;
  • relative paths resolve against ProjectDir (which the CLI sets to the forge project root).

The cross-repo Air case is the load-bearing example: a host workload declares `working_dir = "../sibling-repo"` so an Air config that lives in the sibling repo and references build paths relative to ITS own repo root resolves correctly even though forge itself runs from the caller's project root.

func (RunnerSpec) AirRunsArgs added in v0.1.45

func (s RunnerSpec) AirRunsArgs() bool

AirRunsArgs reports whether the air config this spec launches with runs the binary it builds with exactly the declared Args — its `[build] entrypoint` is `[<binary>, <Args>...]` — so the args are honoured after all, by the config rather than by forge. The scaffolded `.air.toml` runs `<bin> server`, which is what a dev env's API workload declares.

false when the config cannot be read or parsed, names no entrypoint, or passes different args: the caller then says the declared args are not passed, because nothing proves they are.

func (RunnerSpec) IgnoresArgs added in v0.1.45

func (s RunnerSpec) IgnoresArgs() bool

IgnoresArgs reports whether declared Args cannot reach the process: under air the air config's entrypoint / args_bin owns the argv. Callers warn on it, because an arg that is silently not passed looks exactly like an arg the program ignored.

Jump to

Keyboard shortcuts

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