From 26576c946cfb2521ff61cf9c8f9301518437a5f1 Mon Sep 17 00:00:00 2001 From: cookiecutter-e3 Date: Mon, 16 Dec 2024 08:58:46 +0000 Subject: [PATCH] Remove tag need in build_wheel.py Instead of counting commits since the last tag, the script will find the commit when the major/minor version was bumped and count the number of commits from it. Now the script will only output the version change by default like `22.4 -> 22.4.35`. Ref it/org/operation_support/iaas/issues#214 See merge request it/cookiecutter-e3!65 of the template --- build_wheel.py | 203 ++++++++++++++++++++++++------------------------- 1 file changed, 101 insertions(+), 102 deletions(-) diff --git a/build_wheel.py b/build_wheel.py index 0ec63ec..a00b101 100644 --- a/build_wheel.py +++ b/build_wheel.py @@ -1,11 +1,8 @@ #!/usr/bin/env python """Build the wheel. -The number of commits since the last tagged version is automatically +The number of commits since the last major/minor version change is automatically added to the version of the package as the patch version. - -For that, a tag v..0 must be manually added after the -merge when a major or minor version change occurs. """ from __future__ import annotations import sys @@ -19,24 +16,23 @@ logger = getLogger("build_wheel") -ROOT_DIR = Path(__file__).parent - -def run(cmd: list[str], fail_ok: bool | None = None) -> Run: - """Print a command, run it, and print the result. +def run(cmd: list[str], cwd: Path, fail_ok: bool | None = None) -> str: + """Run a command and check the status. :param cmd: the command + :param cwd: the directory where to run the command :param fail_ok: allow the command to fail - :return: the Run instance + :return: the output of the command """ - logger.info(f"$ {' '.join(cmd)}") - p = Run(cmd, cwd=str(ROOT_DIR)) + p = Run(cmd, cwd=cwd) if p.status != 0 and not fail_ok: logger.error(p.out) sys.exit(1) - logger.info(p.out) - return p + output = p.out + assert output is not None + return output def main() -> None: @@ -46,120 +42,123 @@ def main() -> None: parser = main.argument_parser parser.description = "Build the wheel" parser.add_argument( - "--last-tag", - help="Provide the last tagged version", + "--project", + default="pyproject.toml", + help="Path to a Python project or pyproject.toml file", ) + parser.add_argument("--dry-run", action="store_true", help="Do not build the wheel") main.parse_args() assert main.args - # Find and read version file - with open(ROOT_DIR / "pyproject.toml", "rb") as f: - version_config = tomllib.load(f)["tool"]["setuptools"]["dynamic"]["version"] + project = Path(main.args.project) + if project.is_dir(): + project = project / "pyproject.toml" + + logger.debug(f"Project dir: {project.parent}") + if not project.is_file(): + logger.error(f"{project} is not a file") + sys.exit(1) + + # Find the path to version file + with open(project, "rb") as f: + version_config = ( + tomllib.load(f) + .get("tool", {}) + .get("setuptools", {}) + .get("dynamic", {}) + .get("version") + ) + + if version_config is None: + logger.error("Missing the version key in [tool.setuptools.dynamic] section") + sys.exit(1) - version_path = ROOT_DIR / ( + version_path = ( version_config["file"] if "file" in version_config else f'src/{version_config["attr"].replace(".", "/")}.py' ) - with open(version_path) as f: + logger.debug(f"Version path: {version_path}") + + # Read the version + root_dir = project.parent + version_abspath = root_dir / version_path + with open(version_abspath) as f: version_content = f.read() # Extract the .(.)? part. # We will replace the patch version by the number of commits since the most # recent tagged version - match = re.search( - r"(?P(?P\d+)\.(?P\d+)(\.\w+)?)", - version_content, - ) + version_pattern = r"(?P(?P\d+)\.(?P\d+)(\.\w+)?)" + match = re.search(version_pattern, version_content) if not match: - logger.error( - f"No .(.)? version found in {version_path.name}" - ) + logger.error(f"No .(.)? version found in {version_path}") sys.exit(1) version_major = match.group("major") version_minor = match.group("minor") version = match.group("version") - logger.info(f"Version is {version}") - - # Find previous version from the most recent tag - last_tag = main.args.last_tag - if not last_tag: - # Need to unshallow the clone so we get the list of tags. - # That command can fail for an already complete clone - run(["git", "fetch", "--unshallow", "--tags"], fail_ok=True) - # Describe the most recent tag - p = run(["git", "describe", "--tags"]) - output = p.out - assert output is not None - last_tag = output.strip() - - # Format is v..(-)? with commits omitted if - # the current commit is also the one tagged - match = re.match( - r"v(?P\d+)\.(?P\d+)\.(?P\w+)(\-(?P\d+))?", - last_tag, - ) - if not match: - logger.error( - f"Expected v..(-)? format for tag {last_tag}" - ) - sys.exit(1) + logger.debug(f"Version: {version}") - # Ensure the major and minor versions match. - # Also ensure the patch version is 0 because multiple tags for the same - # . would mess up with the versioning system and then only - # ..0 should exist - tagged_version_major = match.group("major") - tagged_version_minor = match.group("minor") - tagged_version_patch = match.group("patch") - if (version_major, version_minor, "0") != ( - tagged_version_major, - tagged_version_minor, - tagged_version_patch, - ): - logger.error( - "Found tag " - f"v{tagged_version_major}.{tagged_version_minor}.{tagged_version_patch} " - f"but was expecting v{version_major}.{version_minor}.0. " - "Please manually create the tag if not done yet or make sure this " - "is the most recent tag" - ) - sys.exit(1) + # Need to unshallow the clone to get all commits. + # That command can fail for an already complete clone + run(["git", "fetch", "--unshallow", "--tags"], cwd=root_dir, fail_ok=True) - # match.group("commits") is None only if the current commit is also - # the one tagged so there is 0 commits since that tag - new_version = "{}.{}.{}".format( - version_major, - version_minor, - match.group("commits") or "0", + # Walk through the commits on the main branch only that modified the version file + output = run( + ["git", "log", "--first-parent", "--format=format:%H", version_path], + cwd=root_dir, ) - # Replace the version in the file - logger.info(f"Set version to {new_version}") - with open(version_path, "w") as f: - f.write(version_content.replace(version, new_version)) - - try: - # Build the wheel - run( - [ - sys.executable, - "-m", - "pip", - "wheel", - ".", - "-q", - "--no-deps", - "-C--python-tag=py3", - "-w", - "build", - ] - ) - finally: - # Revert change to version file - run(["git", "restore", str(version_path)], fail_ok=True) + # Find the SHA when the major/minor version changed + previous_commit_sha = "HEAD" + for commit_sha in output.strip().splitlines(): + output = run(["git", "show", f"{commit_sha}:{version_path}"], cwd=root_dir) + match = re.search(version_pattern, output) + if ( + not match + or version_major != match.group("major") + or version_minor != match.group("minor") + ): + logger.debug(f"Different version found at commit {commit_sha}") + break + + previous_commit_sha = commit_sha + + # Count the number of commits since that SHA. + output = run( + ["git", "rev-list", f"{previous_commit_sha}..HEAD", "--count"], + cwd=root_dir, + ) + build_version = f"{version_major}.{version_minor}.{output.strip()}" + logger.info(f"{version_major}.{version_minor} -> {build_version}") + + if not main.args.dry_run: + # Replace the version in the file + with open(version_abspath, "w") as f: + f.write(version_content.replace(version, build_version)) + + try: + # Build the wheel + run( + [ + sys.executable, + "-m", + "pip", + "wheel", + ".", + "-q", + "--no-deps", + "-C--python-tag=py3", + "-w", + "build", + ], + cwd=root_dir, + ) + finally: + # Revert change to version file + run(["git", "restore", version_path], cwd=root_dir, fail_ok=True) if __name__ == "__main__":