| // Copyright 2023 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| //go:generate go run . -w | |
| // Package configgen generates the upload config file stored in the config.json | |
| // file of golang.org/x/telemetry/config based on the chartconfig stored in | |
| // config.txt. | |
| package main | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "flag" | |
| "fmt" | |
| "go/version" | |
| "log" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "regexp" | |
| "slices" | |
| "sort" | |
| "strings" | |
| "golang.org/x/mod/semver" | |
| "golang.org/x/telemetry/internal/chartconfig" | |
| "golang.org/x/telemetry/internal/telemetry" | |
| ) | |
| var ( | |
| write = flag.Bool("w", false, "if set, write the config file; otherwise, print to stdout") | |
| force = flag.Bool("f", false, "if set, force the write of the config file even if the current content is still valid") | |
| // SamplingRate is the fraction of otherwise uploadable reports that will be uploaded | |
| SamplingRate = 1.0 | |
| ) | |
| // minimumPaddings maps from program name to padding used to determine whether | |
| // an update of config.json is neccessary. | |
| // The purpose of these paddings is to pad enough versions to do two patches | |
| // releases tomorrow. | |
| var minimumPaddings = map[string]padding{ | |
| "golang.org/x/tools/gopls": { | |
| releases: 2, | |
| maj: 1, | |
| majmin: 1, // we're not ever going to do more than one major/minor release in a day | |
| patch: 2, | |
| pre: 2, // in a single day, we wouldn't prep more than two prereleases per version | |
| }, | |
| "github.com/golang/vscode-go/vscgo": { | |
| releases: 2, | |
| maj: 1, | |
| majmin: 1, | |
| patch: 2, | |
| pre: 0, | |
| }, | |
| "golang.org/x/vuln/cmd/govulncheck": { | |
| releases: 2, | |
| maj: 1, | |
| majmin: 2, | |
| patch: 0, | |
| pre: 0, | |
| }, | |
| "github.com/go-delve/delve/cmd/dlv": { | |
| releases: 1, | |
| maj: 0, | |
| majmin: 1, | |
| patch: 1, | |
| pre: 0, | |
| }, | |
| "golang.org/x/tools/cmd/goimports": { | |
| releases: 2, | |
| maj: 0, | |
| majmin: 1, | |
| patch: 2, | |
| pre: 0, | |
| }, | |
| "golang.org/x/pkgsite/cmd/internal/pkgsite-cli": { | |
| releases: 2, | |
| maj: 0, | |
| majmin: 1, | |
| patch: 2, | |
| pre: 0, | |
| }, | |
| } | |
| // regularPaddings maps from program name to padding used to reserve enough | |
| // versions for a quarter. | |
| var regularPaddings = map[string]padding{ | |
| // The padding heuristics below are based on the example of gopls. | |
| "golang.org/x/tools/gopls": { | |
| // 6 releases into the future translates to approximately three months for gopls. | |
| releases: 6, | |
| // We may release gopls 1.0, but won't release 2.0 in a three month timespan! | |
| maj: 1, | |
| // We don't usually do more than one minor release a month. | |
| majmin: 3, | |
| // Since golang/go#55267, which committed to adhering to semver, gopls | |
| // hasn't had more than 5 patches per minor version. | |
| patch: 6, | |
| // Gopls has never had more than 4 prereleases. | |
| pre: 4, | |
| }, | |
| "github.com/golang/vscode-go/vscgo": { | |
| // 8 releases into the future including 4 in the same major version, | |
| // 4 in the next major version. | |
| releases: 8, | |
| maj: 1, | |
| majmin: 4, | |
| patch: 5, | |
| pre: 0, | |
| }, | |
| "golang.org/x/vuln/cmd/govulncheck": { | |
| releases: 4, | |
| maj: 1, | |
| majmin: 1, | |
| patch: 4, | |
| pre: 0, | |
| }, | |
| "github.com/go-delve/delve/cmd/dlv": { | |
| releases: 2, | |
| maj: 0, | |
| majmin: 1, | |
| patch: 2, | |
| pre: 0, | |
| }, | |
| "golang.org/x/tools/cmd/goimports": { | |
| // same as gopls | |
| releases: 6, | |
| maj: 1, | |
| majmin: 6, | |
| patch: 3, | |
| pre: 4, | |
| }, | |
| "golang.org/x/pkgsite/cmd/internal/pkgsite-cli": { | |
| releases: 4, | |
| maj: 0, | |
| majmin: 2, | |
| patch: 4, | |
| pre: 0, | |
| }, | |
| } | |
| func main() { | |
| flag.Parse() | |
| gcfgs, err := chartconfig.Load() | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| uCfg, err := generate(gcfgs, regularPaddings) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| cfgJSON, err := json.MarshalIndent(uCfg, "", "\t") | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| if !*write { | |
| fmt.Println(string(cfgJSON)) | |
| os.Exit(0) | |
| } | |
| configFile, err := configFile() | |
| if err != nil { | |
| log.Fatalf("finding config file: %v", err) | |
| } | |
| if !*force { | |
| currentCfg, err := readConfig(configFile) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| minCfg, err := generate(gcfgs, minimumPaddings) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| if contains(currentCfg, minCfg) { | |
| fmt.Fprintln(os.Stderr, "not writing the config file as it is still valid; use -f to force") | |
| os.Exit(0) | |
| } | |
| } | |
| if err := os.WriteFile(configFile, cfgJSON, 0666); err != nil { | |
| log.Fatal(err) | |
| } | |
| } | |
| // configFile returns the path to the x/telemetry/config config.json file in | |
| // this repo. | |
| // | |
| // The file must already exist: this won't be a valid location if running from | |
| // the module cache; this functionality only works when executed from the | |
| // telemetry repo. | |
| func configFile() (string, error) { | |
| out, err := exec.Command("go", "list", "-f", "{{.Dir}}", "golang.org/x/telemetry/internal/configgen").Output() | |
| if err != nil { | |
| return "", err | |
| } | |
| dir := strings.TrimSpace(string(out)) | |
| configFile := filepath.Join(dir, "..", "..", "config", "config.json") | |
| if _, err := os.Stat(configFile); err != nil { | |
| return "", err | |
| } | |
| return configFile, nil | |
| } | |
| func readConfig(file string) (*telemetry.UploadConfig, error) { | |
| data, err := os.ReadFile(file) | |
| if err != nil { | |
| return nil, fmt.Errorf("reading config file: %v", err) | |
| } | |
| cfg := new(telemetry.UploadConfig) | |
| if err := json.Unmarshal(data, cfg); err != nil { | |
| return nil, fmt.Errorf("unmarshalling config file: %v", err) | |
| } | |
| return cfg, nil | |
| } | |
| // generate computes the upload config from chart configs and module | |
| // information, returning the resulting formatted JSON. | |
| func generate(gcfgs []chartconfig.ChartConfig, paddings map[string]padding) (*telemetry.UploadConfig, error) { | |
| ucfg := &telemetry.UploadConfig{ | |
| GOOS: goos(), | |
| GOARCH: goarch(), | |
| // the probability of uploading a report | |
| SampleRate: SamplingRate, | |
| } | |
| var err error | |
| ucfg.GoVersion, err = goVersions() | |
| if err != nil { | |
| return nil, fmt.Errorf("querying go info: %v", err) | |
| } | |
| for i, r := range gcfgs { | |
| if err := ValidateChartConfig(r); err != nil { | |
| // TODO(rfindley): this is a poor way to identify the faulty record. We | |
| // should probably store position information in the ChartConfig. | |
| return nil, fmt.Errorf("chart config #%d (%q): %v", i, r.Title, err) | |
| } | |
| } | |
| var ( | |
| programs = make(map[string]*telemetry.ProgramConfig) // package path -> config | |
| modules = make(map[string]string) // package path -> module path | |
| minVersions = make(map[string]string) // package path -> min version required, or "" for all | |
| ) | |
| for _, gcfg := range gcfgs { | |
| pcfg := programs[gcfg.Program] | |
| if pcfg == nil { | |
| pcfg = &telemetry.ProgramConfig{ | |
| Name: gcfg.Program, | |
| } | |
| modules[gcfg.Program] = gcfg.Module | |
| programs[gcfg.Program] = pcfg | |
| minVersions[gcfg.Program] = gcfg.Version | |
| } | |
| minVersions[gcfg.Program] = minVersion(minVersions[gcfg.Program], gcfg.Version) | |
| ccfg := telemetry.CounterConfig{ | |
| Name: gcfg.Counter, | |
| Rate: 1.0, // TODO(rfindley): how should rate be configured? | |
| Depth: gcfg.Depth, | |
| } | |
| if gcfg.Depth > 0 { | |
| pcfg.Stacks = append(pcfg.Stacks, ccfg) | |
| } else { | |
| pcfg.Counters = append(pcfg.Counters, ccfg) | |
| } | |
| } | |
| for _, p := range programs { | |
| minVersion := minVersions[p.Name] | |
| // Collect eligible program versions. If p is a toolchain tool (cmd/go, | |
| // cmd/compile, etc), these come out of the Go versions queried above. | |
| // Otherwise, they come from the proxy. | |
| // | |
| // In both of these cases, the versions should be valid, but we verify | |
| // anyway as otherwise the version comparison is meaningless. | |
| // (and in fact, there is an invalid go1.9.2rc2 version in the proxy) | |
| if telemetry.IsToolchainProgram(p.Name) { | |
| // Note: no need to pad versions for toolchain programs, since the | |
| // toolchain is released infrequently. | |
| // (and in any case, version padding only works for semantic versions) | |
| for _, v := range ucfg.GoVersion { | |
| if !version.IsValid(v) { | |
| // The proxy toolchain versions list go1.9.2rc2, which is invalid. | |
| // Skip it. | |
| continue | |
| } | |
| if minVersion == "" || version.Compare(minVersion, v) <= 0 { | |
| p.Versions = append(p.Versions, v) | |
| } | |
| } | |
| } else { | |
| versions, err := listProxyVersions(modules[p.Name]) | |
| if err != nil { | |
| return nil, fmt.Errorf("listing versions for %q: %v", p.Name, err) | |
| } | |
| // Filter proxy versions in place. | |
| i := 0 | |
| for _, v := range versions { | |
| if !semver.IsValid(v) { | |
| return nil, fmt.Errorf("invalid semver %q returned from proxy for %q", v, p.Name) | |
| } | |
| if minVersion == "" || semver.Compare(minVersion, v) <= 0 { | |
| versions[i] = v | |
| i++ | |
| } | |
| } | |
| // Look up paddings based on program name. | |
| if _, ok := paddings[p.Name]; !ok { | |
| return nil, fmt.Errorf("padding not defined for program %q", p.Name) | |
| } | |
| p.Versions = padVersions(versions[:i], prereleasesForProgram(p.Name), paddings[p.Name]) | |
| } | |
| ucfg.Programs = append(ucfg.Programs, p) | |
| } | |
| sort.Slice(ucfg.Programs, func(i, j int) bool { | |
| return ucfg.Programs[i].Name < ucfg.Programs[j].Name | |
| }) | |
| return ucfg, nil | |
| } | |
| // contains reports whether outer contains all program versions of inner, and | |
| // is otherwise equivalent to inner. | |
| func contains(outer, inner *telemetry.UploadConfig) bool { | |
| if !slices.Equal(outer.GOARCH, inner.GOARCH) { | |
| return false | |
| } | |
| if !slices.Equal(outer.GOOS, inner.GOOS) { | |
| return false | |
| } | |
| if !slices.Equal(outer.GoVersion, inner.GoVersion) { | |
| return false | |
| } | |
| for _, pi := range inner.Programs { | |
| i := slices.IndexFunc(outer.Programs, func(po *telemetry.ProgramConfig) bool { | |
| return po.Name == pi.Name | |
| }) | |
| if i < 0 { | |
| return false | |
| } | |
| po := outer.Programs[i] | |
| if !sliceContains(po.Versions, pi.Versions) { | |
| return false | |
| } | |
| if !slices.Equal(po.Counters, pi.Counters) { | |
| return false | |
| } | |
| if !slices.Equal(po.Stacks, pi.Stacks) { | |
| return false | |
| } | |
| } | |
| for _, po := range outer.Programs { | |
| if !slices.ContainsFunc(inner.Programs, func(pi *telemetry.ProgramConfig) bool { | |
| return pi.Name == po.Name | |
| }) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func sliceContains[T comparable](outer, inner []T) bool { | |
| m := toMap(outer) | |
| for _, v := range inner { | |
| if !m[v] { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func toMap[T comparable](s []T) map[T]bool { | |
| m := make(map[T]bool) | |
| for _, v := range s { | |
| m[v] = true | |
| } | |
| return m | |
| } | |
| // prereleasesForProgram returns the set of prereleases to use for the provided | |
| // program. We may need to customize this for the conventions of different | |
| // programs. | |
| func prereleasesForProgram(program string) []string { | |
| // Surely eight prereleases is enough for any program... :) | |
| return []string{"pre.1", "pre.2", "pre.3", "pre.4", "pre.5", "pre.6", "pre.7", "pre.8"} | |
| } | |
| // minVersion returns the lesser semantic version of v1 and v2. | |
| // | |
| // As a special case, the empty string is treated as an absolute minimum | |
| // (empty => all versions are greater). | |
| func minVersion(v1, v2 string) string { | |
| if v1 == "" || v2 == "" { | |
| return "" | |
| } | |
| if semver.Compare(v1, v2) > 0 { | |
| return v2 | |
| } | |
| return v1 | |
| } | |
| // goos returns a sorted slice of known GOOS values. | |
| func goos() []string { | |
| var gooses []string | |
| for goos := range knownOS { | |
| gooses = append(gooses, goos) | |
| } | |
| sort.Strings(gooses) | |
| return gooses | |
| } | |
| // goarch returns a sorted slice of known GOARCH values. | |
| func goarch() []string { | |
| var arches []string | |
| for arch := range knownArch { | |
| arches = append(arches, arch) | |
| } | |
| sort.Strings(arches) | |
| return arches | |
| } | |
| // goInfo queries the proxy for information about go distributions, including | |
| // versions, GOOS, and GOARCH values. | |
| func goVersions() ([]string, error) { | |
| // Trick: read Go distribution information from the module versions of | |
| // golang.org/toolchain. These define the set of valid toolchains, and | |
| // therefore are a reasonable source for version information. | |
| // | |
| // A more authoritative source for this information may be | |
| // https://go.dev/dl?mode=json&include=all. | |
| proxyVersions, err := listProxyVersions("golang.org/toolchain") | |
| if err != nil { | |
| return nil, fmt.Errorf("listing toolchain versions: %v", err) | |
| } | |
| var goVersionRx = regexp.MustCompile(`^-(go.+)\.[^.]+-[^.]+$`) | |
| verSet := make(map[string]struct{}) | |
| for _, v := range proxyVersions { | |
| pre := semver.Prerelease(v) | |
| match := goVersionRx.FindStringSubmatch(pre) | |
| if match == nil { | |
| return nil, fmt.Errorf("proxy version %q does not match prerelease regexp %q", v, goVersionRx) | |
| } | |
| verSet[match[1]] = struct{}{} | |
| } | |
| var vers []string | |
| for v := range verSet { | |
| vers = append(vers, v) | |
| } | |
| sort.Sort(byGoVersion(vers)) | |
| return vers, nil | |
| } | |
| type byGoVersion []string | |
| func (vs byGoVersion) Len() int { return len(vs) } | |
| func (vs byGoVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } | |
| func (vs byGoVersion) Less(i, j int) bool { | |
| cmp := version.Compare(vs[i], vs[j]) | |
| if cmp != 0 { | |
| return cmp < 0 | |
| } | |
| // To ensure that we have a stable sort, order equivalent Go versions lexically. | |
| return vs[i] < vs[j] | |
| } | |
| // versionsForTesting contains versions to use for testing, rather than | |
| // querying the proxy. | |
| var versionsForTesting map[string][]string | |
| // listProxyVersions queries the Go module mirror for published versions of the | |
| // given modulePath. | |
| // | |
| // modulePath must be lower-case (or already escaped): this function doesn't do | |
| // any escaping of upper-cased letters, as is required by the proxy prototol | |
| // (https://go.dev/ref/mod#goproxy-protocol). | |
| func listProxyVersions(modulePath string) ([]string, error) { | |
| // Avoid problematic interactions with the local workspace by running in a | |
| // temp directory. | |
| listDir, err := os.MkdirTemp("", "") | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer os.RemoveAll(listDir) | |
| if vers, ok := versionsForTesting[modulePath]; ok { | |
| return vers, nil | |
| } | |
| cmd := exec.Command("go", "list", "-m", "--versions", modulePath) | |
| cmd.Dir = listDir | |
| var stderr bytes.Buffer | |
| cmd.Stderr = &stderr | |
| out, err := cmd.Output() | |
| if err != nil { | |
| return nil, fmt.Errorf("listing versions: %v (stderr: %v)", err, stderr.String()) | |
| } | |
| fields := strings.Fields(strings.TrimSpace(string(out))) | |
| if len(fields) == 0 { | |
| return nil, fmt.Errorf("invalid version list output: %q", string(out)) | |
| } | |
| return fields[1:], nil | |
| } | |
| // padding defines constraints on additional versions to pad. | |
| // | |
| // These constraints help restrict version padding to "reasonable" versions, | |
| // based on heuristics such as "we never do more than 3 prereleases for a given | |
| // version" or "we never have more than 5 patch versions" or "we can't do more | |
| // than 10 total releases over that time period". See the field documentation | |
| // for details. | |
| type padding struct { | |
| releases int // bounds on the total number of releases | |
| maj int // bounds the number of new major versions | |
| majmin int // bounds the number of new major+minor versions | |
| patch int // bounds the number of new patch versions | |
| pre int // the number of prereleases to pad per release | |
| } | |
| // padVersions pads the existing version list with potential next versions, so | |
| // that we don't have to wait an additional day to start getting reports for a | |
| // newly tagged version. | |
| // | |
| // The prereleases argument may be supplied to provide a set of potential | |
| // prerelease candidates. For example, if the program releases prereleases of | |
| // the form "-pre.N", prereleases should be {"pre.1", "pre.2", ...}. For each | |
| // potential next release version, the next two prerelease versions will be | |
| // selected out of the provided set of prereleases. | |
| func padVersions(versions []string, prereleasePatterns []string, padding padding) []string { | |
| versions = slices.Clone(versions) | |
| semver.Sort(versions) | |
| latestRelease := "v0.0.0" | |
| all := make(map[string]bool) // for de-duplicating padded versions | |
| for _, v := range versions { | |
| cv := semver.Canonical(v) | |
| all[cv] = true | |
| if semver.Prerelease(cv) == "" && semver.Compare(latestRelease, cv) < 0 { | |
| latestRelease = cv | |
| } | |
| } | |
| parsedLatest, ok := parseSemver(latestRelease) | |
| if !ok { | |
| // "can't happen", since the latest release version should always be canonical. | |
| panic(fmt.Sprintf("unable to parse latest release version %q", latestRelease)) | |
| } | |
| // Pad the latest version only. | |
| // | |
| // This assumes that the program in question doesn't patch older releases | |
| // (as is the case with gopls). If that assumption ever changes, we may need | |
| // to apply padding to older versions as well. | |
| versionsToPad := []semversion{parsedLatest} | |
| var maj, min, patch int | |
| for _, toPad := range versionsToPad { | |
| for majPadding := 0; majPadding <= padding.maj; majPadding++ { | |
| maj = toPad.major + majPadding | |
| for minPadding := 0; minPadding+majPadding <= padding.majmin; minPadding++ { | |
| if majPadding == 0 { | |
| min = toPad.minor + minPadding | |
| } else { | |
| min = minPadding | |
| } | |
| for patchPadding := 0; patchPadding <= padding.patch; patchPadding++ { | |
| releases := majPadding + minPadding + patchPadding | |
| if releases == 0 || releases > padding.releases { | |
| continue | |
| } | |
| if majPadding == 0 && minPadding == 0 { | |
| patch = toPad.patch + patchPadding | |
| } else { | |
| patch = patchPadding | |
| } | |
| v := fmt.Sprintf("v%d.%d.%d", maj, min, patch) | |
| if all[v] { | |
| // This guard is future proofing: we may have seen this version | |
| // before if we are ever padding something other than the latest | |
| // version. | |
| continue | |
| } | |
| versions = append(versions, v) | |
| // We may already have prereleases at this version. Don't pad | |
| // additional prereleases, under the assumption that we don't | |
| // typically have more than padding.pre prereleases. | |
| nextPrerelease := 0 | |
| for i, patt := range prereleasePatterns { | |
| pre := fmt.Sprintf("%s-%s", v, patt) | |
| if all[pre] { | |
| nextPrerelease = i + 1 | |
| } | |
| } | |
| for i := nextPrerelease; i < len(prereleasePatterns) && i < padding.pre; i++ { | |
| pre := fmt.Sprintf("%s-%s", v, prereleasePatterns[i]) | |
| versions = append(versions, pre) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| semver.Sort(versions) | |
| return versions | |
| } | |
| // version is a parsed semantic version. | |
| type semversion struct { | |
| major, minor, patch int | |
| pre string | |
| } | |
| // parseSemver attempts to parse semver components out of the provided semver | |
| // v. If v is not valid semver in canonical form, parseSemver returns _, _, _, | |
| // _, false. | |
| func parseSemver(v string) (_ semversion, ok bool) { | |
| var parsed semversion | |
| v, parsed.pre, _ = strings.Cut(v, "-") | |
| if _, err := fmt.Sscanf(v, "v%d.%d.%d", &parsed.major, &parsed.minor, &parsed.patch); err == nil { | |
| ok = true | |
| } | |
| return parsed, ok | |
| } |