| // Package main implements 'stampgo', a tool to convert build status data into a |
| // version.spec.Version proto, which is then embedded into a Go source file. |
| package main |
| |
| import ( |
| "bufio" |
| "flag" |
| "fmt" |
| "log" |
| "os" |
| "regexp" |
| "strconv" |
| "strings" |
| |
| "github.com/coreos/go-semver/semver" |
| "google.golang.org/protobuf/proto" |
| |
| "source.monogon.dev/version/spec" |
| ) |
| |
| var ( |
| flagStatusFile string |
| flagOutFile string |
| flagImportpath string |
| flagProduct string |
| ) |
| |
| var ( |
| rePrereleaseCommitOffset = regexp.MustCompile(`^dev([0-9]+)$`) |
| rePrereleaseDirty = regexp.MustCompile(`^dirty$`) |
| rePrereleaseGitHash = regexp.MustCompile(`^g[0-9a-f]+$`) |
| ) |
| |
| func init() { |
| flag.StringVar(&flagStatusFile, "status_file", "", "path to bazel workspace status file") |
| flag.StringVar(&flagOutFile, "out_file", "version.go", "path to os-release output file") |
| flag.StringVar(&flagImportpath, "importpath", "", "importpath of generated source") |
| flag.StringVar(&flagProduct, "product", "", "product name within the monogon source tree") |
| } |
| |
| // parseStatusFile reads and parses a Bazel build status file and returns |
| // a string -> string map based on its contents. |
| func parseStatusFile(path string) (map[string]string, error) { |
| f, err := os.Open(path) |
| if err != nil { |
| return nil, fmt.Errorf("open failed: %w", err) |
| } |
| defer f.Close() |
| |
| res := make(map[string]string) |
| scanner := bufio.NewScanner(f) |
| for scanner.Scan() { |
| line := scanner.Text() |
| parts := strings.Split(line, " ") |
| if len(parts) != 2 { |
| return nil, fmt.Errorf("invalid line: %q", line) |
| } |
| k, v := parts[0], parts[1] |
| if k == "" { |
| return nil, fmt.Errorf("invalid line: %q", line) |
| } |
| if _, ok := res[k]; ok { |
| return nil, fmt.Errorf("repeated key %q", k) |
| } |
| res[k] = v |
| } |
| return res, nil |
| } |
| |
| func main() { |
| flag.Parse() |
| if flagImportpath == "" { |
| log.Fatalf("Importpath must be set") |
| } |
| importpathParts := strings.Split(flagImportpath, "/") |
| packageName := importpathParts[len(importpathParts)-1] |
| |
| values, err := parseStatusFile(flagStatusFile) |
| if err != nil { |
| log.Fatalf("Failed to read workspace status file: %v", err) |
| } |
| |
| commitHash := values["STABLE_MONOGON_gitCommit"] |
| if commitHash == "" { |
| log.Fatalf("No git commit in workspace status") |
| } |
| if len(commitHash) < 8 { |
| log.Fatalf("Git commit hash too short") |
| } |
| buildTreeState := spec.Version_GitInformation_BUILD_TREE_STATE_INVALID |
| switch values["STABLE_MONOGON_gitTreeState"] { |
| case "clean": |
| buildTreeState = spec.Version_GitInformation_BUILD_TREE_STATE_CLEAN |
| case "dirty": |
| buildTreeState = spec.Version_GitInformation_BUILD_TREE_STATE_DIRTY |
| default: |
| log.Fatalf("Invalid git tree state %q", values["STABLE_MONOGON_gitTreeState"]) |
| } |
| |
| version := &spec.Version{ |
| GitInformation: &spec.Version_GitInformation{ |
| CommitHash: commitHash[:8], |
| BuildTreeState: buildTreeState, |
| }, |
| } |
| |
| productVersion := values["STABLE_MONOGON_"+flagProduct+"_version"] |
| if productVersion != "" { |
| if productVersion[0] != 'v' { |
| log.Fatalf("Invalid %s version %q: does not start with v", flagProduct, productVersion) |
| } |
| productVersion = productVersion[1:] |
| v, err := semver.NewVersion(productVersion) |
| if err != nil { |
| log.Fatalf("Invalid %s version %q: %v", flagProduct, productVersion, err) |
| } |
| // Parse prerelease strings (v1.2.3-foo-bar -> [foo, bar]) |
| for _, el := range v.PreRelease.Slice() { |
| // Skip empty slices which happens when there's a semver string with no |
| // prerelease data. |
| if el == "" { |
| continue |
| } |
| preCommitOffset := rePrereleaseCommitOffset.FindStringSubmatch(el) |
| preDirty := rePrereleaseDirty.FindStringSubmatch(el) |
| preGitHash := rePrereleaseGitHash.FindStringSubmatch(el) |
| switch { |
| case preCommitOffset != nil: |
| offset, err := strconv.ParseUint(preCommitOffset[1], 10, 64) |
| if err != nil { |
| log.Fatalf("Invalid commit offset value: %v", err) |
| } |
| version.GitInformation.CommitsSinceRelease = offset |
| case preDirty != nil: |
| // Ignore field, we have it from the global monorepo state. |
| case preGitHash != nil: |
| // Ignore field, we have it from the global monorepo state. |
| default: |
| log.Fatalf("Invalid prerelease string %q (in %q)", el, productVersion) |
| } |
| } |
| version.Release = &spec.Version_Release{ |
| Major: v.Major, |
| Minor: v.Minor, |
| Patch: v.Patch, |
| } |
| } |
| |
| versionBytes, err := proto.Marshal(version) |
| if err != nil { |
| log.Fatalf("failed to marshal version: %v", err) |
| } |
| literalBytes := make([]string, len(versionBytes)) |
| for i, by := range versionBytes { |
| literalBytes[i] = fmt.Sprintf("0x%02x", by) |
| } |
| |
| content := []string{ |
| `// Generated by //version/stampgo`, |
| `package ` + packageName, |
| ``, |
| `import (`, |
| ` "fmt"`, |
| ` "os"`, |
| ``, |
| ` "google.golang.org/protobuf/proto"`, |
| ``, |
| ` "source.monogon.dev/version/spec"`, |
| `)`, |
| ``, |
| `var raw = []byte{`, |
| ` ` + strings.Join(literalBytes, ", ") + `,`, |
| `}`, |
| ``, |
| `var Version *spec.Version`, |
| ``, |
| `func init() {`, |
| ` var version spec.Version`, |
| ` if err := proto.Unmarshal(raw, &version); err != nil {`, |
| ` fmt.Fprintf(os.Stderr, "Invalid stamped version: %v\n", err)`, |
| ` }`, |
| ` Version = &version`, |
| `}`, |
| ``, |
| } |
| |
| if err := os.WriteFile(flagOutFile, []byte(strings.Join(content, "\n")), 0644); err != nil { |
| log.Fatalf("Failed to write output file: %v", err) |
| } |
| } |