Skip to content

Bundle the Go toolchain in azd (self-install like Bicep/gh) to remove the 'install Go first' requirement for Go Functions #8793

Description

@vhvb1989

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<ver>.<os>-<arch>.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=0no 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. <configDir>/go/<version>/), exec path <dir>/go/bin/go (.exe on Windows).
  • downloadGo(ctx):
    • Build per-OS/arch release name: go<ver>.<goos>-<goarch>.<ext> 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=<installDir>/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 Add Go Azure Functions framework service support #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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions