Files
coder/scripts/clidocgen/gen.go
T
Mathias Fredriksson a6a8fd94d7 build(Makefile): enable parallel make -j gen with correct dependency graph (#22612)
`make gen` could not run with `-j` because inter-target dependency edges
were missing. Multiple recipes compile `coderd/rbac` (which includes
generated files like `object_gen.go`), and without explicit ordering,
parallel runs produced syntax errors from mid-write reads.

Three main changes:

**Dependency graph fixes** declare the compile-time chain through
`coderd/rbac` so that `object_gen.go` is written before anything that
imports it is compiled. The DB generation targets use a GNU Make 4.3+
grouped target (`&:`) so Make knows `generate.sh` co-produces
`querier.go`, `unique_constraint.go`, `dbmetrics`, and `dbauthz` in a
single invocation. `SKIP_DUMP_SQL=1` avoids re-entrant `make` inside
`generate.sh` when the Makefile already guarantees `dump.sql` is fresh.

**`scripts/atomicwrite` package** replaces `os.WriteFile` in all gen
scripts with a temp-file-in-same-dir + rename pattern, preventing
interrupted runs from leaving partial files.

**`.PRECIOUS` and shell atomic writes** protect git-tracked generated
files from Make's default delete-on-error behavior. Since these files
are committed, deletion is worse than staleness -- `git restore` is the
recovery path.

CI now runs `make -j --output-sync -B gen` (~32s, down from ~85s
serial).

| Scenario                          | Before             | After    |
|-----------------------------------|--------------------|----------|
| `make gen` (serial)               | 95s                | 95s      |
| `make -j gen` (parallel)          | race error         | **22s**  |
| CI `make -j --output-sync -B gen` | forced serial ~85s | **~32s** |
2026-03-05 11:58:10 +00:00

154 lines
3.3 KiB
Go

package main
import (
_ "embed"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/acarl005/stripansi"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/scripts/atomicwrite"
"github.com/coder/flog"
"github.com/coder/serpent"
)
//go:embed command.tpl
var commandTemplateRaw string
var commandTemplate *template.Template
func init() {
commandTemplate = template.Must(
template.New("command.tpl").Funcs(template.FuncMap{
"visibleSubcommands": func(cmd *serpent.Command) []*serpent.Command {
var visible []*serpent.Command
for _, sub := range cmd.Children {
if sub.Hidden {
continue
}
visible = append(visible, sub)
}
return visible
},
"visibleOptions": func(cmd *serpent.Command) []serpent.Option {
var visible []serpent.Option
for _, opt := range cmd.Options {
if opt.Hidden {
continue
}
visible = append(visible, opt)
}
return visible
},
"atRoot": func(cmd *serpent.Command) bool {
return cmd.FullName() == "coder"
},
"newLinesToBr": func(s string) string {
return strings.ReplaceAll(s, "\n", "<br/>")
},
"wrapCode": func(s string) string {
return fmt.Sprintf("<code>%s</code>", s)
},
"commandURI": fmtDocFilename,
"fullName": fullName,
"tableHeader": func() string {
return `| | |
| --- | --- |`
},
"typeHelper": func(opt *serpent.Option) string {
switch v := opt.Value.(type) {
case *serpent.Enum:
return strings.Join(v.Choices, "\\|")
case *serpent.EnumArray:
return fmt.Sprintf("[%s]", strings.Join(v.Choices, "\\|"))
default:
return v.Type()
}
},
},
).Parse(strings.TrimSpace(commandTemplateRaw)),
)
}
func fullName(cmd *serpent.Command) string {
if cmd.FullName() == "coder" {
return "coder"
}
return strings.TrimPrefix(cmd.FullName(), "coder ")
}
func fmtDocFilename(cmd *serpent.Command) string {
if cmd.FullName() == "coder" {
// Special case for index.
return "./index.md"
}
name := strings.ReplaceAll(fullName(cmd), " ", "_")
return fmt.Sprintf("%s.md", name)
}
func writeCommand(w io.Writer, cmd *serpent.Command) error {
var b strings.Builder
err := commandTemplate.Execute(&b, cmd)
if err != nil {
return err
}
content := stripansi.Strip(b.String())
// Remove the version and its right space, since during this script running
// there is no build info available
content = strings.ReplaceAll(content, buildinfo.Version()+" ", "")
// Remove references to the current working directory
cwd, err := os.Getwd()
if err != nil {
return err
}
content = strings.ReplaceAll(content, cwd, ".")
homedir, err := os.UserHomeDir()
if err != nil {
return err
}
content = strings.ReplaceAll(content, homedir, "~")
_, err = w.Write([]byte(content))
return err
}
func genTree(dir string, cmd *serpent.Command, wroteLog map[string]*serpent.Command) error {
if cmd.Hidden {
return nil
}
path := filepath.Join(dir, fmtDocFilename(cmd))
var buf strings.Builder
err := writeCommand(&buf, cmd)
if err != nil {
return err
}
err = atomicwrite.File(path, []byte(buf.String()))
if err != nil {
return err
}
flog.Successf(
"wrote\t%s",
path,
)
wroteLog[path] = cmd
for _, sub := range cmd.Children {
err = genTree(dir, sub, wroteLog)
if err != nil {
return err
}
}
return nil
}