Bundle the Go toolchain in azd (self-install like Bicep/gh) to remove the 'install Go first' requirement for Go Functions
- Dominant language
- Go
- Stars
- 569
- Forks
- 364
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 136
Description
## Summary
As part of #8307 (PR #8599) azd gained support for deploying **Go** Azure Functions. Today this requires the user to have the **Go toolchain pre-installed** and on `PATH` (`cli/azd/pkg/tools/golang/golang.go` runs `go version`, `go mod download`, `go build`, min version 1.24).
This issue proposes investigating/implementing **azd downloading the Go toolchain itself** — the same self-install pattern azd already uses for the **Bicep CLI** (`cli/azd/pkg/tools/bicep/bicep.go`) and the **GitHub CLI** (`cli/azd/pkg/tools/github/github.go`), which download per-OS/arch binaries into `config.GetUserConfigDir()/bin/` and execute them from there.
## Feasibility (verified)
This was validated empirically: the official Go archive (`https://go.dev/dl/go.-.tar.gz`, `.zip` on Windows) is **fully self-contained and relocatable**. After extracting to a non-standard path, with a **clean environment (empty `PATH`, no `GOROOT`, no system Go)**:
- `go/bin/go version` works — `GOROOT` is auto-resolved from the binary's own location.
- Cross-compiling to **linux/amd64** (the Functions target) and to other targets (e.g. windows/arm64) succeeds with `CGO_ENABLED=0` — **no gcc/clang or system toolchain required**.
The azd Functions build path already sets `GOOS=linux GOARCH=amd64 CGO_ENABLED=0` (`cli/azd/pkg/project/framework_service_go.go`), which is exactly the dependency-free mode. So azd can download the **host** Go archive once and cross-compile to the target, just like the current code does.
## Trade-offs
| Aspect | Win (bundle Go in azd) | Cost / Risk |
|---|---|---|
| **Onboarding** | User runs `azd up` on a Go Functions app with **zero prep** — no "install Go first" step | — |
| **Consistency** | azd controls the exact Go version used to build; reproducible across machines/CI | Must track & bump the pinned Go version (another tool to maintain, like Bicep/gh) |
| **Disk footprint** | — | **~71 MB download, ~269 MB extracted** per version (vs ~10 MB Bicep). Largest self-installed tool by far |
| **First-build latency** | — | One-time download + extract before first build; needs progress UX |
| **Offline / determinism** | Can pin `GOTOOLCHAIN` to avoid surprise toolchain downloads | If a user's `go.mod` requires a newer `go`, Go 1.21+ auto-downloads a matching toolchain unless pinned |
| **Caches** | azd can isolate `GOCACHE`/`GOMODCACHE` under `~/.azd` | Extra disk under azd's dir; `go mod download` still needs network |
| **Maintenance** | Reuses the proven Bicep/gh self-install pattern | New code path, checksum verification, per-OS/arch URL matrix to keep working |
| **Respecting user setup** | Keep an `AZD_GO_TOOL_PATH` override + prefer on-`PATH` `go` if present | Slight added complexity in resolution logic |
### ⚠️ Is this actually worth it?
**Go is generally easy to install** (single archive, `winget`/`brew`/`apt`, no complex system changes). If the team judges that "please install Go" is an acceptable one-time ask, the **~269 MB on-disk cost + maintenance of yet another pinned tool may outweigh the convenience**. Unlike Bicep (niche, hard to discover) or a C/cross toolchain (genuinely painful), Go's installer story is good. This proposal should only proceed if the friction-removal is deemed clearly worth that cost — otherwise simply requiring a Go install (current behavior) is a reasonable default.
## Broader benefit: `azd extensions`
A bundled, self-managed Go toolchain isn't only useful for Go Functions. It would let **azd build Go code without the user having Go installed**, which unlocks extension scenarios such as:
- Installing an **extension from source** (`azd ext` building a Go extension locally).
- Installing/trying an extension **from a specific PR or branch** by compiling it on the fly.
This makes a shared internal "Go-as-a-tool" capability (download + build) potentially reusable beyond the Functions framework service.
---
## Implementation Plan
The goal: make Go a **self-installed tool** following the existing Bicep pattern, while preserving the ability to use a user-provided Go.
### 1. Add download/install to the Go tool wrapper
File: `cli/azd/pkg/tools/golang/golang.go` (model on `cli/azd/pkg/tools/bicep/bicep.go`).
- Add a pinned `Version` constant (e.g. `1.24.x`, ≥ the current `MinimumVersion`).
- `azdGoPath()` → resolve install dir under `config.GetUserConfigDir()` (e.g. `/go//`), exec path `/go/bin/go` (`.exe` on Windows).
- `downloadGo(ctx)`:
- Build per-OS/arch release name: `go.-.` where `ext` = `zip` (Windows) / `tar.gz` (Linux, macOS), arch in Go's naming (`amd64`, `arm64`).
- URL base: `https://go.dev/dl/`.
- **Verify checksum** — Go publishes SHA256 per file; the version index is available at `https://go.dev/dl/?mode=json` (and `...?mode=json&include=all`). Match Bicep's integrity approach.
- Extract `.zip`/`.tar.gz` into the install dir (reuse extraction helpers like those in `github.go`'s `extractFromZip`/`extractFromTar`; **preserve executable bits** on `go/bin/go`).
- `ensureInstalled(ctx)` (lazy, `sync.Once`-style like Bicep):
1. Honor `AZD_GO_TOOL_PATH` env override.
2. If a suitable `go` is already on `PATH` and meets `MinimumVersion`, optionally prefer it (decide policy; see step 5).
3. Else if `azdGoPath()` exists and version OK, use it.
4. Else download.
- Update `CheckInstalled` to call `ensureInstalled` instead of erroring when `go` is missing.
### 2. Execute the bundled Go correctly
In `Build`/`ModDownload` (`golang.go`):
- Run the resolved `go` path (not the literal `"go"`).
- Set `GOROOT=/go` explicitly (defensive; auto-detect also works).
- Set `GOCACHE` / `GOMODCACHE` to azd-managed dirs under `config.GetUserConfigDir()` for isolation (optional but recommended).
- Consider setting `GOTOOLCHAIN` policy (e.g. `local` for determinism, or leave `auto` to allow `go.mod`-driven upgrades). Document the choice.
### 3. Wire-up / DI
- Confirm `cli/azd/cmd/container.go` registration of `golang.NewCli` still holds (no constructor signature change needed if download is internal/lazy).
- `framework_service_go.go` `RequiredExternalTools` continues to return the Go CLI; the difference is `CheckInstalled` now self-heals by downloading.
### 4. Tests
- Unit-test URL/release-name construction across all OS/arch combos (mirror Bicep tests).
- Unit-test version parsing/comparison and the `AZD_GO_TOOL_PATH` / prefer-PATH logic.
- Extraction test (zip + tar.gz) preserving the executable bit.
- Keep the functional test `Test_CLI_Up_Down_GoFuncApp` (added in #8599) green; ensure it works without a system Go when bundling is enabled.
- Follow repo testing rules in `cli/azd/AGENTS.md` (no writes to `os.Stdout`, table-driven tests, etc.).
### 5. Resolution policy & UX (decide explicitly)
- **Prefer user's Go vs always bundle?** Recommended: prefer a valid on-`PATH` Go (fast, no download); fall back to bundling. Add `AZD_GO_TOOL_PATH` override either way.
- **Progress UX**: show a download/extract progress indicator (Go is ~71 MB). Follow `cli/azd/docs/style-guidelines/azd-style-guide.md`.
### 6. Documentation
- `cli/azd/docs/environment-variables.md`: document `AZD_GO_TOOL_PATH` (+ any `GOTOOLCHAIN`/cache behavior azd sets), verifying parsing semantics against source.
- Note the new self-install behavior wherever Bicep/gh auto-install is described.
### 7. (Optional, follow-up) Generalize for extensions
- Extract a reusable "ensure Go toolchain" capability so `azd ext` can build Go extensions from source / from a PR without a user Go install. Track as a separate issue once the Functions path lands.
### Acceptance criteria
- On a machine **without Go installed**, `azd up` on the `gofuncapp` sample builds and deploys successfully.
- A user-provided Go on `PATH` (and `AZD_GO_TOOL_PATH`) is still honored.
- Download is checksum-verified, cached per version, and re-used across runs.
- Pre-commit checks pass (`gofmt`, `golangci-lint`, `cspell`, copyright, build, tests) — e.g. via the `/azd-preflight` skill.
### References
- Self-install patterns: `cli/azd/pkg/tools/bicep/bicep.go`, `cli/azd/pkg/tools/github/github.go`
- Current Go tool: `cli/azd/pkg/tools/golang/golang.go`, `cli/azd/pkg/project/framework_service_go.go`
- Original Go Functions work: #8307 / #8599
- Go downloads + version index: https://go.dev/dl/ , https://go.dev/dl/?mode=json
Contributor guide
Research direction
Start with cli/azd/pkg/tools/golang/golang.go and compare the self-install flows in cli/azd/pkg/tools/bicep/bicep.go and cli/azd/pkg/tools/github/github.go. Trace framework_service_go.go and the container registration, then review the listed unit and functional tests plus cli/azd/AGENTS.md. Done means checksum-verified, cached Go installation works without system Go while PATH and AZD_GO_TOOL_PATH overrides remain honored.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100