blob: 3f8502cda6e50c64ec1522e66266f07be0f5ae3f [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanski8999faa2023-11-20 12:42:13 +01004// Package main implements 'stampgo', a tool to convert build status data into a
5// version.spec.Version proto, which is then embedded into a Go source file.
6package main
7
8import (
9 "bufio"
10 "flag"
11 "fmt"
12 "log"
13 "os"
14 "regexp"
15 "strconv"
16 "strings"
17
18 "github.com/coreos/go-semver/semver"
19 "google.golang.org/protobuf/proto"
20
21 "source.monogon.dev/version/spec"
22)
23
24var (
25 flagStatusFile string
26 flagOutFile string
27 flagImportpath string
28 flagProduct string
29)
30
31var (
32 rePrereleaseCommitOffset = regexp.MustCompile(`^dev([0-9]+)$`)
Serge Bazanski8999faa2023-11-20 12:42:13 +010033 rePrereleaseGitHash = regexp.MustCompile(`^g[0-9a-f]+$`)
34)
35
36func init() {
37 flag.StringVar(&flagStatusFile, "status_file", "", "path to bazel workspace status file")
38 flag.StringVar(&flagOutFile, "out_file", "version.go", "path to os-release output file")
39 flag.StringVar(&flagImportpath, "importpath", "", "importpath of generated source")
40 flag.StringVar(&flagProduct, "product", "", "product name within the monogon source tree")
41}
42
43// parseStatusFile reads and parses a Bazel build status file and returns
44// a string -> string map based on its contents.
45func parseStatusFile(path string) (map[string]string, error) {
46 f, err := os.Open(path)
47 if err != nil {
48 return nil, fmt.Errorf("open failed: %w", err)
49 }
50 defer f.Close()
51
52 res := make(map[string]string)
53 scanner := bufio.NewScanner(f)
54 for scanner.Scan() {
55 line := scanner.Text()
Jan Schär10670e52025-04-23 12:54:48 +000056 parts := strings.SplitN(line, " ", 2)
Serge Bazanski8999faa2023-11-20 12:42:13 +010057 if len(parts) != 2 {
58 return nil, fmt.Errorf("invalid line: %q", line)
59 }
60 k, v := parts[0], parts[1]
61 if k == "" {
62 return nil, fmt.Errorf("invalid line: %q", line)
63 }
64 if _, ok := res[k]; ok {
65 return nil, fmt.Errorf("repeated key %q", k)
66 }
67 res[k] = v
68 }
69 return res, nil
70}
71
72func main() {
73 flag.Parse()
74 if flagImportpath == "" {
75 log.Fatalf("Importpath must be set")
76 }
77 importpathParts := strings.Split(flagImportpath, "/")
78 packageName := importpathParts[len(importpathParts)-1]
79
80 values, err := parseStatusFile(flagStatusFile)
81 if err != nil {
82 log.Fatalf("Failed to read workspace status file: %v", err)
83 }
84
Jan Schärbddad352025-04-23 14:55:26 +000085 version := &spec.Version{}
Serge Bazanski8999faa2023-11-20 12:42:13 +010086
Jan Schärbddad352025-04-23 14:55:26 +000087 commitHash := values["STABLE_MONOGON_gitCommit"]
88 if commitHash != "" {
89 if len(commitHash) < 8 {
90 log.Fatalf("Git commit hash too short")
91 }
92 buildTreeState := spec.Version_GitInformation_BUILD_TREE_STATE_INVALID
93 switch values["STABLE_MONOGON_gitTreeState"] {
94 case "clean":
95 buildTreeState = spec.Version_GitInformation_BUILD_TREE_STATE_CLEAN
96 case "dirty":
97 buildTreeState = spec.Version_GitInformation_BUILD_TREE_STATE_DIRTY
98 default:
99 log.Fatalf("Invalid git tree state %q", values["STABLE_MONOGON_gitTreeState"])
100 }
101 version.GitInformation = &spec.Version_GitInformation{
Serge Bazanski8999faa2023-11-20 12:42:13 +0100102 CommitHash: commitHash[:8],
103 BuildTreeState: buildTreeState,
Jan Schärbddad352025-04-23 14:55:26 +0000104 }
Serge Bazanski8999faa2023-11-20 12:42:13 +0100105 }
106
107 productVersion := values["STABLE_MONOGON_"+flagProduct+"_version"]
108 if productVersion != "" {
109 if productVersion[0] != 'v' {
110 log.Fatalf("Invalid %s version %q: does not start with v", flagProduct, productVersion)
111 }
112 productVersion = productVersion[1:]
113 v, err := semver.NewVersion(productVersion)
114 if err != nil {
115 log.Fatalf("Invalid %s version %q: %v", flagProduct, productVersion, err)
116 }
117 // Parse prerelease strings (v1.2.3-foo-bar -> [foo, bar])
118 for _, el := range v.PreRelease.Slice() {
Serge Bazanski8999faa2023-11-20 12:42:13 +0100119 preCommitOffset := rePrereleaseCommitOffset.FindStringSubmatch(el)
Serge Bazanski8999faa2023-11-20 12:42:13 +0100120 preGitHash := rePrereleaseGitHash.FindStringSubmatch(el)
121 switch {
Jan Schärbddad352025-04-23 14:55:26 +0000122 case el == "":
123 // Skip empty slices which happens when there's a semver string with no
124 // prerelease data.
125 case el == "nostamp":
126 // Ignore field, we have it from the global monorepo state.
Serge Bazanski8999faa2023-11-20 12:42:13 +0100127 case preCommitOffset != nil:
128 offset, err := strconv.ParseUint(preCommitOffset[1], 10, 64)
129 if err != nil {
130 log.Fatalf("Invalid commit offset value: %v", err)
131 }
Jan Schärbddad352025-04-23 14:55:26 +0000132 if version.GitInformation == nil {
133 log.Fatalf("Have git offset but no git commit")
134 }
Serge Bazanski8999faa2023-11-20 12:42:13 +0100135 version.GitInformation.CommitsSinceRelease = offset
Jan Schärbddad352025-04-23 14:55:26 +0000136 case el == "dirty":
Serge Bazanski8999faa2023-11-20 12:42:13 +0100137 // Ignore field, we have it from the global monorepo state.
138 case preGitHash != nil:
139 // Ignore field, we have it from the global monorepo state.
140 default:
141 log.Fatalf("Invalid prerelease string %q (in %q)", el, productVersion)
142 }
143 }
144 version.Release = &spec.Version_Release{
145 Major: v.Major,
146 Minor: v.Minor,
147 Patch: v.Patch,
148 }
149 }
150
151 versionBytes, err := proto.Marshal(version)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200152 if err != nil {
153 log.Fatalf("failed to marshal version: %v", err)
154 }
Serge Bazanski8999faa2023-11-20 12:42:13 +0100155 literalBytes := make([]string, len(versionBytes))
156 for i, by := range versionBytes {
157 literalBytes[i] = fmt.Sprintf("0x%02x", by)
158 }
159
160 content := []string{
161 `// Generated by //version/stampgo`,
162 `package ` + packageName,
163 ``,
164 `import (`,
165 ` "fmt"`,
166 ` "os"`,
167 ``,
168 ` "google.golang.org/protobuf/proto"`,
169 ``,
170 ` "source.monogon.dev/version/spec"`,
171 `)`,
172 ``,
173 `var raw = []byte{`,
174 ` ` + strings.Join(literalBytes, ", ") + `,`,
175 `}`,
176 ``,
177 `var Version *spec.Version`,
178 ``,
179 `func init() {`,
180 ` var version spec.Version`,
181 ` if err := proto.Unmarshal(raw, &version); err != nil {`,
182 ` fmt.Fprintf(os.Stderr, "Invalid stamped version: %v\n", err)`,
183 ` }`,
184 ` Version = &version`,
185 `}`,
186 ``,
187 }
188
189 if err := os.WriteFile(flagOutFile, []byte(strings.Join(content, "\n")), 0644); err != nil {
190 log.Fatalf("Failed to write output file: %v", err)
191 }
192}