blob: 3f8502cda6e50c64ec1522e66266f07be0f5ae3f [file] [log] [blame]
// Copyright The Monogon Project Authors.
// SPDX-License-Identifier: Apache-2.0
// 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]+)$`)
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.SplitN(line, " ", 2)
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)
}
version := &spec.Version{}
commitHash := values["STABLE_MONOGON_gitCommit"]
if commitHash != "" {
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.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() {
preCommitOffset := rePrereleaseCommitOffset.FindStringSubmatch(el)
preGitHash := rePrereleaseGitHash.FindStringSubmatch(el)
switch {
case el == "":
// Skip empty slices which happens when there's a semver string with no
// prerelease data.
case el == "nostamp":
// Ignore field, we have it from the global monorepo state.
case preCommitOffset != nil:
offset, err := strconv.ParseUint(preCommitOffset[1], 10, 64)
if err != nil {
log.Fatalf("Invalid commit offset value: %v", err)
}
if version.GitInformation == nil {
log.Fatalf("Have git offset but no git commit")
}
version.GitInformation.CommitsSinceRelease = offset
case el == "dirty":
// 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)
}
}