| Jan Schär | e6c0c32 | 2025-05-12 16:14:25 +0000 | [diff] [blame] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| 4 | // genproductinfo generates a product info JSON file from arguments and |
| 5 | // stamping. Additionally, it generates an os-release file following the |
| 6 | // freedesktop spec from the same information. |
| 7 | // |
| 8 | // https://www.freedesktop.org/software/systemd/man/os-release.html |
| 9 | package main |
| 10 | |
| 11 | import ( |
| 12 | "encoding/json" |
| 13 | "flag" |
| 14 | "fmt" |
| 15 | "log" |
| 16 | "os" |
| 17 | "regexp" |
| 18 | "strings" |
| 19 | |
| 20 | "github.com/joho/godotenv" |
| 21 | |
| 22 | "source.monogon.dev/osbase/oci/osimage" |
| 23 | ) |
| 24 | |
| 25 | var ( |
| 26 | flagStatusFile = flag.String("status_file", "", "path to bazel workspace status file") |
| 27 | flagProductInfoFile = flag.String("product_info_file", "product-info", "path to product info output file") |
| 28 | flagOSReleaseFile = flag.String("os_release_file", "os-release", "path to os-release output file") |
| 29 | flagStampVar = flag.String("stamp_var", "", "variable to use as version from the workspace status file") |
| 30 | flagName = flag.String("name", "", "name parameter (see freedesktop spec)") |
| 31 | flagID = flag.String("id", "", "id parameter (see freedesktop spec)") |
| 32 | flagArchitecture = flag.String("architecture", "", "CPU architecture") |
| 33 | flagBuildFlags = flag.String("build_flags", "", "build flags joined by '-'") |
| Jan Schär | d4309bb | 2025-07-18 10:13:22 +0200 | [diff] [blame^] | 34 | flagPlatformOS = flag.String("platform_os", "", "platform OS") |
| Jan Schär | e6c0c32 | 2025-05-12 16:14:25 +0000 | [diff] [blame] | 35 | ) |
| 36 | |
| 37 | var ( |
| 38 | rePrereleaseGitHash = regexp.MustCompile(`^g[0-9a-f]+$`) |
| 39 | ) |
| 40 | |
| 41 | func versionWithoutGitInfo(version string) string { |
| 42 | version, metadata, hasMetadata := strings.Cut(version, "+") |
| 43 | version, prerelease, hasPrerelease := strings.Cut(version, "-") |
| 44 | if hasPrerelease { |
| 45 | var filteredParts []string |
| 46 | for part := range strings.SplitSeq(prerelease, ".") { |
| 47 | switch { |
| 48 | case part == "dirty": |
| 49 | // Ignore field. |
| 50 | case rePrereleaseGitHash.FindStringSubmatch(part) != nil: |
| 51 | // Ignore field. |
| 52 | default: |
| 53 | filteredParts = append(filteredParts, part) |
| 54 | } |
| 55 | } |
| 56 | if len(filteredParts) != 0 { |
| 57 | version = version + "-" + strings.Join(filteredParts, ".") |
| 58 | } |
| 59 | } |
| 60 | if hasMetadata { |
| 61 | version = version + "+" + metadata |
| 62 | } |
| 63 | return version |
| 64 | } |
| 65 | |
| 66 | func main() { |
| 67 | var componentIDs []string |
| 68 | flag.Func("component", "ID of a component", func(component string) error { |
| 69 | componentIDs = append(componentIDs, component) |
| 70 | return nil |
| 71 | }) |
| 72 | flag.Parse() |
| 73 | |
| 74 | statusFileContent, err := os.ReadFile(*flagStatusFile) |
| 75 | if err != nil { |
| 76 | log.Fatalf("Failed to open bazel workspace status file: %v", err) |
| 77 | } |
| 78 | statusVars := make(map[string]string) |
| 79 | for line := range strings.SplitSeq(string(statusFileContent), "\n") { |
| 80 | if line == "" { |
| 81 | continue |
| 82 | } |
| 83 | key, value, ok := strings.Cut(line, " ") |
| 84 | if !ok { |
| 85 | log.Fatalf("Invalid line in status file: %q", line) |
| 86 | } |
| 87 | statusVars[key] = value |
| 88 | } |
| 89 | |
| 90 | version, ok := statusVars[*flagStampVar] |
| 91 | if !ok { |
| 92 | log.Fatalf("%s key not set in bazel workspace status file", *flagStampVar) |
| 93 | } |
| 94 | |
| 95 | var components []osimage.Component |
| 96 | for _, id := range componentIDs { |
| 97 | versionKey := fmt.Sprintf("STABLE_MONOGON_componentVersion_%s", id) |
| 98 | version, ok := statusVars[versionKey] |
| 99 | if !ok { |
| 100 | log.Fatalf("%s key not set in bazel workspace status file", versionKey) |
| 101 | } |
| 102 | components = append(components, osimage.Component{ |
| 103 | ID: id, |
| 104 | Version: version, |
| 105 | }) |
| 106 | } |
| 107 | |
| 108 | variant := *flagArchitecture |
| 109 | if *flagBuildFlags != "" { |
| 110 | variant = variant + "-" + *flagBuildFlags |
| 111 | } |
| 112 | |
| 113 | productInfo := osimage.ProductInfo{ |
| 114 | ID: *flagID, |
| 115 | Name: *flagName, |
| 116 | Version: versionWithoutGitInfo(version), |
| 117 | Variant: variant, |
| 118 | CommitHash: statusVars["STABLE_MONOGON_gitCommit"], |
| 119 | CommitDate: statusVars["STABLE_MONOGON_gitCommitDate"], |
| 120 | BuildTreeDirty: statusVars["STABLE_MONOGON_gitTreeState"] == "dirty", |
| Jan Schär | d4309bb | 2025-07-18 10:13:22 +0200 | [diff] [blame^] | 121 | PlatformOS: *flagPlatformOS, |
| Jan Schär | e6c0c32 | 2025-05-12 16:14:25 +0000 | [diff] [blame] | 122 | Components: components, |
| 123 | } |
| 124 | productInfoBytes, err := json.MarshalIndent(productInfo, "", "\t") |
| 125 | if err != nil { |
| 126 | log.Fatalf("Failed to marshal OS image config: %v", err) |
| 127 | } |
| 128 | productInfoBytes = append(productInfoBytes, '\n') |
| 129 | if err := os.WriteFile(*flagProductInfoFile, productInfoBytes, 0644); err != nil { |
| 130 | log.Fatalf("Failed to write product info file: %v", err) |
| 131 | } |
| 132 | |
| 133 | // As specified by https://www.freedesktop.org/software/systemd/man/os-release.html |
| 134 | osReleaseVars := map[string]string{ |
| 135 | "NAME": *flagName, |
| 136 | "ID": *flagID, |
| 137 | "VERSION": version, |
| 138 | "VERSION_ID": version, |
| 139 | "PRETTY_NAME": *flagName + " " + version, |
| 140 | } |
| 141 | osReleaseContent, err := godotenv.Marshal(osReleaseVars) |
| 142 | if err != nil { |
| 143 | log.Fatalf("Failed to encode os-release file: %v", err) |
| 144 | } |
| 145 | if err := os.WriteFile(*flagOSReleaseFile, []byte(osReleaseContent+"\n"), 0644); err != nil { |
| 146 | log.Fatalf("Failed to write os-release file: %v", err) |
| 147 | } |
| 148 | } |