blob: b765e0c2597520bfe204eb95a2499ca9e5f23c19 [file] [log] [blame]
#!/usr/bin/env python3
"""Workspace status script used for build stamping."""
# Treat this script as shell code, but with Python syntax. We want to remain as
# simple as possible, and absolutely never use any non-standard Python library.
# This script should be able to run on any 'modern' Linux distribution with
# Python 3.8 or newer.
# The following versioning concepts apply:
# 1. Version numbers follow the Semantic Versioning 2.0 spec.
# 2. Git tags in the form `<product>-vX.Y.Z` will be used as a basis for
# versioning a build. If the currently built release is exactly the same as
# such a tag, it will be versioned at vX.Y.Z. Otherwise, a devNNN prerelease
# identifier will be appended to signify the amount of commits since the
# release.
# 3. Product git tags are only made up of a major/minor/patch version.
# Prerelease and build tags are assigned by the build system and this
# script, Git tags have no influence on them.
# 4. 'Products' are release numbering trains within the Monogon monorepo. This
# means there is no such thing as a 'version' for the monorepo by itself,
# only within the context of some product.
from dataclasses import dataclass
import re
import subprocess
from typing import Optional
# Variables to output. These will be printed to stdout at the end of the script
# runtime, sorted by key.
variables: dict[str, str] = {}
# Git build tree status: clean or dirty.
git_tree_state: str = "clean"
git_status = subprocess.check_output(["git", "status", "--porcelain"])
if git_status.decode().strip() != "":
git_tree_state = "dirty"
# Git commit hash.
git_commit: str = (
subprocess.check_output(["git", "rev-parse", "HEAD^{commit}"]).decode().strip()
)
# Git commit date.
git_commit_date: str = (
subprocess.check_output(["git", "show", "--pretty=format:%cI", "--no-patch", "HEAD"]).decode().strip()
)
# Git tags pointing at this commit.
git_tags_b: [bytes] = subprocess.check_output(
["git", "tag", "--sort=-version:refname", "--points-at", "HEAD"]
).split(b"\n")
git_tags: [str] = [t.decode().strip() for t in git_tags_b if t.decode().strip() != ""]
variables["STABLE_MONOGON_gitCommit"] = git_commit
variables["STABLE_MONOGON_gitTreeState"] = git_tree_state
copyright_year = git_commit_date.partition("-")[0]
copyright_line = f"Copyright 2020-{copyright_year} The Monogon Project Authors"
variables["STABLE_MONOGON_copyright"] = copyright_line
# Per product. Each product has it's own semver-style version number, which is
# deduced from git tags.
#
# For example: metropolis v. 1.2.3 would be tagged 'metropolis-v1.2.3'.
@dataclass
class Version:
"""Describes a semver version for a given product."""
product: str
version: str
prerelease: [str]
def __str__(self) -> str:
ver = self.version
if self.prerelease:
ver += "-" + ".".join(self.prerelease)
return ver
def parse_tag(tag: str, product: str) -> Optional[Version]:
prefix = product + "-"
if not tag.startswith(prefix):
return None
version = tag[len(prefix) :]
# The first release of Metropolis was v0.1, which we extend to v0.1.0.
if product == "metropolis" and version == "v0.1":
version = "v0.1.0"
# Only care about the limited major/minor/patch subset of semver from git
# tags. All prerelease identifies will be appended by this code.
if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", version):
return None
return Version(product, version, [])
# Is this a release build of the given product?
is_release: dict[str, bool] = {}
for product in ["metropolis", "cloud"]:
# Get exact version from tags.
version = None
for tag in git_tags:
version = parse_tag(tag, product)
if version is not None:
break
is_release[product] = version is not None and git_tree_state == "clean"
if version is None:
# No exact version found. Use latest tag for the given product and
# append a 'devXXX' identifier based on number of commits since that
# tag.
for tag in (
subprocess.check_output(
["git", "tag", "--sort=-version:refname", "--merged", "HEAD"]
)
.decode()
.strip()
.split("\n")
):
version = parse_tag(tag, product)
if version is None:
continue
# Found the latest tag for this product. Augment it with the
# devXXX identifier and add it to our versions.
count = (
subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"])
.decode()
.strip()
)
version.prerelease.append(f"dev{count}")
break
if version is None:
# This product never had a release! Use v0.0.0 as a fallback.
version = Version(product, "v0.0.0", [])
# ... and count the number of all commits ever to use as the devXXX
# prerelease identifier.
count = (
subprocess.check_output(["git", "rev-list", "HEAD", "--count"])
.decode()
.strip()
)
version.prerelease.append(f"dev{count}")
version.prerelease.append(f"g{git_commit[:8]}")
if git_tree_state == "dirty":
version.prerelease.append("dirty")
variables[f"STABLE_MONOGON_{product}_version"] = str(version)
# Special treatment for Kubernetes, which uses these stamp values in its build
# system. We populate the Kubernetes version from whatever is in
# //go.mod.
def parse_go_mod(path: str) -> dict[str, str]:
"""
Shoddily parse a go.mod into a map of name->version.
This relies heavily on go.mod being correctly formatted and
sorted.
If this breaks, it's probably best to try to port this to Go
and parse it using golang.org/x/mod/modfile, shell out to
"go mod edit -json", or similar.
"""
# Just a copied together regex to find the url followed by a semver.
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-]+)*))?)"
res = {}
for line in open(path):
matches = re.findall(NAME_VERSION_REGEX, line)
if not matches:
continue
[name, version] = matches[0][0].strip().split(" ")
# If we already saw a package, skip it.
if name in res:
continue
res[name] = version
return res
# Parse go.mod.
go_versions = parse_go_mod("go.mod")
# Find Kubernetes version.
kubernetes_version: str = go_versions.get("k8s.io/kubernetes")
if kubernetes_version is None:
raise Exception("could not figure out Kubernetes version")
kubernetes_version_parsed = re.match(
r"^v([0-9]+)\.([0-9]+)\.[0-9]+$", kubernetes_version
)
if not kubernetes_version_parsed:
raise Exception("invalid Kubernetes version: " + kubernetes_version)
variables["STABLE_KUBERNETES_gitMajor"] = kubernetes_version_parsed[1]
variables["STABLE_KUBERNETES_gitMinor"] = kubernetes_version_parsed[2]
variables["STABLE_KUBERNETES_gitVersion"] = kubernetes_version + "+mngn"
# Stamp commit info into Kubernetes only for release builds, to avoid
# unnecessary rebuilds of hyperkube during development.
if is_release["metropolis"]:
variables["STABLE_KUBERNETES_gitCommit"] = git_commit
variables["STABLE_KUBERNETES_gitTreeState"] = git_tree_state
variables["STABLE_KUBERNETES_buildDate"] = git_commit_date
# Emit variables to stdout for consumption by Bazel and targets.
for key in sorted(variables.keys()):
print("{} {}".format(key, variables[key]))