#!/usr/bin/python3 import argparse import glob import os import re from subprocess import check_output, run PACKAGE_PRERELEASE_RE = re.compile(r"(?<=[\.\d])(?Pa|alpha|b|beta|c|rc|pre|preview)(?P\d*)$") SPEC_VERSION_RE = re.compile(r"^(?PVersion:[ \t]*).*", re.M) class Git: def __init__(self, workdir=None): self.workdir = workdir self._topdir = None @property def topdir(self): if not self._topdir: cmd = ["git", "rev-parse", "--show-toplevel"] self._topdir = check_output(cmd, cwd=self.workdir, encoding="utf-8").strip() return self._topdir def describe(self): cmd = ["git", "describe", "--tags", "--abbrev=0"] git_tag = check_output(cmd, cwd=self.workdir, encoding="utf-8").strip() cmd = ["git", "describe", "--tags"] desc = check_output(cmd, cwd=self.workdir, encoding="utf-8").strip() if desc == git_tag: # we're at the latest git tag git_commits = 0 git_hash = None else: # there are additional commits on top of the latest tag _, git_commits, git_hash = desc.rsplit("-", 2) git_commits = int(git_commits) git_hash = git_hash[1:] return git_tag, git_commits, git_hash def get_package_version(self): """ Return package version based on a git tag. Pre-releases will be prefixed with '~' to comply with RPM pre-release versioning. """ git_tag, git_commits, git_hash = self.describe() version = PACKAGE_PRERELEASE_RE.sub(r"~\g\g", git_tag) if git_commits: version += f".{git_commits}.g{git_hash}" return version def archive(self, pkg_name, destdir=None): pkg_version = self.get_package_version() prefix = f"{pkg_name}-{pkg_version}" destdir = destdir or self.topdir tar_path = os.path.abspath(os.path.join(destdir, f"{prefix}.tar.gz")) cmd = ["git", "archive", "--format=tar.gz", f"--prefix={prefix}/", "HEAD", f"--output={tar_path}"] run(cmd, check=True, cwd=self.topdir) return tar_path class Spec: @classmethod def find(cls, topdir): paths = ["", "contrib"] for path in paths: spec_paths = glob.glob(os.path.join(topdir, path, "*.spec")) if spec_paths: return cls(spec_paths[0]) raise RuntimeError(f"Couldn't find spec under {topdir}") def __init__(self, path): self.path = os.path.abspath(path) self.topdir = os.path.dirname(self.path) self._name = None @property def name(self): if not self._name: cmd = ["rpmspec", self.path, "-q", "--qf=%{name}"] self._name = check_output(cmd, encoding="utf-8").strip() return self._name def set_version(self, version): with open(self.path, "r+", encoding="utf-8") as f: data = f.read() new_data = SPEC_VERSION_RE.sub(fr"\g{version}", data) f.seek(0) f.write(new_data) f.truncate() def build(self, srpm=True, rpm=True, nodeps=False): cmd = [ "rpmbuild", self.path, "--define", f"_sourcedir {self.topdir}", "--define", f"_srcrpmdir {self.topdir}", "--define", f"_rpmdir {self.topdir}", ] if srpm and rpm: cmd.append("-ba") elif srpm: cmd.append("-bs") elif rpm: cmd.append("-bb") if nodeps: cmd.append("--nodeps") env = os.environ.copy() env["LC_ALL"] = "C.UTF-8" env["LANGUAGE"] = "C" run(cmd, check=True, encoding="utf-8", cwd=self.topdir, env=env) def main(): parser = argparse.ArgumentParser() parser.add_argument( "--rpm", action="store_true", help="Build binary RPMs", ) parser.add_argument( "--srpm", action="store_true", help="Build source RPM", ) parser.add_argument( "--nodeps", action="store_true", help="Do not verify build dependencies", ) args = parser.parse_args() if not args.rpm and not args.srpm: parser.error('No build output specified. Please specify --rpm, --srpm or both.') git = Git() spec = Spec.find(git.topdir) git.archive(spec.name, destdir=spec.topdir) spec.set_version(git.get_package_version()) spec.build(srpm=args.srpm, rpm=args.rpm, nodeps=args.nodeps) if __name__ == "__main__": main()