# P4LF Developer Guide This guide is for developers working on p4lf itself (as opposed to end users installing/operating it — see `README.md` and `install.sh` for that). It covers required tooling, the build system, versioning, and the Perforce stream/promotion workflow. ## Required Tools * **Go 1.20+** — install from https://go.dev/dl/ if not already present. Verify with `go version`. * **make** — the project's build/test/release automation is driven entirely through the `Makefile`; no other build tool is required. * **gofmt** — ships with the Go toolchain; used by `make fmt` / `make fmt-fix`. * **A P4 client** — mapped to the `//p4lf/dev` stream (or a `//p4lf/dev_TAG` stream parented by it, for experimental/disposable work). See [Perforce Workflow](#perforce-workflow) below. * **curl** — only needed if testing `install.sh` end-to-end (it downloads release artifacts from workshop.perforce.com). No other dependencies are required; p4lf's Go module has no third-party runtime dependencies beyond the Go standard library (check `go.mod` if that ever changes). ## Building All builds are driven by the `Makefile`. Common targets: ```bash make build # Build for the current platform → bin/p4lf make build-linux # Cross-compile Linux amd64 (primary deployment target) make build-linux-arm64 # Cross-compile Linux arm64 make build-darwin # Cross-compile macOS arm64 (Apple Silicon dev machines) make build-darwin-amd64 # Cross-compile macOS Intel make build-all # All of the above ``` Each binary embeds the current `SemVer` (see [Versioning](#versioning) below) via `-ldflags`, plus Perforce keyword expansion (`$Id$`, `$Change$`, `$DateTime$`) baked into `internal/version/version.go` at submit time. Check what's embedded in a given binary with: ```bash bin/p4lf -version # or, without running the binary: strings bin/p4lf | grep '\$' ``` ## Testing and Quality Checks ```bash make test # go test -race -count=1 ./... make test-verbose # same, with -v make vet # go vet ./... make fmt # Check gofmt compliance (fails if files need formatting) make fmt-fix # Apply gofmt and rewrite files in place make check # vet + fmt + test — run this before every submit ``` Always run `make check` before submitting a changelist. CI/reviewers expect a clean `make check` as a baseline. ## Versioning p4lf follows [Semantic Versioning](https://semver.org/) (Major.Minor.Patch). The version is hand-maintained in `internal/version/version.go`: ```go var SemVer = "1.0.4" ``` **To bump the version:** 1. `p4 edit internal/version/version.go` 2. Update the `SemVer` string, e.g. `"1.0.4"` → `"1.0.5"`. 3. Run `make check` to confirm nothing is broken. 4. Submit as part of (or immediately alongside) the change(s) that justify the bump. Guidelines for which part to bump: * **Patch** (`X.Y.Z+1`) — bug fixes, doc-only changes, internal refactors with no behavior change visible to operators. * **Minor** (`X.Y+1.0`) — new config options, new features, backward- compatible behavior changes. * **Major** (`X+1.0.0`) — breaking changes (e.g. config format changes, removed options, changed on-disk state/chunk file formats). The version can also be overridden at build time without touching the source, e.g. for a CI/release pipeline that wants to stamp a different value: ```bash make build VERSION=1.2.3 # equivalent to: go build -ldflags "-X workshop.perforce.com/p4lf/internal/version.SemVer=1.2.3" ... ``` `version.go` also embeds Perforce keywords (`$Id$`, `$Change$`, `$DateTime$`) which are expanded automatically by Perforce on submit — these are informational (depot path/revision, changelist, submit date) and require no manual maintenance. Do not hand-edit the keyword lines; just leave them as `$Id$` etc. in new files and let Perforce expand them. ## Perforce Workflow See `ai/P4WorkflowNotes.md` for full details on streams, jobs, and how AI sessions interact with version control. Summary for day-to-day development: * **`//p4lf/dev`** — the main development stream. Iterate freely here; the quality bar is "passes `make check`", not "release-ready". This is where we are currently working. * **`//p4lf/dev_TAG`** — optional short-lived streams parented by `dev`, for prototyping, R&D, or disposable/experimental work (`TAG` is a job-style identifier, e.g. `P4LF-3`). Use only when a change is risky/experimental enough to want an easy way to discard it. * **`//p4lf/main`** — populated only via a **Copy Up** from `dev`, after the change has had some testing in `dev`. `main` is expected to always be usable as "latest release" — e.g. `install.sh` defaults to installing from `main`, and `https://workshop.perforce.com/download/p4lf/main/bin/...` is treated as the stable download URL. * **`//p4lf/rX.Y`** — release streams, populated via a Copy Up from `main` when cutting a release. Emergency fixes can be made directly in a release stream if needed, but must then be **Merged Down** to `main`, then to `dev`, and from there to any active `dev_TAG` streams. ### Promoting dev → main There are two equally valid ways to work across streams: * **A dedicated workspace per stream** — e.g. one client mapped to `//p4lf/dev` and a separate client mapped to `//p4lf/main`. This is the more traditional approach and tends to be simpler and more predictable for larger or more complex workspaces. * **A single stream-switchable client**, repurposed on the fly with `p4 switch`. This works fine for a small project like this one (it's how this promotion was actually done), but `p4 switch` has more edge cases and gotchas at larger scale or with more complex workspace mappings, so don't treat it as the only or default approach — use whichever fits your setup. If using `p4 switch`: ```bash p4 switch -l # list available streams; '*' marks the current one p4 switch main # switch this client to //p4lf/main ``` If using a dedicated `main` workspace instead, just `cd` into it — no `p4 switch` needed. Preview what would be promoted before doing it for real: ```bash p4 copy -n -S //p4lf/dev # dry run — shows what would sync/integrate/branch p4 copy -S //p4lf/dev # opens the files for integrate/branch p4 submit -d "Promote vX.Y.Z from dev to main: ." ``` Only promote code from `dev` that has passed `make check` and had at least some manual/integration testing. `main` should always be safe to treat as the current stable release. **Important — rebuild binaries in `main` after promoting.** Because `bin/` is an `isolate`d path (see [The `ai/` Folder](#the-ai-folder) below), the copy-up above will **not** bring `main`'s binaries up to date — the copy only touches source/docs/config files. You must rebuild and submit binaries separately, from within the `main` stream: ```bash make build-all p4 reconcile bin/... # or: p4 rec bin/... p4 submit -d "Rebuild platform binaries for vX.Y.Z in main." bin/... ``` Then verify: ```bash ./bin/p4lf- -version # confirm the new SemVer and CL are stamped in p4 status ... # confirm the workspace is clean (no reconcile needed) ``` When you're done, switch back to `dev` to resume development (if using `p4 switch`; if using a dedicated workspace per stream, just `cd` back): ```bash p4 switch dev ``` ### Cutting a release stream Run from the new `//p4lf/rX.Y` workspace, after promoting to `main`: ```bash p4 copy //p4lf/main/... //p4lf/rX.Y/... # Then, in the rX.Y workspace, build and add platform binaries: make build-all p4 add -t binary bin/p4lf-linux-amd64 bin/p4lf-linux-arm64 p4 add -t binary bin/p4lf-darwin-arm64 bin/p4lf-darwin-amd64 p4 submit -d "Release vX.Y.Z: add platform binaries." ``` See the `## Release` section of the `Makefile` for the exact commands, and `make release VERSION=X.Y.Z` to build all platforms and produce `dist/` tarballs locally for testing before cutting a real release stream. ### Jobs Perforce jobs (e.g. `P4LF-3`) are used in place of a separate issue tracker. See `ai/P4WorkflowNotes.md` for the job spec format, `Type`/`Severity` values, and how to link a job to a changelist with `p4 fix -c `. ## The `ai/` Folder The `ai/` directory (this repo's root also symlinks `ai/AGENTS.md` to `copilot-instructions.md`) holds AI-assistant working notes: the current `AGENTS.md` task brief and dated session logs (e.g. `ai/session_log_2026-08-25.md`). These are **not part of the shipped product** — they exist purely to help a human or a future AI session pick up context on prior work, decisions, and rationale. Because of that, `ai/` (along with `bin/`) is configured as an **isolated** path in the `//p4lf/main` stream spec (`Paths:` includes `share ...` plus `isolate ai/...` and `isolate bin/...`). This means: * `ai/` content is versioned in `//p4lf/dev` (and any `dev_TAG` streams parented by it), but a Copy Up from `dev` to `main` will **not** bring `ai/` along — it stays isolated to the dev side of the tree. * `bin/` is isolated for a different reason: it's expected to exist in every stream, but each stream builds and commits its own binaries rather than inheriting them from `dev` (e.g. `main` gets freshly built binaries as part of promotion/release, per the release workflow above). Net effect: when reading `main` or a release stream, you'll see the product source and binaries, but not the `ai/` session history — that history is intentionally kept dev-side.