blob: 164e08af65dc028ed6e3466b7bd9f37bf7968b4d [file] [log] [blame]
Serge Bazanski8999faa2023-11-20 12:42:13 +01001// Package main implements 'stampgo', a tool to convert build status data into a
2// version.spec.Version proto, which is then embedded into a Go source file.
3package main
4
5import (
6 "bufio"
7 "flag"
8 "fmt"
9 "log"
10 "os"
11 "regexp"
12 "strconv"
13 "strings"
14
15 "github.com/coreos/go-semver/semver"
16 "google.golang.org/protobuf/proto"
17
18 "source.monogon.dev/version/spec"
19)
20
21var (
22 flagStatusFile string
23 flagOutFile string
24 flagImportpath string
25 flagProduct string
26)
27
28var (
29 rePrereleaseCommitOffset = regexp.MustCompile(`^dev([0-9]+)$`)
30 rePrereleaseDirty = regexp.MustCompile(`^dirty$`)
31 rePrereleaseGitHash = regexp.MustCompile(`^g[0-9a-f]+$`)
32)
33
34func init() {
35 flag.StringVar(&flagStatusFile, "status_file", "", "path to bazel workspace status file")
36 flag.StringVar(&flagOutFile, "out_file", "version.go", "path to os-release output file")
37 flag.StringVar(&flagImportpath, "importpath", "", "importpath of generated source")
38 flag.StringVar(&flagProduct, "product", "", "product name within the monogon source tree")
39}
40
41// parseStatusFile reads and parses a Bazel build status file and returns
42// a string -> string map based on its contents.
43func parseStatusFile(path string) (map[string]string, error) {
44 f, err := os.Open(path)
45 if err != nil {
46 return nil, fmt.Errorf("open failed: %w", err)
47 }
48 defer f.Close()
49
50 res := make(map[string]string)
51 scanner := bufio.NewScanner(f)
52 for scanner.Scan() {
53 line := scanner.Text()
54 parts := strings.Split(line, " ")
55 if len(parts) != 2 {
56 return nil, fmt.Errorf("invalid line: %q", line)
57 }
58 k, v := parts[0], parts[1]
59 if k == "" {
60 return nil, fmt.Errorf("invalid line: %q", line)
61 }
62 if _, ok := res[k]; ok {
63 return nil, fmt.Errorf("repeated key %q", k)
64 }
65 res[k] = v
66 }
67 return res, nil
68}
69
70func main() {
71 flag.Parse()
72 if flagImportpath == "" {
73 log.Fatalf("Importpath must be set")
74 }
75 importpathParts := strings.Split(flagImportpath, "/")
76 packageName := importpathParts[len(importpathParts)-1]
77
78 values, err := parseStatusFile(flagStatusFile)
79 if err != nil {
80 log.Fatalf("Failed to read workspace status file: %v", err)
81 }
82
83 commitHash := values["STABLE_MONOGON_gitCommit"]
84 if commitHash == "" {
85 log.Fatalf("No git commit in workspace status")
86 }
87 if len(commitHash) < 8 {
88 log.Fatalf("Git commit hash too short")
89 }
90 buildTreeState := spec.Version_GitInformation_BUILD_TREE_STATE_INVALID
91 switch values["STABLE_MONOGON_gitTreeState"] {
92 case "clean":
93 buildTreeState = spec.Version_GitInformation_BUILD_TREE_STATE_CLEAN
94 case "dirty":
95 buildTreeState = spec.Version_GitInformation_BUILD_TREE_STATE_DIRTY
96 default:
97 log.Fatalf("Invalid git tree state %q", values["STABLE_MONOGON_gitTreeState"])
98 }
99
100 version := &spec.Version{
101 GitInformation: &spec.Version_GitInformation{
102 CommitHash: commitHash[:8],
103 BuildTreeState: buildTreeState,
104 },
105 }
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() {
119 // Skip empty slices which happens when there's a semver string with no
120 // prerelease data.
121 if el == "" {
122 continue
123 }
124 preCommitOffset := rePrereleaseCommitOffset.FindStringSubmatch(el)
125 preDirty := rePrereleaseDirty.FindStringSubmatch(el)
126 preGitHash := rePrereleaseGitHash.FindStringSubmatch(el)
127 switch {
128 case preCommitOffset != nil:
129 offset, err := strconv.ParseUint(preCommitOffset[1], 10, 64)
130 if err != nil {
131 log.Fatalf("Invalid commit offset value: %v", err)
132 }
133 version.GitInformation.CommitsSinceRelease = offset
134 case preDirty != nil:
135 // Ignore field, we have it from the global monorepo state.
136 case preGitHash != nil:
137 // Ignore field, we have it from the global monorepo state.
138 default:
139 log.Fatalf("Invalid prerelease string %q (in %q)", el, productVersion)
140 }
141 }
142 version.Release = &spec.Version_Release{
143 Major: v.Major,
144 Minor: v.Minor,
145 Patch: v.Patch,
146 }
147 }
148
149 versionBytes, err := proto.Marshal(version)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200150 if err != nil {
151 log.Fatalf("failed to marshal version: %v", err)
152 }
Serge Bazanski8999faa2023-11-20 12:42:13 +0100153 literalBytes := make([]string, len(versionBytes))
154 for i, by := range versionBytes {
155 literalBytes[i] = fmt.Sprintf("0x%02x", by)
156 }
157
158 content := []string{
159 `// Generated by //version/stampgo`,
160 `package ` + packageName,
161 ``,
162 `import (`,
163 ` "fmt"`,
164 ` "os"`,
165 ``,
166 ` "google.golang.org/protobuf/proto"`,
167 ``,
168 ` "source.monogon.dev/version/spec"`,
169 `)`,
170 ``,
171 `var raw = []byte{`,
172 ` ` + strings.Join(literalBytes, ", ") + `,`,
173 `}`,
174 ``,
175 `var Version *spec.Version`,
176 ``,
177 `func init() {`,
178 ` var version spec.Version`,
179 ` if err := proto.Unmarshal(raw, &version); err != nil {`,
180 ` fmt.Fprintf(os.Stderr, "Invalid stamped version: %v\n", err)`,
181 ` }`,
182 ` Version = &version`,
183 `}`,
184 ``,
185 }
186
187 if err := os.WriteFile(flagOutFile, []byte(strings.Join(content, "\n")), 0644); err != nil {
188 log.Fatalf("Failed to write output file: %v", err)
189 }
190}