| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 1 | #!/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 Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 13 | # 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 Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 16 | # 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är | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 23 | import argparse |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 24 | from dataclasses import dataclass |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 25 | import re |
| 26 | import subprocess |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 27 | |
| 28 | from typing import Optional |
| 29 | |
| 30 | |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 31 | parser = argparse.ArgumentParser() |
| 32 | parser.add_argument("--nostamp", action="store_true") |
| 33 | args = parser.parse_args() |
| 34 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 35 | # Variables to output. These will be printed to stdout at the end of the script |
| 36 | # runtime, sorted by key. |
| 37 | variables: dict[str, str] = {} |
| 38 | |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 39 | git_tree_state: str = "unknown" |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 40 | |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 41 | if 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 Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 47 | |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 48 | # 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är | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame] | 57 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 58 | # Git tags pointing at this commit. |
| 59 | git_tags_b: [bytes] = subprocess.check_output( |
| Jan Schär | db3866a | 2024-04-08 17:33:45 +0200 | [diff] [blame] | 60 | ["git", "tag", "--sort=-version:refname", "--points-at", "HEAD"] |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 61 | ).split(b"\n") |
| 62 | git_tags: [str] = [t.decode().strip() for t in git_tags_b if t.decode().strip() != ""] |
| 63 | |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 64 | if not args.nostamp: |
| 65 | variables["STABLE_MONOGON_gitCommit"] = git_commit |
| Jan Schär | e6c0c32 | 2025-05-12 16:14:25 +0000 | [diff] [blame] | 66 | variables["STABLE_MONOGON_gitCommitDate"] = git_commit_date |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 67 | variables["STABLE_MONOGON_gitTreeState"] = git_tree_state |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 68 | |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 69 | if args.nostamp: |
| 70 | copyright_line = "Copyright The Monogon Project Authors" |
| 71 | else: |
| 72 | copyright_year = git_commit_date.partition("-")[0] |
| 73 | copyright_line = f"Copyright 2020-{copyright_year} The Monogon Project Authors" |
| Jan Schär | 10670e5 | 2025-04-23 12:54:48 +0000 | [diff] [blame] | 74 | variables["STABLE_MONOGON_copyright"] = copyright_line |
| 75 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 76 | # 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 |
| 81 | class Version: |
| 82 | """Describes a semver version for a given product.""" |
| 83 | |
| 84 | product: str |
| 85 | version: str |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 86 | 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 Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 93 | |
| 94 | |
| 95 | def 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 Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 103 | # 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 Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 105 | if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", version): |
| 106 | return None |
| Jan Schär | df588d0 | 2025-04-23 15:17:11 +0000 | [diff] [blame] | 107 | return Version(product, version[1:], []) |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 108 | |
| 109 | |
| Jan Schär | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame] | 110 | # Is this a release build of the given product? |
| 111 | is_release: dict[str, bool] = {} |
| 112 | |
| 113 | |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 114 | for product in ["metropolis", "cloud"]: |
| Jan Schär | db3866a | 2024-04-08 17:33:45 +0200 | [diff] [blame] | 115 | # Get exact version from tags. |
| 116 | version = None |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 117 | for tag in git_tags: |
| 118 | version = parse_tag(tag, product) |
| Jan Schär | db3866a | 2024-04-08 17:33:45 +0200 | [diff] [blame] | 119 | if version is not None: |
| 120 | break |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 121 | |
| Jan Schär | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame] | 122 | is_release[product] = version is not None and git_tree_state == "clean" |
| 123 | |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 124 | if version is None: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 125 | # No exact version found. Use latest tag for the given product and |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 126 | # append a 'devXXX' identifier based on number of commits since that |
| 127 | # tag. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 128 | for tag in ( |
| 129 | subprocess.check_output( |
| Jan Schär | db3866a | 2024-04-08 17:33:45 +0200 | [diff] [blame] | 130 | ["git", "tag", "--sort=-version:refname", "--merged", "HEAD"] |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 131 | ) |
| 132 | .decode() |
| 133 | .strip() |
| 134 | .split("\n") |
| 135 | ): |
| 136 | version = parse_tag(tag, product) |
| 137 | if version is None: |
| 138 | continue |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 139 | if args.nostamp: |
| 140 | break |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 141 | # Found the latest tag for this product. Augment it with the |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 142 | # devXXX identifier and add it to our versions. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 143 | count = ( |
| 144 | subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"]) |
| 145 | .decode() |
| 146 | .strip() |
| 147 | ) |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 148 | version.prerelease.append(f"dev{count}") |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 149 | break |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 150 | |
| 151 | if version is None: |
| Jan Schär | df588d0 | 2025-04-23 15:17:11 +0000 | [diff] [blame] | 152 | # This product never had a release! Use 0.0.0 as a fallback. |
| 153 | version = Version(product, "0.0.0", []) |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 154 | 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 Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 163 | |
| Jan Schär | bddad35 | 2025-04-23 14:55:26 +0000 | [diff] [blame] | 164 | 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 Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 170 | variables[f"STABLE_MONOGON_{product}_version"] = str(version) |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 171 | |
| 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 Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 175 | # //go.mod. |
| 176 | def parse_go_mod(path: str) -> dict[str, str]: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 177 | """ |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 178 | Shoddily parse a go.mod into a map of name->version. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 179 | |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 180 | This relies heavily on go.mod being correctly formatted and |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 181 | sorted. |
| 182 | |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 183 | 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 Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 186 | """ |
| 187 | |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 188 | # 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 Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 190 | |
| 191 | res = {} |
| 192 | for line in open(path): |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 193 | matches = re.findall(NAME_VERSION_REGEX, line) |
| 194 | if not matches: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 195 | continue |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 196 | |
| 197 | [name, version] = matches[0][0].strip().split(" ") |
| 198 | |
| 199 | # If we already saw a package, skip it. |
| 200 | if name in res: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 201 | continue |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 202 | |
| 203 | res[name] = version |
| 204 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 205 | return res |
| 206 | |
| 207 | |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 208 | # Parse go.mod. |
| 209 | go_versions = parse_go_mod("go.mod") |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 210 | |
| 211 | # Find Kubernetes version. |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 212 | kubernetes_version: str = go_versions.get("k8s.io/kubernetes") |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 213 | if kubernetes_version is None: |
| 214 | raise Exception("could not figure out Kubernetes version") |
| 215 | kubernetes_version_parsed = re.match( |
| 216 | r"^v([0-9]+)\.([0-9]+)\.[0-9]+$", kubernetes_version |
| 217 | ) |
| 218 | if not kubernetes_version_parsed: |
| 219 | raise Exception("invalid Kubernetes version: " + kubernetes_version) |
| 220 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 221 | variables["STABLE_KUBERNETES_gitMajor"] = kubernetes_version_parsed[1] |
| 222 | variables["STABLE_KUBERNETES_gitMinor"] = kubernetes_version_parsed[2] |
| 223 | variables["STABLE_KUBERNETES_gitVersion"] = kubernetes_version + "+mngn" |
| 224 | |
| Jan Schär | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame] | 225 | # Stamp commit info into Kubernetes only for release builds, to avoid |
| 226 | # unnecessary rebuilds of hyperkube during development. |
| 227 | if 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är | 36f3b6d | 2025-05-20 09:05:12 +0000 | [diff] [blame] | 231 | else: |
| 232 | variables["STABLE_KUBERNETES_gitCommit"] = "" |
| 233 | variables["STABLE_KUBERNETES_gitTreeState"] = "" |
| 234 | variables["STABLE_KUBERNETES_buildDate"] = "1970-01-01T00:00:00Z" |
| Jan Schär | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame] | 235 | |
| Jan Schär | e6c0c32 | 2025-05-12 16:14:25 +0000 | [diff] [blame] | 236 | |
| 237 | # Collect component versions. |
| 238 | with open("build/bazel/third_party.MODULE.bazel") as f: |
| 239 | third_party_bazel = f.read() |
| 240 | |
| 241 | linux_version_result = re.findall(r'^LINUX_VERSION = "([a-zA-Z0-9_.-]+)"$', third_party_bazel, re.MULTILINE) |
| 242 | if len(linux_version_result) != 1: |
| 243 | raise Exception("did not find LINUX_VERSION") |
| 244 | variables["STABLE_MONOGON_componentVersion_linux"] = linux_version_result[0] |
| 245 | |
| 246 | if kubernetes_version[:1] != "v": |
| 247 | raise Exception("expected v prefix: " + kubernetes_version) |
| 248 | variables["STABLE_MONOGON_componentVersion_kubernetes"] = kubernetes_version[1:] |
| 249 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 250 | # Emit variables to stdout for consumption by Bazel and targets. |
| 251 | for key in sorted(variables.keys()): |
| 252 | print("{} {}".format(key, variables[key])) |