|  |  |  | #!/usr/bin/env python3
 | 
					
						
							|  |  |  | from dataclasses import dataclass
 | 
					
						
							|  |  |  | from functools import cache
 | 
					
						
							|  |  |  | import json
 | 
					
						
							|  |  |  | import os
 | 
					
						
							|  |  |  | import pathlib
 | 
					
						
							|  |  |  | import subprocess
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from openpilot.common.basedir import BASEDIR
 | 
					
						
							|  |  |  | from openpilot.common.swaglog import cloudlog
 | 
					
						
							|  |  |  | from openpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | RELEASE_BRANCHES = ['release3-staging', 'release3', 'nightly']
 | 
					
						
							|  |  |  | TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging']
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | BUILD_METADATA_FILENAME = "build.json"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | training_version: bytes = b"0.2.0"
 | 
					
						
							|  |  |  | terms_version: bytes = b"2"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_version(path: str = BASEDIR) -> str:
 | 
					
						
							|  |  |  |   with open(os.path.join(path, "common", "version.h")) as _versionf:
 | 
					
						
							|  |  |  |     version = _versionf.read().split('"')[1]
 | 
					
						
							|  |  |  |   return version
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_release_notes(path: str = BASEDIR) -> str:
 | 
					
						
							|  |  |  |   with open(os.path.join(path, "RELEASES.md")) as f:
 | 
					
						
							|  |  |  |     return f.read().split('\n\n', 1)[0]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @cache
 | 
					
						
							|  |  |  | def is_prebuilt(path: str = BASEDIR) -> bool:
 | 
					
						
							|  |  |  |   return os.path.exists(os.path.join(path, 'prebuilt'))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @cache
 | 
					
						
							|  |  |  | def is_dirty(cwd: str = BASEDIR) -> bool:
 | 
					
						
							|  |  |  |   origin = get_origin()
 | 
					
						
							|  |  |  |   branch = get_branch()
 | 
					
						
							|  |  |  |   if not origin or not branch:
 | 
					
						
							|  |  |  |     return True
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   dirty = False
 | 
					
						
							|  |  |  |   try:
 | 
					
						
							|  |  |  |     # Actually check dirty files
 | 
					
						
							|  |  |  |     if not is_prebuilt(cwd):
 | 
					
						
							|  |  |  |       # This is needed otherwise touched files might show up as modified
 | 
					
						
							|  |  |  |       try:
 | 
					
						
							|  |  |  |         subprocess.check_call(["git", "update-index", "--refresh"], cwd=cwd)
 | 
					
						
							|  |  |  |       except subprocess.CalledProcessError:
 | 
					
						
							|  |  |  |         pass
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       dirty = (subprocess.call(["git", "diff-index", "--quiet", branch, "--"], cwd=cwd)) != 0
 | 
					
						
							|  |  |  |   except subprocess.CalledProcessError:
 | 
					
						
							|  |  |  |     cloudlog.exception("git subprocess failed while checking dirty")
 | 
					
						
							|  |  |  |     dirty = True
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return dirty
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @dataclass
 | 
					
						
							|  |  |  | class OpenpilotMetadata:
 | 
					
						
							|  |  |  |   version: str
 | 
					
						
							|  |  |  |   release_notes: str
 | 
					
						
							|  |  |  |   git_commit: str
 | 
					
						
							|  |  |  |   git_origin: str
 | 
					
						
							|  |  |  |   git_commit_date: str
 | 
					
						
							|  |  |  |   build_style: str
 | 
					
						
							|  |  |  |   is_dirty: bool  # whether there are local changes
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property
 | 
					
						
							|  |  |  |   def short_version(self) -> str:
 | 
					
						
							|  |  |  |     return self.version.split('-')[0]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property
 | 
					
						
							|  |  |  |   def comma_remote(self) -> bool:
 | 
					
						
							|  |  |  |     # note to fork maintainers, this is used for release metrics. please do not
 | 
					
						
							|  |  |  |     # touch this to get rid of the orange startup alert. there's better ways to do that
 | 
					
						
							|  |  |  |     return self.git_normalized_origin == "github.com/commaai/openpilot"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property
 | 
					
						
							|  |  |  |   def git_normalized_origin(self) -> str:
 | 
					
						
							|  |  |  |     return self.git_origin \
 | 
					
						
							|  |  |  |       .replace("git@", "", 1) \
 | 
					
						
							|  |  |  |       .replace(".git", "", 1) \
 | 
					
						
							|  |  |  |       .replace("https://", "", 1) \
 | 
					
						
							|  |  |  |       .replace(":", "/", 1)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @dataclass
 | 
					
						
							|  |  |  | class BuildMetadata:
 | 
					
						
							|  |  |  |   channel: str
 | 
					
						
							|  |  |  |   openpilot: OpenpilotMetadata
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property
 | 
					
						
							|  |  |  |   def tested_channel(self) -> bool:
 | 
					
						
							|  |  |  |     return self.channel in TESTED_BRANCHES
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property
 | 
					
						
							|  |  |  |   def release_channel(self) -> bool:
 | 
					
						
							|  |  |  |     return self.channel in RELEASE_BRANCHES
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property
 | 
					
						
							|  |  |  |   def canonical(self) -> str:
 | 
					
						
							|  |  |  |     return f"{self.openpilot.version}-{self.openpilot.git_commit}-{self.openpilot.build_style}"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property
 | 
					
						
							|  |  |  |   def ui_description(self) -> str:
 | 
					
						
							|  |  |  |     return f"{self.openpilot.version} / {self.openpilot.git_commit[:6]} / {self.channel}"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def build_metadata_from_dict(build_metadata: dict) -> BuildMetadata:
 | 
					
						
							|  |  |  |   channel = build_metadata.get("channel", "unknown")
 | 
					
						
							|  |  |  |   openpilot_metadata = build_metadata.get("openpilot", {})
 | 
					
						
							|  |  |  |   version = openpilot_metadata.get("version", "unknown")
 | 
					
						
							|  |  |  |   release_notes = openpilot_metadata.get("release_notes", "unknown")
 | 
					
						
							|  |  |  |   git_commit = openpilot_metadata.get("git_commit", "unknown")
 | 
					
						
							|  |  |  |   git_origin = openpilot_metadata.get("git_origin", "unknown")
 | 
					
						
							|  |  |  |   git_commit_date = openpilot_metadata.get("git_commit_date", "unknown")
 | 
					
						
							|  |  |  |   build_style = openpilot_metadata.get("build_style", "unknown")
 | 
					
						
							|  |  |  |   return BuildMetadata(channel,
 | 
					
						
							|  |  |  |             OpenpilotMetadata(
 | 
					
						
							|  |  |  |               version=version,
 | 
					
						
							|  |  |  |               release_notes=release_notes,
 | 
					
						
							|  |  |  |               git_commit=git_commit,
 | 
					
						
							|  |  |  |               git_origin=git_origin,
 | 
					
						
							|  |  |  |               git_commit_date=git_commit_date,
 | 
					
						
							|  |  |  |               build_style=build_style,
 | 
					
						
							|  |  |  |               is_dirty=False))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_build_metadata(path: str = BASEDIR) -> BuildMetadata:
 | 
					
						
							|  |  |  |   build_metadata_path = pathlib.Path(path) / BUILD_METADATA_FILENAME
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if build_metadata_path.exists():
 | 
					
						
							|  |  |  |     build_metadata = json.loads(build_metadata_path.read_text())
 | 
					
						
							|  |  |  |     return build_metadata_from_dict(build_metadata)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   git_folder = pathlib.Path(path) / ".git"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if git_folder.exists():
 | 
					
						
							|  |  |  |     return BuildMetadata(get_short_branch(path),
 | 
					
						
							|  |  |  |                     OpenpilotMetadata(
 | 
					
						
							|  |  |  |                       version=get_version(path),
 | 
					
						
							|  |  |  |                       release_notes=get_release_notes(path),
 | 
					
						
							|  |  |  |                       git_commit=get_commit(path),
 | 
					
						
							|  |  |  |                       git_origin=get_origin(path),
 | 
					
						
							|  |  |  |                       git_commit_date=get_commit_date(path),
 | 
					
						
							|  |  |  |                       build_style="unknown",
 | 
					
						
							|  |  |  |                       is_dirty=is_dirty(path)))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cloudlog.exception("unable to get build metadata")
 | 
					
						
							|  |  |  |   raise Exception("invalid build metadata")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__":
 | 
					
						
							|  |  |  |   from openpilot.common.params import Params
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   params = Params()
 | 
					
						
							|  |  |  |   params.put("TermsVersion", terms_version)
 | 
					
						
							|  |  |  |   params.put("TrainingVersion", training_version)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   print(get_build_metadata())
 |