treewide: build product info
This change adds the type definition and generator for product info,
which will be added to the OCI OS image to provide information about the
contents.
Here is an example product info:
{
"id": "metropolis-node",
"name": "Metropolis Node",
"version": "0.1.0-dev1059",
"variant": "x86_64-race",
"commit_hash": "56248c1c1d5039bdf3c1043ade88f3f158ceb52b",
"commit_date": "2025-05-08T18:26:46+00:00",
"build_tree_dirty": true,
"components": [
{"id": "linux", "version": "6.12.15"},
{"id": "kubernetes", "version": "1.32.0"}
]
}
The product info has the same inputs and a similar purpose as the
os-release file, so they are both generated by the same build action.
Change-Id: I89d453f2d72ac9df49e404f46381cd594534f800
Reviewed-on: https://review.monogon.dev/c/monogon/+/4192
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/osbase/build/genosrelease/BUILD.bazel b/osbase/build/genosrelease/BUILD.bazel
deleted file mode 100644
index f845cae..0000000
--- a/osbase/build/genosrelease/BUILD.bazel
+++ /dev/null
@@ -1,18 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-
-go_library(
- name = "genosrelease_lib",
- srcs = ["main.go"],
- importpath = "source.monogon.dev/osbase/build/genosrelease",
- visibility = ["//visibility:private"],
- deps = ["@com_github_joho_godotenv//:godotenv"],
-)
-
-go_binary(
- name = "genosrelease",
- embed = [":genosrelease_lib"],
- visibility = [
- "//metropolis/installer:__subpackages__",
- "//metropolis/node:__subpackages__",
- ],
-)
diff --git a/osbase/build/genosrelease/defs.bzl b/osbase/build/genosrelease/defs.bzl
deleted file mode 100644
index 2e7c613..0000000
--- a/osbase/build/genosrelease/defs.bzl
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright 2020 The Monogon Project Authors.
-#
-# SPDX-License-Identifier: Apache-2.0
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-def _os_release_impl(ctx):
- ctx.actions.run(
- mnemonic = "GenOSRelease",
- progress_message = "Generating os-release",
- inputs = [ctx.info_file],
- outputs = [ctx.outputs.out],
- executable = ctx.executable._genosrelease,
- arguments = [
- "-status_file",
- ctx.info_file.path,
- "-out_file",
- ctx.outputs.out.path,
- "-stamp_var",
- ctx.attr.stamp_var,
- "-name",
- ctx.attr.os_name,
- "-id",
- ctx.attr.os_id,
- ],
- )
-
-os_release = rule(
- implementation = _os_release_impl,
- attrs = {
- "os_name": attr.string(mandatory = True),
- "os_id": attr.string(mandatory = True),
- "stamp_var": attr.string(mandatory = True),
- "_genosrelease": attr.label(
- default = Label("//osbase/build/genosrelease"),
- cfg = "exec",
- executable = True,
- allow_files = True,
- ),
- },
- outputs = {
- "out": "os-release",
- },
-)
diff --git a/osbase/build/genosrelease/main.go b/osbase/build/genosrelease/main.go
deleted file mode 100644
index 8d061f9..0000000
--- a/osbase/build/genosrelease/main.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright The Monogon Project Authors.
-// SPDX-License-Identifier: Apache-2.0
-
-// genosrelease provides rudimentary support to generate os-release files
-// following the freedesktop spec from arguments and stamping
-//
-// https://www.freedesktop.org/software/systemd/man/os-release.html
-package main
-
-import (
- "flag"
- "fmt"
- "os"
- "strings"
-
- "github.com/joho/godotenv"
-)
-
-var (
- flagStatusFile = flag.String("status_file", "", "path to bazel workspace status file")
- flagOutFile = flag.String("out_file", "os-release", "path to os-release output file")
- flagStampVar = flag.String("stamp_var", "", "variable to use as version from the workspace status file")
- flagName = flag.String("name", "", "name parameter (see freedesktop spec)")
- flagID = flag.String("id", "", "id parameter (see freedesktop spec)")
-)
-
-func main() {
- flag.Parse()
- statusFileContent, err := os.ReadFile(*flagStatusFile)
- if err != nil {
- fmt.Printf("Failed to open bazel workspace status file: %v\n", err)
- os.Exit(1)
- }
- statusVars := make(map[string]string)
- for _, line := range strings.Split(string(statusFileContent), "\n") {
- line = strings.TrimSpace(line)
- parts := strings.Fields(line)
- if len(parts) != 2 {
- continue
- }
- statusVars[parts[0]] = parts[1]
- }
-
- version, ok := statusVars[*flagStampVar]
- if !ok {
- fmt.Printf("%v key not set in bazel workspace status file\n", *flagStampVar)
- os.Exit(1)
- }
- // As specified by https://www.freedesktop.org/software/systemd/man/os-release.html
- osReleaseVars := map[string]string{
- "NAME": *flagName,
- "ID": *flagID,
- "VERSION": version,
- "VERSION_ID": version,
- "PRETTY_NAME": *flagName + " " + version,
- }
- osReleaseContent, err := godotenv.Marshal(osReleaseVars)
- if err != nil {
- fmt.Printf("Failed to encode os-release file: %v\n", err)
- os.Exit(1)
- }
- if err := os.WriteFile(*flagOutFile, []byte(osReleaseContent+"\n"), 0644); err != nil {
- fmt.Printf("Failed to write os-release file: %v\n", err)
- os.Exit(1)
- }
-}
diff --git a/osbase/build/genproductinfo/BUILD.bazel b/osbase/build/genproductinfo/BUILD.bazel
new file mode 100644
index 0000000..036d448
--- /dev/null
+++ b/osbase/build/genproductinfo/BUILD.bazel
@@ -0,0 +1,28 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "genproductinfo_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/osbase/build/genproductinfo",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//osbase/oci/osimage",
+ "@com_github_joho_godotenv//:godotenv",
+ ],
+)
+
+go_binary(
+ name = "genproductinfo",
+ embed = [":genproductinfo_lib"],
+ visibility = ["//visibility:public"],
+)
+
+config_setting(
+ name = "flag_debug",
+ values = {"compilation_mode": "dbg"},
+)
+
+config_setting(
+ name = "flag_race",
+ flag_values = {"@io_bazel_rules_go//go/config:race": "True"},
+)
diff --git a/osbase/build/genproductinfo/defs.bzl b/osbase/build/genproductinfo/defs.bzl
new file mode 100644
index 0000000..c484bbb
--- /dev/null
+++ b/osbase/build/genproductinfo/defs.bzl
@@ -0,0 +1,68 @@
+# Copyright The Monogon Project Authors.
+# SPDX-License-Identifier: Apache-2.0
+
+def _product_info_impl(ctx):
+ product_info_file = ctx.actions.declare_file(ctx.label.name + ".json")
+ args = ctx.actions.args()
+ args.add("-status_file", ctx.info_file)
+ args.add("-product_info_file", product_info_file)
+ args.add("-os_release_file", ctx.outputs.out_os_release)
+ args.add("-stamp_var", ctx.attr.stamp_var)
+ args.add("-name", ctx.attr.os_name)
+ args.add("-id", ctx.attr.os_id)
+ args.add("-architecture", ctx.attr.architecture)
+ args.add("-build_flags", "-".join(ctx.attr.build_flags))
+ args.add_all(ctx.attr.components, before_each = "-component")
+ ctx.actions.run(
+ mnemonic = "GenProductInfo",
+ progress_message = "Generating product info",
+ inputs = [ctx.info_file],
+ outputs = [product_info_file, ctx.outputs.out_os_release],
+ executable = ctx.executable._genproductinfo,
+ arguments = [args],
+ )
+ return [DefaultInfo(files = depset([product_info_file]))]
+
+_product_info = rule(
+ implementation = _product_info_impl,
+ attrs = {
+ "os_name": attr.string(mandatory = True),
+ "os_id": attr.string(mandatory = True),
+ "stamp_var": attr.string(mandatory = True),
+ "components": attr.string_list(),
+ "out_os_release": attr.output(
+ mandatory = True,
+ doc = """Output, contains the os-release file.""",
+ ),
+ "architecture": attr.string(mandatory = True),
+ "build_flags": attr.string_list(),
+ "_genproductinfo": attr.label(
+ default = ":genproductinfo",
+ cfg = "exec",
+ executable = True,
+ allow_files = True,
+ ),
+ },
+)
+
+def _product_info_macro_impl(**kwargs):
+ _product_info(
+ architecture = select({
+ "@platforms//cpu:x86_64": "x86_64",
+ "@platforms//cpu:aarch64": "aarch64",
+ }),
+ build_flags = select({
+ Label(":flag_debug"): ["debug"],
+ "//conditions:default": [],
+ }) + select({
+ Label(":flag_race"): ["race"],
+ "//conditions:default": [],
+ }),
+ **kwargs
+ )
+
+product_info = macro(
+ inherit_attrs = _product_info,
+ attrs = {"architecture": None, "build_flags": None},
+ implementation = _product_info_macro_impl,
+)
diff --git a/osbase/build/genproductinfo/main.go b/osbase/build/genproductinfo/main.go
new file mode 100644
index 0000000..e4b566d
--- /dev/null
+++ b/osbase/build/genproductinfo/main.go
@@ -0,0 +1,146 @@
+// Copyright The Monogon Project Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// genproductinfo generates a product info JSON file from arguments and
+// stamping. Additionally, it generates an os-release file following the
+// freedesktop spec from the same information.
+//
+// https://www.freedesktop.org/software/systemd/man/os-release.html
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "regexp"
+ "strings"
+
+ "github.com/joho/godotenv"
+
+ "source.monogon.dev/osbase/oci/osimage"
+)
+
+var (
+ flagStatusFile = flag.String("status_file", "", "path to bazel workspace status file")
+ flagProductInfoFile = flag.String("product_info_file", "product-info", "path to product info output file")
+ flagOSReleaseFile = flag.String("os_release_file", "os-release", "path to os-release output file")
+ flagStampVar = flag.String("stamp_var", "", "variable to use as version from the workspace status file")
+ flagName = flag.String("name", "", "name parameter (see freedesktop spec)")
+ flagID = flag.String("id", "", "id parameter (see freedesktop spec)")
+ flagArchitecture = flag.String("architecture", "", "CPU architecture")
+ flagBuildFlags = flag.String("build_flags", "", "build flags joined by '-'")
+)
+
+var (
+ rePrereleaseGitHash = regexp.MustCompile(`^g[0-9a-f]+$`)
+)
+
+func versionWithoutGitInfo(version string) string {
+ version, metadata, hasMetadata := strings.Cut(version, "+")
+ version, prerelease, hasPrerelease := strings.Cut(version, "-")
+ if hasPrerelease {
+ var filteredParts []string
+ for part := range strings.SplitSeq(prerelease, ".") {
+ switch {
+ case part == "dirty":
+ // Ignore field.
+ case rePrereleaseGitHash.FindStringSubmatch(part) != nil:
+ // Ignore field.
+ default:
+ filteredParts = append(filteredParts, part)
+ }
+ }
+ if len(filteredParts) != 0 {
+ version = version + "-" + strings.Join(filteredParts, ".")
+ }
+ }
+ if hasMetadata {
+ version = version + "+" + metadata
+ }
+ return version
+}
+
+func main() {
+ var componentIDs []string
+ flag.Func("component", "ID of a component", func(component string) error {
+ componentIDs = append(componentIDs, component)
+ return nil
+ })
+ flag.Parse()
+
+ statusFileContent, err := os.ReadFile(*flagStatusFile)
+ if err != nil {
+ log.Fatalf("Failed to open bazel workspace status file: %v", err)
+ }
+ statusVars := make(map[string]string)
+ for line := range strings.SplitSeq(string(statusFileContent), "\n") {
+ if line == "" {
+ continue
+ }
+ key, value, ok := strings.Cut(line, " ")
+ if !ok {
+ log.Fatalf("Invalid line in status file: %q", line)
+ }
+ statusVars[key] = value
+ }
+
+ version, ok := statusVars[*flagStampVar]
+ if !ok {
+ log.Fatalf("%s key not set in bazel workspace status file", *flagStampVar)
+ }
+
+ var components []osimage.Component
+ for _, id := range componentIDs {
+ versionKey := fmt.Sprintf("STABLE_MONOGON_componentVersion_%s", id)
+ version, ok := statusVars[versionKey]
+ if !ok {
+ log.Fatalf("%s key not set in bazel workspace status file", versionKey)
+ }
+ components = append(components, osimage.Component{
+ ID: id,
+ Version: version,
+ })
+ }
+
+ variant := *flagArchitecture
+ if *flagBuildFlags != "" {
+ variant = variant + "-" + *flagBuildFlags
+ }
+
+ productInfo := osimage.ProductInfo{
+ ID: *flagID,
+ Name: *flagName,
+ Version: versionWithoutGitInfo(version),
+ Variant: variant,
+ CommitHash: statusVars["STABLE_MONOGON_gitCommit"],
+ CommitDate: statusVars["STABLE_MONOGON_gitCommitDate"],
+ BuildTreeDirty: statusVars["STABLE_MONOGON_gitTreeState"] == "dirty",
+ Components: components,
+ }
+ productInfoBytes, err := json.MarshalIndent(productInfo, "", "\t")
+ if err != nil {
+ log.Fatalf("Failed to marshal OS image config: %v", err)
+ }
+ productInfoBytes = append(productInfoBytes, '\n')
+ if err := os.WriteFile(*flagProductInfoFile, productInfoBytes, 0644); err != nil {
+ log.Fatalf("Failed to write product info file: %v", err)
+ }
+
+ // As specified by https://www.freedesktop.org/software/systemd/man/os-release.html
+ osReleaseVars := map[string]string{
+ "NAME": *flagName,
+ "ID": *flagID,
+ "VERSION": version,
+ "VERSION_ID": version,
+ "PRETTY_NAME": *flagName + " " + version,
+ }
+ osReleaseContent, err := godotenv.Marshal(osReleaseVars)
+ if err != nil {
+ log.Fatalf("Failed to encode os-release file: %v", err)
+ }
+ if err := os.WriteFile(*flagOSReleaseFile, []byte(osReleaseContent+"\n"), 0644); err != nil {
+ log.Fatalf("Failed to write os-release file: %v", err)
+ }
+}