| #!/usr/bin/env python3 |
| |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Verify Go native release artifacts and their target-specific licenses.""" |
| |
| from __future__ import annotations |
| |
| import argparse |
| import subprocess |
| import sys |
| import tempfile |
| from dataclasses import dataclass |
| from pathlib import Path |
| |
| |
| @dataclass(frozen=True) |
| class Artifact: |
| library: str |
| report: str |
| format_markers: tuple[str, ...] |
| architecture_markers: tuple[str, ...] |
| |
| |
| ARTIFACTS = { |
| "x86_64-unknown-linux-gnu": Artifact( |
| library="libpaimon_c.linux.amd64.so.zst", |
| report="THIRD-PARTY-LICENSES.linux.amd64.html", |
| format_markers=("elf 64-bit", "shared object"), |
| architecture_markers=("x86-64", "x86_64"), |
| ), |
| "aarch64-unknown-linux-gnu": Artifact( |
| library="libpaimon_c.linux.arm64.so.zst", |
| report="THIRD-PARTY-LICENSES.linux.arm64.html", |
| format_markers=("elf 64-bit", "shared object"), |
| architecture_markers=("aarch64", "arm64"), |
| ), |
| "x86_64-apple-darwin": Artifact( |
| library="libpaimon_c.darwin.amd64.dylib.zst", |
| report="THIRD-PARTY-LICENSES.darwin.amd64.html", |
| format_markers=("mach-o 64-bit", "dynamically linked shared library"), |
| architecture_markers=("x86_64", "x86-64"), |
| ), |
| "aarch64-apple-darwin": Artifact( |
| library="libpaimon_c.darwin.arm64.dylib.zst", |
| report="THIRD-PARTY-LICENSES.darwin.arm64.html", |
| format_markers=("mach-o 64-bit", "dynamically linked shared library"), |
| architecture_markers=("arm64", "aarch64"), |
| ), |
| } |
| |
| LICENSE_PLACEHOLDERS = ( |
| "<year>", |
| "<owner>", |
| "<copyright holders>", |
| "<year>", |
| "<owner>", |
| "<copyright holders>", |
| ) |
| AVRO_NOTICE_MARKERS = ( |
| "Apache Avro", |
| "Flavien Raynaud", |
| "donated to the Apache Avro project in 2020.", |
| ) |
| |
| |
| def repository_root() -> Path: |
| return Path(__file__).resolve().parent.parent |
| |
| |
| def require_file(path: Path) -> None: |
| if not path.is_file() or path.stat().st_size == 0: |
| raise ValueError(f"missing or empty release file: {path}") |
| |
| |
| def verify_avro_notice(content: str, description: str) -> None: |
| for marker in AVRO_NOTICE_MARKERS: |
| if marker not in content: |
| raise ValueError(f"{description} is missing {marker!r}") |
| |
| |
| def verify_report(report: Path, target: str) -> None: |
| require_file(report) |
| report_text = report.read_text(encoding="utf-8") |
| required_markers = ( |
| "<strong>Artifact:</strong> <code>go</code>", |
| f"<strong>Rust target:</strong> <code>{target}</code>", |
| "<strong>Root manifest:</strong> <code>bindings/c/Cargo.toml</code>", |
| ) |
| for marker in required_markers: |
| if marker not in report_text: |
| raise ValueError(f"{report} is missing {marker!r}") |
| for placeholder in LICENSE_PLACEHOLDERS: |
| if placeholder in report_text: |
| raise ValueError(f"{report} contains placeholder {placeholder!r}") |
| |
| |
| def verify_library(library: Path, artifact: Artifact) -> None: |
| require_file(library) |
| with tempfile.TemporaryDirectory(prefix="paimon-go-release-") as temp_dir: |
| unpacked = Path(temp_dir) / library.name.removesuffix(".zst") |
| with unpacked.open("wb") as destination: |
| result = subprocess.run( |
| ["zstd", "-d", "-q", "-c", str(library)], |
| stdout=destination, |
| stderr=subprocess.PIPE, |
| text=True, |
| check=False, |
| ) |
| if result.returncode != 0: |
| raise ValueError(f"cannot decompress {library}: {result.stderr.strip()}") |
| require_file(unpacked) |
| description = subprocess.check_output( |
| ["file", "-b", str(unpacked)], text=True |
| ).strip() |
| |
| normalized = description.lower() |
| if any(marker not in normalized for marker in artifact.format_markers): |
| raise ValueError(f"unexpected binary format for {library}: {description}") |
| if not any(marker in normalized for marker in artifact.architecture_markers): |
| raise ValueError(f"unexpected binary architecture for {library}: {description}") |
| |
| |
| def verify_target(root: Path, artifacts_dir: Path, target: str) -> None: |
| artifact = ARTIFACTS[target] |
| library = artifacts_dir / artifact.library |
| report = artifacts_dir / artifact.report |
| verify_library(library, artifact) |
| verify_report(report, target) |
| print(f"verified {artifact.library}: {target}") |
| |
| |
| def verify_release_legal_files(root: Path, artifacts_dir: Path) -> None: |
| license_path = artifacts_dir / "LICENSE" |
| require_file(license_path) |
| license_content = license_path.read_text(encoding="utf-8") |
| apache_license = (root / "LICENSE").read_text(encoding="utf-8").rstrip() |
| if not license_content.startswith(apache_license + "\n"): |
| raise ValueError("binary LICENSE does not begin with the Apache License") |
| for artifact in ARTIFACTS.values(): |
| if artifact.report not in license_content: |
| raise ValueError(f"binary LICENSE is missing {artifact.report}") |
| |
| notice_path = artifacts_dir / "NOTICE" |
| require_file(notice_path) |
| notice = notice_path.read_text(encoding="utf-8") |
| verify_avro_notice(notice, "NOTICE") |
| |
| |
| def main() -> int: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "--artifacts-dir", |
| required=True, |
| type=Path, |
| help="directory containing downloaded or locally built release assets", |
| ) |
| parser.add_argument( |
| "--target", |
| choices=tuple(ARTIFACTS), |
| help="verify one matrix target; omit to verify the complete release set", |
| ) |
| args = parser.parse_args() |
| |
| root = repository_root() |
| artifacts_dir = args.artifacts_dir.resolve() |
| try: |
| if args.target: |
| verify_target(root, artifacts_dir, args.target) |
| else: |
| verify_release_legal_files(root, artifacts_dir) |
| for target in ARTIFACTS: |
| verify_target(root, artifacts_dir, target) |
| except (OSError, UnicodeError, ValueError, subprocess.SubprocessError) as error: |
| print(f"Go release artifact verification failed: {error}", file=sys.stderr) |
| return 1 |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |