diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33489496..165b62ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,10 +125,16 @@ jobs: run: ./wrappers/python/scripts/ci/github/debug_python_paths.sh - name: package binaries working-directory: ./wrappers/python - run: python -m scripts.build.all ./vendor # should take ~30s + run: | # should take ~30s; writes wheels to wrappers/python/dist + export PAGEFIND_PYTHON_LOG_LEVEL=DEBUG + python -m scripts.build.all_binary_only_wheels \ + --git-tag "${{ github.ref_name }}" \ + --bin-dir ./vendor - name: package python api working-directory: ./wrappers/python - run: python3 -m scripts.build.api_package + run: | # writes stdist + wheel to wrappers/python/dist + export PAGEFIND_PYTHON_LOG_LEVEL=DEBUG + python -m scripts.build.api_package --tag "${{ github.ref_name }}" - name: Archive dist uses: actions/upload-artifact@v4 with: diff --git a/wrappers/python/pyproject.toml b/wrappers/python/pyproject.toml index 68742ea7..67f39657 100644 --- a/wrappers/python/pyproject.toml +++ b/wrappers/python/pyproject.toml @@ -1,8 +1,8 @@ + [tool.poetry] name = "pagefind" version = "0.0.0a0" -# note that ^this^ is the version number of the python API, not the version of -# the pagefind executable. +# note this^^^^^^^ version will be replaced by scripts/build/api_package.py description = "Python API for Pagefind" authors = ["CloudCannon"] license = "MIT" diff --git a/wrappers/python/scripts/build/all.py b/wrappers/python/scripts/build/all_binary_only_wheels.py similarity index 69% rename from wrappers/python/scripts/build/all.py rename to wrappers/python/scripts/build/all_binary_only_wheels.py index 946e615d..e0d6a179 100644 --- a/wrappers/python/scripts/build/all.py +++ b/wrappers/python/scripts/build/all_binary_only_wheels.py @@ -1,10 +1,11 @@ """A script that builds all the pagefind binary-only wheels.""" -import os +import logging +import re import tarfile import tempfile from pathlib import Path -from typing import List, Optional, Tuple +from typing import List, NamedTuple, Optional from argparse import ArgumentParser from . import dist_dir, setup_logging @@ -21,9 +22,12 @@ "pagefind_extended.exe", ) +log = logging.getLogger(__name__) + def find_bin(dir: Path) -> Path: for file in dir.iterdir(): + log.debug("Checking for executable @ %s", (dir / file).absolute()) if file.is_file() and file.name in __candidates: return file raise FileNotFoundError(f"Could not find any of {__candidates} in {dir}") @@ -36,6 +40,7 @@ def get_llvm_triple(tar_gz: Path) -> str: llvm_triple = llvm_triple.removesuffix(".tar.gz") llvm_triple = llvm_triple.removeprefix(f"pagefind-{tag_name}-") llvm_triple = llvm_triple.removeprefix(f"pagefind_extended-{tag_name}-") + log.debug(f"derived llvm_triple {llvm_triple} from {tar_gz.name}") return llvm_triple @@ -51,27 +56,39 @@ def check_platforms(certified: List[Path]) -> None: raise ValueError(err_message) -def parse_args() -> Tuple[bool, Optional[Path]]: +class Args(NamedTuple): + dry_run: bool + bin_dir: Optional[Path] + tag: Optional[str] + + +def parse_args() -> Args: parser = ArgumentParser() + parser.add_argument("--tag", type=str, default=None) parser.add_argument("--dry-run", action="store_true") - parser.add_argument("DIR", type=Path, default=None, nargs="?") + parser.add_argument("--bin-dir", type=Path, default=None) args = parser.parse_args() dry_run: bool = args.dry_run - bin_dir: Optional[Path] = args.DIR - return dry_run, bin_dir + bin_dir: Optional[Path] = args.bin_dir + tag: Optional[str] = args.tag + return Args(dry_run=dry_run, bin_dir=bin_dir, tag=tag) if __name__ == "__main__": - dry_run, bin_dir = parse_args() + dry_run, bin_dir, tag_name = parse_args() + log.debug("args: dry_run=%s; bin_dir=%s; tag_name=%s", dry_run, bin_dir, tag_name) setup_logging() if bin_dir is None: + log.debug("no bin_dir specified, downloading latest release") + assert tag_name is None, f"--tag={tag_name} conflicts with downloading" certified, tag_name = download("latest", dry_run=False) else: - if os.environ.get("GIT_VERSION") is None: - raise KeyError("Missing DIR argument and GIT_VERSION environment variable") - else: - tag_name = os.environ["GIT_VERSION"] certified = find_bins(bin_dir) + if tag_name is None: + raise ValueError("tag_name is None") + assert re.match( + r"^v\d+\.\d+\.\d+(-\w+)?", tag_name + ), f"Invalid tag_name: {tag_name}" check_platforms(certified) if not dry_run: @@ -79,8 +96,11 @@ def parse_args() -> Tuple[bool, Optional[Path]]: dist_dir.mkdir(exist_ok=True) for tar_gz in certified: + log.info("Processing %s", tar_gz) llvm_triple = get_llvm_triple(tar_gz) + log.debug("llvm_triple=%s", llvm_triple) platform = LLVM_TRIPLES_TO_PYTHON_WHEEL_PLATFORMS[llvm_triple] + log.debug("platform=%s", platform) if platform is None: raise ValueError(f"Unsupported platform: {llvm_triple}") # TODO: avoid writing the extracted bin to disk diff --git a/wrappers/python/scripts/build/api_package.py b/wrappers/python/scripts/build/api_package.py index 9a26e994..99de3364 100644 --- a/wrappers/python/scripts/build/api_package.py +++ b/wrappers/python/scripts/build/api_package.py @@ -2,30 +2,92 @@ # optional dependencies. It might be preferable to use setuptools directly rather than # work around poetry. -from . import python_root, setup_logging +import logging import subprocess import re -import os +from argparse import ArgumentParser + +from . import python_root, setup_logging pyproject_toml = python_root / "pyproject.toml" +cli = ArgumentParser() +cli.add_argument("--dry-run", action="store_true") +cli.add_argument("--tag", required=True, help="The version to build.") +log = logging.getLogger(__name__) + + +def process_tag(tag: str) -> str: + """Convert a git tag to a version string compliant with PEP 440. + See https://peps.python.org/pep-0440/#public-version-identifiers + """ + pattern = ( + # note that this pattern accepts a superset of the tagging pattern used + # in this repository. + r"^v(?P\d+)" + r"\.(?P\d+)" + r"\.(?P\d+)" + r"(-" + r"(?Palpha|beta|rc)" + r"\.?(?P\d+)" + ")?" + ) + parts = re.match(pattern, tag) + if parts is None: + raise ValueError(f"Invalid tag: `{tag}` does not match pattern `{pattern}`") + major = int(parts["major"]) + minor = int(parts["minor"]) + patch = int(parts["patch"]) + suffix = "" + + if (prerelease_kind := parts["prerelease_kind"]) is not None: + if prerelease_kind == "rc": + suffix = "rc" + elif prerelease_kind.startswith("alpha"): + suffix = "a" + elif prerelease_kind.startswith("beta"): + suffix = "b" + if (prerelease_number := parts["prerelease_number"]) is not None: + suffix += str(int(prerelease_number)) + + return f"{major}.{minor}.{patch}{suffix}" + def main() -> None: - version = os.environ.get("PAGEFIND_VERSION") - if version is None: - version = "1" + setup_logging() + args = cli.parse_args() + tag: str = args.tag + dry_run: bool = args.dry_run + log.debug("args: dry_run=%s; tag=%s", dry_run, tag) + version = process_tag(tag) + + log.info("Building version %s", version) + # create a pyproject.toml with updated versions original = pyproject_toml.read_text() temp = "" for line in original.splitlines(): - if line.endswith("#!!opt"): - line = line.removeprefix("# ") + "\n" + if "0.0.0a0" in line: + line = line.replace("0.0.0a0", version) + log.debug("patching: %s", line) + elif line.endswith("#!!opt"): + line = line.removeprefix("# ").removesuffix("#!!opt") line = re.sub(r'version = "[^"]+"', f'version = "~={version}"', line) + log.debug("patching: %s", line) temp += line + "\n" + log.debug("patched pyproject.toml", extra={"updated": temp}) + + if dry_run: + return + with pyproject_toml.open("w") as f: f.write(temp) + log.debug("wrote patched pyproject.toml") + + log.info("Building API package") subprocess.run(["poetry", "build"], check=True) - with pyproject_toml.open("w") as f: + with pyproject_toml.open("w") as f: # restore the original f.write(original) + log.debug("restored original pyproject.toml") if __name__ == "__main__":