blob: 992a8fce497b4728a9f65623f46bd840cfeca7cf [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
66 variables["STABLE_MONOGON_gitTreeState"] = git_tree_state
Serge Bazanskia6a03922023-11-13 19:57:48 +010067
Jan Schärbddad352025-04-23 14:55:26 +000068if args.nostamp:
69 copyright_line = "Copyright The Monogon Project Authors"
70else:
71 copyright_year = git_commit_date.partition("-")[0]
72 copyright_line = f"Copyright 2020-{copyright_year} The Monogon Project Authors"
Jan Schär10670e52025-04-23 12:54:48 +000073variables["STABLE_MONOGON_copyright"] = copyright_line
74
Serge Bazanskia6a03922023-11-13 19:57:48 +010075# Per product. Each product has it's own semver-style version number, which is
76# deduced from git tags.
77#
78# For example: metropolis v. 1.2.3 would be tagged 'metropolis-v1.2.3'.
79@dataclass
80class Version:
81 """Describes a semver version for a given product."""
82
83 product: str
84 version: str
Serge Bazanskif42e3642023-11-28 16:25:25 +010085 prerelease: [str]
86
87 def __str__(self) -> str:
88 ver = self.version
89 if self.prerelease:
90 ver += "-" + ".".join(self.prerelease)
91 return ver
Serge Bazanskia6a03922023-11-13 19:57:48 +010092
93
94def parse_tag(tag: str, product: str) -> Optional[Version]:
95 prefix = product + "-"
96 if not tag.startswith(prefix):
97 return None
98 version = tag[len(prefix) :]
99 # The first release of Metropolis was v0.1, which we extend to v0.1.0.
100 if product == "metropolis" and version == "v0.1":
101 version = "v0.1.0"
Serge Bazanskif42e3642023-11-28 16:25:25 +0100102 # Only care about the limited major/minor/patch subset of semver from git
103 # tags. All prerelease identifies will be appended by this code.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100104 if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", version):
105 return None
Jan Schärdf588d02025-04-23 15:17:11 +0000106 return Version(product, version[1:], [])
Serge Bazanskia6a03922023-11-13 19:57:48 +0100107
108
Jan Schär0cbf51a2025-04-23 10:21:17 +0000109# Is this a release build of the given product?
110is_release: dict[str, bool] = {}
111
112
Serge Bazanskif42e3642023-11-28 16:25:25 +0100113for product in ["metropolis", "cloud"]:
Jan Schärdb3866a2024-04-08 17:33:45 +0200114 # Get exact version from tags.
115 version = None
Serge Bazanskia6a03922023-11-13 19:57:48 +0100116 for tag in git_tags:
117 version = parse_tag(tag, product)
Jan Schärdb3866a2024-04-08 17:33:45 +0200118 if version is not None:
119 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100120
Jan Schär0cbf51a2025-04-23 10:21:17 +0000121 is_release[product] = version is not None and git_tree_state == "clean"
122
Serge Bazanskif42e3642023-11-28 16:25:25 +0100123 if version is None:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100124 # No exact version found. Use latest tag for the given product and
Serge Bazanskif42e3642023-11-28 16:25:25 +0100125 # append a 'devXXX' identifier based on number of commits since that
126 # tag.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100127 for tag in (
128 subprocess.check_output(
Jan Schärdb3866a2024-04-08 17:33:45 +0200129 ["git", "tag", "--sort=-version:refname", "--merged", "HEAD"]
Serge Bazanskia6a03922023-11-13 19:57:48 +0100130 )
131 .decode()
132 .strip()
133 .split("\n")
134 ):
135 version = parse_tag(tag, product)
136 if version is None:
137 continue
Jan Schärbddad352025-04-23 14:55:26 +0000138 if args.nostamp:
139 break
Serge Bazanskia6a03922023-11-13 19:57:48 +0100140 # Found the latest tag for this product. Augment it with the
Serge Bazanskif42e3642023-11-28 16:25:25 +0100141 # devXXX identifier and add it to our versions.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100142 count = (
143 subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"])
144 .decode()
145 .strip()
146 )
Serge Bazanskif42e3642023-11-28 16:25:25 +0100147 version.prerelease.append(f"dev{count}")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100148 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100149
150 if version is None:
Jan Schärdf588d02025-04-23 15:17:11 +0000151 # This product never had a release! Use 0.0.0 as a fallback.
152 version = Version(product, "0.0.0", [])
Jan Schärbddad352025-04-23 14:55:26 +0000153 if not args.nostamp:
154 # ... and count the number of all commits ever to use as the devXXX
155 # prerelease identifier.
156 count = (
157 subprocess.check_output(["git", "rev-list", "HEAD", "--count"])
158 .decode()
159 .strip()
160 )
161 version.prerelease.append(f"dev{count}")
Serge Bazanskif42e3642023-11-28 16:25:25 +0100162
Jan Schärbddad352025-04-23 14:55:26 +0000163 if args.nostamp:
164 version.prerelease.append("nostamp")
165 else:
166 version.prerelease.append(f"g{git_commit[:8]}")
167 if git_tree_state == "dirty":
168 version.prerelease.append("dirty")
Serge Bazanskif42e3642023-11-28 16:25:25 +0100169 variables[f"STABLE_MONOGON_{product}_version"] = str(version)
Serge Bazanskia6a03922023-11-13 19:57:48 +0100170
171
172# Special treatment for Kubernetes, which uses these stamp values in its build
173# system. We populate the Kubernetes version from whatever is in
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200174# //go.mod.
175def parse_go_mod(path: str) -> dict[str, str]:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100176 """
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200177 Shoddily parse a go.mod into a map of name->version.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100178
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200179 This relies heavily on go.mod being correctly formatted and
Serge Bazanskia6a03922023-11-13 19:57:48 +0100180 sorted.
181
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200182 If this breaks, it's probably best to try to port this to Go
183 and parse it using golang.org/x/mod/modfile, shell out to
184 "go mod edit -json", or similar.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100185 """
186
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200187 # Just a copied together regex to find the url followed by a semver.
188 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 +0100189
190 res = {}
191 for line in open(path):
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200192 matches = re.findall(NAME_VERSION_REGEX, line)
193 if not matches:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100194 continue
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200195
196 [name, version] = matches[0][0].strip().split(" ")
197
198 # If we already saw a package, skip it.
199 if name in res:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100200 continue
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200201
202 res[name] = version
203
Serge Bazanskia6a03922023-11-13 19:57:48 +0100204 return res
205
206
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200207# Parse go.mod.
208go_versions = parse_go_mod("go.mod")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100209
210# Find Kubernetes version.
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200211kubernetes_version: str = go_versions.get("k8s.io/kubernetes")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100212if kubernetes_version is None:
213 raise Exception("could not figure out Kubernetes version")
214kubernetes_version_parsed = re.match(
215 r"^v([0-9]+)\.([0-9]+)\.[0-9]+$", kubernetes_version
216)
217if not kubernetes_version_parsed:
218 raise Exception("invalid Kubernetes version: " + kubernetes_version)
219
Serge Bazanskia6a03922023-11-13 19:57:48 +0100220variables["STABLE_KUBERNETES_gitMajor"] = kubernetes_version_parsed[1]
221variables["STABLE_KUBERNETES_gitMinor"] = kubernetes_version_parsed[2]
222variables["STABLE_KUBERNETES_gitVersion"] = kubernetes_version + "+mngn"
223
Jan Schär0cbf51a2025-04-23 10:21:17 +0000224# Stamp commit info into Kubernetes only for release builds, to avoid
225# unnecessary rebuilds of hyperkube during development.
226if is_release["metropolis"]:
227 variables["STABLE_KUBERNETES_gitCommit"] = git_commit
228 variables["STABLE_KUBERNETES_gitTreeState"] = git_tree_state
229 variables["STABLE_KUBERNETES_buildDate"] = git_commit_date
230
Serge Bazanskia6a03922023-11-13 19:57:48 +0100231# Emit variables to stdout for consumption by Bazel and targets.
232for key in sorted(variables.keys()):
233 print("{} {}".format(key, variables[key]))