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