Skip to content

Command Palette

Search for a command to run...

Build intelligence

Matrix builds

Cross-version and cross-platform builds from CLI flags or a phelix.yaml profile.

Run a matrix

Matrix mode builds the cross-product of toolchain version × platform in one command. It is auto-enabled when --go-versions, --rust-versions, or --platforms is set.

The matrix can be configured three ways - CLI flags, a matrix: profile in phelix.yaml, or the interactive wizard - all three converge into the same configuration and are validated identically.

Terminal
# Native binariesphelix build myapp --matrix --go-versions 1.22,1.23,1.27 --platforms linux/amd64,linux/arm64phelix build myapp --matrix --rust-versions 1.77,1.78 --platforms linux/amd64,linux/arm64phelix build myapp --matrix --go-versions 1.27 --platforms linux/arm/v7,linux/arm64 # Dry run / debugphelix build myapp --matrix --go-versions 1.22,1.23 --platforms linux/amd64 --matrix-dry-runphelix build myapp --matrix --go-versions 1.22,1.23 --platforms linux/amd64,linux/arm64 --debug # Docker images (per-combination tags, optional multi-arch manifest)phelix dockerize myapp --matrix --go-versions 1.22,1.27 --platforms linux/amd64,linux/arm64phelix dockerize myapp --matrix --go-versions 1.27 --platforms linux/amd64,linux/arm64 --multi-arch-tag --pushphelix dockerize myapp --matrix --go-versions 1.22,1.27 --platforms linux/amd64,linux/arm64 --push --push-partial

Matrix flags

FlagPurpose
--matrixEnable matrix mode (auto-enabled by the dimension flags or an enabled phelix.yaml profile; an explicit --matrix=false disables an enabled profile - combining it with dimension flags is rejected)
--go-versionsComma-separated Go versions (1.21, 1.22.4, go1.27, v1.27 all work; patch versions allowed)
--rust-versionsComma-separated Rust versions (same format)
--platformsTarget platforms (e.g. linux/amd64,linux/arm64,linux/arm/v7,darwin/arm64; case-insensitive)
--matrix-concurrencyMax parallel builds (default: 3)
--matrix-retriesRetry failed combinations up to N additional times (default: 0; only transient failures are retried - see Matrix runs)
--resumeResume an interrupted matrix run (--resume picks the most recent, --resume=mx_... a specific run; implies matrix mode)
--matrix-dry-runPrint the matrix plan without executing
--debugVerbose output: Docker commands, build logs, cache paths
--multi-arch-tag(dockerize) additionally assemble a multi-arch manifest list per toolchain version via docker buildx
--push-partial(dockerize) push only the successful images even if some combinations failed

Known targets

Versions are validated as major.minor or major.minor.patch - any current or future toolchain release works, including patch versions. Duplicates are removed, whitespace and version prefixes (go, rust, v) are normalized, and every combination is validated before the first build starts.

DimensionSupported
Go versionsAny major.minor or major.minor.patch
Rust versionsAny major.minor or major.minor.patch
Linuxamd64, arm64, arm/v7, arm/v6
macOSamd64, arm64
Windowsamd64

The matrix profile

The same matrix can be configured persistently in phelix.yaml:

phelix.yamlyaml
matrix:  enabled: true  go:                # or rust: - exactly one ecosystem    versions:      - "1.25"      - "1.26"      - "1.27"  platforms:    - linux/amd64    - linux/arm64    - windows/amd64  concurrency: 4     # optional, default 3  retries: 2         # optional, automatic retries for transient failures  include:           # optional, extra combinations / metadata    - go: "1.28"      platform: linux/amd64      tag: latest  exclude:           # optional, drop matching combinations    - go: "1.25"      platform: windows/amd64

With matrix.enabled: true, a plain phelix build runs the matrix - no flags needed - and so does a plain phelix dockerize (its Linux combinations build images; other platforms fail fast with a pointer to phelix build --matrix). The profile goes through the exact same validation and expansion as CLI flags.

Dimensions, include, exclude

Internally the matrix is a set of dimensions (lang, version, os, arch, variant); the Cartesian product of the configured versions × platforms is only the base of the expansion. The full pipeline is deterministic and identical for CLI flags, phelix.yaml, the wizard, and any future remote execution:

Expansion pipeline
Base Cartesian product  →  Include rules  →  Exclude rules  →  Final combinations

Exclude rules are partial matchers: a rule matches every combination that carries the constrained values, regardless of the other dimensions. Rule keys: go/rust (shorthand for ecosystem + version), lang, version, platform, os, arch, variant. This drops all Windows combinations of Go 1.25 and nothing else:

excludeyaml
exclude:  - go: "1.25"    platform: windows/amd64

Include rules do two things: a rule that names a full combination (ecosystem + version + platform) adds it when the base product doesn't contain it - go: "1.28" above builds 1.28 even though only 1.25-1.27 are configured - and any other keys on an include entry (like tag: latest) are metadata merged into the matching combination(s), recorded in the run and visible in matrix show.

  • A duplicate include (a combination already in the matrix) never schedules the job twice - it only merges its metadata.
  • A well-formed rule that matches no combination at the point it is applied is a configuration error, so a typo cannot silently shrink (or fail to shrink) the matrix. Exclude rules reject unknown keys outright for the same reason.
  • Excludes apply after includes, so an exclude can remove an included combination.

