blob: 34309f3a81515eb99a7c616824eaf8cf5214ebdc [file] [log] [blame]
Serge Bazanskia6a03922023-11-13 19:57:48 +01001#!/usr/bin/env python3
2"""Workspace status script used for build stamping."""
3
4# Treat this script as shell code, but with Python syntax. We want to remain as
5# simple as possible, and absolutely never use any non-standard Python library.
6# This script should be able to run on any 'modern' Linux distribution with
7# Python 3.8 or newer.
8
9# The following versioning concepts apply:
10# 1. Version numbers follow the Semantic Versioning 2.0 spec.
11# 2. Git tags in the form `<product>-vX.Y.Z` will be used as a basis for
12# versioning a build. If the currently built release is exactly the same as
Serge Bazanskif42e3642023-11-28 16:25:25 +010013# such a tag, it will be versioned at vX.Y.Z. Otherwise, a devNNN prerelease
14# identifier will be appended to signify the amount of commits since the
15# release.
Serge Bazanskia6a03922023-11-13 19:57:48 +010016# 3. Product git tags are only made up of a major/minor/patch version.
17# Prerelease and build tags are assigned by the build system and this
18# script, Git tags have no influence on them.
19# 4. 'Products' are release numbering trains within the Monogon monorepo. This
20# means there is no such thing as a 'version' for the monorepo by itself,
21# only within the context of some product.
22
Jan Schärbddad352025-04-23 14:55:26 +000023import argparse
Serge Bazanskia6a03922023-11-13 19:57:48 +010024from dataclasses import dataclass
Serge Bazanskia6a03922023-11-13 19:57:48 +010025import re
26import subprocess
Serge Bazanskia6a03922023-11-13 19:57:48 +010027
28from typing import Optional
29
30
Jan Schärbddad352025-04-23 14:55:26 +000031parser = argparse.ArgumentParser()
32parser.add_argument("--nostamp", action="store_true")
33args = parser.parse_args()
34
Serge Bazanskia6a03922023-11-13 19:57:48 +010035# Variables to output. These will be printed to stdout at the end of the script
36# runtime, sorted by key.
37variables: dict[str, str] = {}
38
Jan Schärbddad352025-04-23 14:55:26 +000039git_tree_state: str = "unknown"
Serge Bazanskia6a03922023-11-13 19:57:48 +010040
Jan Schärbddad352025-04-23 14:55:26 +000041if not args.nostamp:
42 # Git build tree status: clean or dirty.
43 git_tree_state = "clean"
44 git_status = subprocess.check_output(["git", "status", "--porcelain"])
45 if git_status.decode().strip() != "":
46 git_tree_state = "dirty"
Serge Bazanskia6a03922023-11-13 19:57:48 +010047
Jan Schärbddad352025-04-23 14:55:26 +000048 # Git commit hash.
49 git_commit: str = (
50 subprocess.check_output(["git", "rev-parse", "HEAD^{commit}"]).decode().strip()
51 )
52
53 # Git commit date.
54 git_commit_date: str = (
55 subprocess.check_output(["git", "show", "--pretty=format:%cI", "--no-patch", "HEAD"]).decode().strip()
56 )
Jan Schär0cbf51a2025-04-23 10:21:17 +000057
Serge Bazanskia6a03922023-11-13 19:57:48 +010058# Git tags pointing at this commit.
59git_tags_b: [bytes] = subprocess.check_output(
Jan Schärdb3866a2024-04-08 17:33:45 +020060 ["git", "tag", "--sort=-version:refname", "--points-at", "HEAD"]
Serge Bazanskia6a03922023-11-13 19:57:48 +010061).split(b"\n")
62git_tags: [str] = [t.decode().strip() for t in git_tags_b if t.decode().strip() != ""]
63
Jan Schärbddad352025-04-23 14:55:26 +000064if not args.nostamp:
65 variables["STABLE_MONOGON_gitCommit"] = git_commit
Jan Schäre6c0c322025-05-12 16:14:25 +000066 variables["STABLE_MONOGON_gitCommitDate"] = git_commit_date
Jan Schärbddad352025-04-23 14:55:26 +000067 variables["STABLE_MONOGON_gitTreeState"] = git_tree_state
Serge Bazanskia6a03922023-11-13 19:57:48 +010068
Jan Schärbddad352025-04-23 14:55:26 +000069if args.nostamp:
70 copyright_line = "Copyright The Monogon Project Authors"
71else:
72 copyright_year = git_commit_date.partition("-")[0]
73 copyright_line = f"Copyright 2020-{copyright_year} The Monogon Project Authors"
Jan Schär10670e52025-04-23 12:54:48 +000074variables["STABLE_MONOGON_copyright"] = copyright_line
75
Serge Bazanskia6a03922023-11-13 19:57:48 +010076# Per product. Each product has it's own semver-style version number, which is
77# deduced from git tags.
78#
79# For example: metropolis v. 1.2.3 would be tagged 'metropolis-v1.2.3'.
80@dataclass
81class Version:
82 """Describes a semver version for a given product."""
83
84 product: str
85 version: str
Serge Bazanskif42e3642023-11-28 16:25:25 +010086 prerelease: [str]
87
88 def __str__(self) -> str:
89 ver = self.version
90 if self.prerelease:
91 ver += "-" + ".".join(self.prerelease)
92 return ver
Serge Bazanskia6a03922023-11-13 19:57:48 +010093
94
95def parse_tag(tag: str, product: str) -> Optional[Version]:
96 prefix = product + "-"
97 if not tag.startswith(prefix):
98 return None
99 version = tag[len(prefix) :]
100 # The first release of Metropolis was v0.1, which we extend to v0.1.0.
101 if product == "metropolis" and version == "v0.1":
102 version = "v0.1.0"
Serge Bazanskif42e3642023-11-28 16:25:25 +0100103 # Only care about the limited major/minor/patch subset of semver from git
104 # tags. All prerelease identifies will be appended by this code.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100105 if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", version):
106 return None
Jan Schärdf588d02025-04-23 15:17:11 +0000107 return Version(product, version[1:], [])
Serge Bazanskia6a03922023-11-13 19:57:48 +0100108
109
Jan Schär0cbf51a2025-04-23 10:21:17 +0000110# Is this a release build of the given product?
111is_release: dict[str, bool] = {}
112
113
Serge Bazanskif42e3642023-11-28 16:25:25 +0100114for product in ["metropolis", "cloud"]:
Jan Schärdb3866a2024-04-08 17:33:45 +0200115 # Get exact version from tags.
116 version = None
Serge Bazanskia6a03922023-11-13 19:57:48 +0100117 for tag in git_tags:
118 version = parse_tag(tag, product)
Jan Schärdb3866a2024-04-08 17:33:45 +0200119 if version is not None:
120 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100121
Jan Schär0cbf51a2025-04-23 10:21:17 +0000122 is_release[product] = version is not None and git_tree_state == "clean"
123
Serge Bazanskif42e3642023-11-28 16:25:25 +0100124 if version is None:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100125 # No exact version found. Use latest tag for the given product and
Serge Bazanskif42e3642023-11-28 16:25:25 +0100126 # append a 'devXXX' identifier based on number of commits since that
127 # tag.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100128 for tag in (
129 subprocess.check_output(
Jan Schärdb3866a2024-04-08 17:33:45 +0200130 ["git", "tag", "--sort=-version:refname", "--merged", "HEAD"]
Serge Bazanskia6a03922023-11-13 19:57:48 +0100131 )
132 .decode()
133 .strip()
134 .split("\n")
135 ):
136 version = parse_tag(tag, product)
137 if version is None:
138 continue
Jan Schärbddad352025-04-23 14:55:26 +0000139 if args.nostamp:
140 break
Serge Bazanskia6a03922023-11-13 19:57:48 +0100141 # Found the latest tag for this product. Augment it with the
Serge Bazanskif42e3642023-11-28 16:25:25 +0100142 # devXXX identifier and add it to our versions.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100143 count = (
144 subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"])
145 .decode()
146 .strip()
147 )
Serge Bazanskif42e3642023-11-28 16:25:25 +0100148 version.prerelease.append(f"dev{count}")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100149 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100150
151 if version is None:
Jan Schärdf588d02025-04-23 15:17:11 +0000152 # This product never had a release! Use 0.0.0 as a fallback.
153 version = Version(product, "0.0.0", [])
Jan Schärbddad352025-04-23 14:55:26 +0000154 if not args.nostamp:
155 # ... and count the number of all commits ever to use as the devXXX
156 # prerelease identifier.
157 count = (
158 subprocess.check_output(["git", "rev-list", "HEAD", "--count"])
159 .decode()
160 .strip()
161 )
162 version.prerelease.append(f"dev{count}")
Serge Bazanskif42e3642023-11-28 16:25:25 +0100163
Jan Schärbddad352025-04-23 14:55:26 +0000164 if args.nostamp:
165 version.prerelease.append("nostamp")
166 else:
167 version.prerelease.append(f"g{git_commit[:8]}")
168 if git_tree_state == "dirty":
169 version.prerelease.append("dirty")
Serge Bazanskif42e3642023-11-28 16:25:25 +0100170 variables[f"STABLE_MONOGON_{product}_version"] = str(version)
Serge Bazanskia6a03922023-11-13 19:57:48 +0100171
172
173# Special treatment for Kubernetes, which uses these stamp values in its build
174# system. We populate the Kubernetes version from whatever is in
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200175# //go.mod.
176def parse_go_mod(path: str) -> dict[str, str]:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100177 """
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200178 Shoddily parse a go.mod into a map of name->version.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100179
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200180 This relies heavily on go.mod being correctly formatted and
Serge Bazanskia6a03922023-11-13 19:57:48 +0100181 sorted.
182
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200183 If this breaks, it's probably best to try to port this to Go
184 and parse it using golang.org/x/mod/modfile, shell out to
185 "go mod edit -json", or similar.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100186 """
187
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200188 # Just a copied together regex to find the url followed by a semver.
189 NAME_VERSION_REGEX = r"([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*) v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)"
Serge Bazanskia6a03922023-11-13 19:57:48 +0100190
191 res = {}
192 for line in open(path):
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200193 matches = re.findall(NAME_VERSION_REGEX, line)
194 if not matches:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100195 continue
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200196
197 [name, version] = matches[0][0].strip().split(" ")
198
199 # If we already saw a package, skip it.
200 if name in res:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100201 continue
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200202
203 res[name] = version
204
Serge Bazanskia6a03922023-11-13 19:57:48 +0100205 return res
206
207
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200208# Parse go.mod.
209go_versions = parse_go_mod("go.mod")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100210
211# Find Kubernetes version.
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200212kubernetes_version: str = go_versions.get("k8s.io/kubernetes")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100213if kubernetes_version is None:
214 raise Exception("could not figure out Kubernetes version")
215kubernetes_version_parsed = re.match(
216 r"^v([0-9]+)\.([0-9]+)\.[0-9]+$", kubernetes_version
217)
218if not kubernetes_version_parsed:
219 raise Exception("invalid Kubernetes version: " + kubernetes_version)
220
Serge Bazanskia6a03922023-11-13 19:57:48 +0100221variables["STABLE_KUBERNETES_gitMajor"] = kubernetes_version_parsed[1]
222variables["STABLE_KUBERNETES_gitMinor"] = kubernetes_version_parsed[2]
223variables["STABLE_KUBERNETES_gitVersion"] = kubernetes_version + "+mngn"
224
Jan Schär0cbf51a2025-04-23 10:21:17 +0000225# Stamp commit info into Kubernetes only for release builds, to avoid
226# unnecessary rebuilds of hyperkube during development.
227if is_release["metropolis"]:
228 variables["STABLE_KUBERNETES_gitCommit"] = git_commit
229 variables["STABLE_KUBERNETES_gitTreeState"] = git_tree_state
230 variables["STABLE_KUBERNETES_buildDate"] = git_commit_date
Jan Schär36f3b6d2025-05-20 09:05:12 +0000231else:
232 variables["STABLE_KUBERNETES_gitCommit"] = ""
233 variables["STABLE_KUBERNETES_gitTreeState"] = ""
234 variables["STABLE_KUBERNETES_buildDate"] = "1970-01-01T00:00:00Z"
Jan Schär0cbf51a2025-04-23 10:21:17 +0000235
Jan Schäre6c0c322025-05-12 16:14:25 +0000236
237# Collect component versions.
238with open("build/bazel/third_party.MODULE.bazel") as f:
239 third_party_bazel = f.read()
240
241linux_version_result = re.findall(r'^LINUX_VERSION = "([a-zA-Z0-9_.-]+)"$', third_party_bazel, re.MULTILINE)
242if len(linux_version_result) != 1:
243 raise Exception("did not find LINUX_VERSION")
244variables["STABLE_MONOGON_componentVersion_linux"] = linux_version_result[0]
245
246if kubernetes_version[:1] != "v":
247 raise Exception("expected v prefix: " + kubernetes_version)
248variables["STABLE_MONOGON_componentVersion_kubernetes"] = kubernetes_version[1:]
249
Serge Bazanskia6a03922023-11-13 19:57:48 +0100250# Emit variables to stdout for consumption by Bazel and targets.
251for key in sorted(variables.keys()):
252 print("{} {}".format(key, variables[key]))