fix(coderd/templatebuilder): guard quickstart C/C++ install on every tool it installs (#30374)

The Quickstart base's C/C++ branch guarded on `command -v gcc`, but
installs `gcc g++ make cmake`. `codercom/enterprise-base:ubuntu` ships
gcc but not cmake, so cmake was never installed while the script
reported success. The guard now checks every tool the install provides
(`gcc`, `g++`, `make`, `cmake`).

Adds `TestQuickstartCppInstallGuard`, which runs the rendered C/C++
branch against stubbed tools and asserts the install runs when any of
them is missing. It fails without the fix.

Fixes DOCS-736. The matching example template change is in the DOCS-613
PR.

Generated by Coder Agents on behalf of @nickvigilante.
This commit is contained in:
Nick Vigilante
2026-10-14 12:35:48 -04:00
committed by GitHub
parent b8c9195b3f
commit 6afd1515e0
2 changed files with 76 additions and 1 deletions
@@ -85,7 +85,10 @@ if has_language java; then
fi
if has_language cpp; then
if command -v gcc >/dev/null 2>&1; then
# Guard on every tool the install below provides, not just gcc: the base
# image ships gcc but not cmake.
if command -v gcc >/dev/null 2>&1 && command -v g++ >/dev/null 2>&1 \
&& command -v make >/dev/null 2>&1 && command -v cmake >/dev/null 2>&1; then
echo "C/C++: $(gcc --version | head -1)"
else
echo "Installing C/C++ toolchain..."
+72
View File
@@ -6,7 +6,12 @@ import (
"errors"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -700,3 +705,70 @@ func extractTar(t *testing.T, data []byte) map[string]string {
}
return files
}
// TestQuickstartCppInstallGuard verifies the C/C++ branch of the quickstart
// install script installs the toolchain when only part of it is present. The
// base image ships gcc but not cmake, so guarding on gcc alone skipped the
// install and still reported success.
func TestQuickstartCppInstallGuard(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("requires bash")
}
bash, err := exec.LookPath("bash")
require.NoError(t, err)
head, err := exec.LookPath("head")
require.NoError(t, err)
fsys, err := templatebuilder.BaseTemplateFS("quickstart")
require.NoError(t, err)
raw, err := fs.ReadFile(fsys, "install-languages.sh.tftpl")
require.NoError(t, err)
// Render the template the way Terraform's templatefile would for the
// two constructs the script uses.
script := strings.ReplaceAll(string(raw), "${LANGUAGES}", "cpp")
script = strings.ReplaceAll(script, "$$", "$")
// gcc is always present: the script echoes its version after installing.
tests := []struct {
name string
present []string
wantInstall bool
}{
{name: "AllPresent", present: []string{"gcc", "g++", "make", "cmake"}, wantInstall: false},
{name: "MissingCmake", present: []string{"gcc", "g++", "make"}, wantInstall: true},
{name: "OnlyGcc", present: []string{"gcc"}, wantInstall: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// PATH holds only stubs, so the host's real toolchain can't
// influence the result.
binDir := t.TempDir()
writeExecutable := func(name, content string) {
path := filepath.Join(binDir, name)
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
require.NoError(t, os.Chmod(path, 0o700))
}
require.NoError(t, os.Symlink(head, filepath.Join(binDir, "head")))
stub := "#!" + bash + "\necho stub 1.0\n"
for _, tool := range tt.present {
writeExecutable(tool, stub)
}
// The sudo stub records its arguments and "installs" nothing.
logPath := filepath.Join(t.TempDir(), "sudo.log")
sudo := "#!" + bash + "\necho \"$@\" >> " + logPath + "\n"
writeExecutable("sudo", sudo)
cmd := exec.Command(bash, "-c", script)
cmd.Env = []string{"PATH=" + binDir, "HOME=" + t.TempDir()}
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
logged, _ := os.ReadFile(logPath)
installed := strings.Contains(string(logged), "install")
require.Equal(t, tt.wantInstall, installed, "output: %s", out)
})
}
}