The wizard (phelix matrix init) configures includes and excludes too, and its preview shows the real pipeline counts (base 6 · included +1 · excluded -1) computed by the same expansion engine the build uses.

Configuration precedence (per dimension, deterministic): CLI explicit value > phelix.yaml matrix profile > command default. List dimensions are replaced, never merged: phelix build --go-versions 1.28 next to the profile above builds only 1.28 (the YAML version list is overridden entirely), while unmentioned dimensions keep their configured values. --matrix=false explicitly disables an enabled profile. Versions and platforms are validated identically wherever they come from: an invalid profile fails phelix build (and every command that reads phelix.yaml) with an error naming the YAML location, e.g. configuration error: matrix.go.versions: ....

Build strategy

Go cross-compiles natively via GOOS/GOARCH (plus GOARM=6/7 for the ARM variant platforms) with CGO_ENABLED=0 - no extra toolchain needed. CGO projects fail with a clear error suggesting Docker-based builds or a C cross-compiler. If no host Go toolchain is installed - or the host toolchain is a different Go version than the requested one - the build automatically falls back to the Docker path below, so an artifact is never labeled with a toolchain it was not built with.

Rust uses the cross tool (not raw rustup target add), building inside a Docker container with the correct linker/C libraries pre-configured. The requested version is pinned via cross +<version>, so each combination really builds with its own toolchain (rustup auto-installs missing ones).

Multi-version builds: with more than one Go version (or no host Go toolchain), each version runs inside its own Docker container (golang:1.22, golang:1.22.4, ...) - no need for multiple toolchains on the host, and clean cache isolation. The build command is passed as separate arguments (never through a shell), with GOOS/GOARCH/GOARM/CGO_ENABLED injected via docker run -e.

Docker matrix builds are linux-only: Docker images cannot target darwin/* or windows/*, so those combinations fail fast with a pointer to phelix build --matrix for native binaries. Per-combination TARGET* build args (TARGETPLATFORM, TARGETOS, TARGETARCH, TARGETVARIANT, TARGETVERSION) are passed to every image build, so generated Dockerfiles can react to the target, and the requested toolchain version is additionally passed as GO_VERSION/RUST_VERSION - the build args the generated Dockerfiles pin their toolchain images on. A Dockerfile is generated when the project has none, exactly like a single-image dockerize. Your own --build-arg KEY=VALUE flags are forwarded too, and malformed entries (KEY without a value, empty keys) are rejected up front instead of being silently dropped.

Concurrency and caching: a worker pool runs combinations in parallel (default 3); each combination gets its own cache directory (.phelix/cache/go/{combo-id}/ or target/{combo-id}/); the live progress display shows running, completed, and failed combinations. A combination whose build panics or returns no result is recorded as failed and never takes down the rest of the matrix.

Output naming

Every combination produces a unique, path-safe artifact - combinations can never overwrite each other:

ArtifactNaming
Binarybuilds/matrix/{lang}{version}-{os}-{arch}[-{variant}]/{app}_{arch}_{lang}_{version}[_{variant}] - e.g. builds/matrix/go1.22.4-linux-arm-v7/myapp_arm_go_1.22.4_v7
Docker per-combination tag{app}:{tag|latest}-{lang}{version}-{arch}[-{variant}] - e.g. myapp:v1.2.3-go1.22-amd64, myapp:latest-go1.22.4-arm-v7
Docker multi-arch manifest{app}:{tag|latest} for a single combination, version-qualified ({app}:{tag}-{version}) whenever the run produced more than one combination, so concurrent versions never overwrite each other's manifest
JSON reportbuilds/matrix/report.json
Release manifest<PHELIX_DATA_DIR>/matrix/runs/<run-id>.manifest.json - one per finished run with artifacts (see Matrix runs)

Reporting and failures

PhaseBehavior
Build phaseFail-open: one failure doesn't stop the rest; full summary at the end. The CLI exits non-zero when any combination failed, so scripts can detect partial failures
Push phaseFail-closed by default: if any combination failed, nothing is pushed. Use --push-partial to push only successful images

Reporting combines a terminal summary with a JSON report (builds/matrix/report.json, carrying the Matrix Run ID as run_id) listing each combination's status, duration, artifact path, SHA-256 checksum, cache status, attempt count and per-attempt log (automatic retries), and error (if failed - redacted, so compiler output with embedded credentials never lands in the report). The report describes the whole run: a resumed run's report covers every combination (earlier sessions included), and combinations that never ran appear as pending.

Per-combination reports

Every combination retains its own build metrics - toolchain version, target platform, duration, cache status, and binary size - recorded alongside the matrix version's artifacts in versions.json and mirrored in builds/matrix/report.json (cache_status per combination).

Regression analysis is combination-aware: Go 1.27 / linux-amd64 is only ever compared against previous Go 1.27 / linux-amd64 builds, never against Go 1.26 / linux-arm64 or a Rust build. After recording, each combination prints a compact summary of its own comparisons.

Every matrix execution also gets a traceable Matrix Run with live status, retries, resume, and a release manifest - see Matrix runs.