blob: 2310abb91ae397a4f8fc8b035837c562fe40778c [file]
#!/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 legal files and native resources in binary convenience artifacts."""
import argparse
import glob
import json
import subprocess
import sys
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
LEGAL_FILES = ("LICENSE", "NOTICE", "LICENSE-binary")
WHEEL_LEGAL_SOURCES = {
"LICENSE": "LICENSE",
"NOTICE": "NOTICE",
"LICENSE-binary": "LICENSE-binary-ffi",
}
JAR_LEGAL_SOURCES = {
"LICENSE-binary": "LICENSE-binary",
}
JAR_NATIVE_FILES = (
"native/linux/x86_64/libpaimon_vindex_jni.so",
"native/linux/aarch64/libpaimon_vindex_jni.so",
"native/macos/aarch64/libpaimon_vindex_jni.dylib",
"native/windows/x86_64/paimon_vindex_jni.dll",
)
def resolve_artifacts(patterns):
artifacts = []
for pattern in patterns:
matches = sorted(glob.glob(pattern))
if not matches:
raise ValueError(f"artifact pattern matched no files: {pattern}")
artifacts.extend(Path(match) for match in matches)
return artifacts
def require_entries(archive, artifact, entries):
names = set(archive.namelist())
missing = [entry for entry in entries if entry not in names]
if missing:
raise ValueError(f"{artifact} is missing entries: {', '.join(missing)}")
def require_canonical_content(archive, artifact, archive_prefix, sources):
for name, source in sources.items():
entry = f"{archive_prefix}/{name}"
expected = (ROOT / source).read_bytes()
actual = archive.read(entry)
if actual != expected:
raise ValueError(f"{artifact}!/{entry} does not match repository {source}")
def verify_wheel(artifact):
with zipfile.ZipFile(artifact) as archive:
names = archive.namelist()
dist_info_metadata = [
name for name in names if name.endswith(".dist-info/METADATA")
]
if len(dist_info_metadata) != 1:
raise ValueError(
f"{artifact} must contain exactly one .dist-info/METADATA entry"
)
dist_info = dist_info_metadata[0][: -len("METADATA")]
legal_entries = [f"{dist_info}licenses/{name}" for name in LEGAL_FILES]
package_legal_entries = [f"paimon_vindex/{name}" for name in LEGAL_FILES]
require_entries(archive, artifact, legal_entries + package_legal_entries)
require_canonical_content(
archive, artifact, "paimon_vindex", WHEEL_LEGAL_SOURCES
)
require_canonical_content(
archive,
artifact,
f"{dist_info}licenses".rstrip("/"),
WHEEL_LEGAL_SOURCES,
)
native_names = [
name
for name in names
if name.startswith("paimon_vindex/")
and name.endswith((".so", ".dylib", ".dll"))
]
if len(native_names) != 1:
raise ValueError(
f"{artifact} must contain exactly one platform native library, found "
f"{len(native_names)}"
)
print(f"Verified Python wheel: {artifact}")
def verify_jar(artifact, native_files):
with zipfile.ZipFile(artifact) as archive:
required = (
"META-INF/LICENSE",
"META-INF/NOTICE",
"META-INF/LICENSE-binary",
"org/apache/paimon/index/vector/NativeLibraryLoader.class",
) + tuple(native_files)
require_entries(archive, artifact, required)
require_canonical_content(
archive,
artifact,
"META-INF",
JAR_LEGAL_SOURCES,
)
print(f"Verified Java JAR: {artifact}")
def verify_source_legal_inventory():
metadata = json.loads(
subprocess.check_output(
["cargo", "metadata", "--format-version", "1", "--locked"],
cwd=ROOT,
text=True,
)
)
packages = {package["id"]: package for package in metadata["packages"]}
nodes = {node["id"]: node for node in metadata["resolve"]["nodes"]}
for root_name, license_name in (
("paimon-vindex-ffi", "LICENSE-binary-ffi"),
("paimon-vindex-jni", "LICENSE-binary"),
):
root_id = next(
package["id"]
for package in metadata["packages"]
if package["source"] is None and package["name"] == root_name
)
resolved = set()
pending = [root_id]
while pending:
package_id = pending.pop()
if package_id in resolved:
continue
resolved.add(package_id)
for dependency in nodes[package_id]["deps"]:
if any(
kind["kind"] in (None, "normal")
for kind in dependency["dep_kinds"]
):
pending.append(dependency["pkg"])
binary_license = (ROOT / license_name).read_text(encoding="utf-8")
missing = []
for package_id in resolved:
package = packages[package_id]
if package["source"] is None:
continue
expression = package.get("license") or ""
needs_additional_text = (
"Apache-2.0" not in expression or " AND " in expression
)
marker = f"{package['name']} {package['version']}"
if needs_additional_text and marker not in binary_license:
missing.append(f"{marker} ({expression})")
if missing:
raise ValueError(
f"{license_name} does not account for {root_name} runtime dependencies: "
+ ", ".join(sorted(missing))
)
for name in (
"LICENSE",
"NOTICE",
"LICENSE-binary",
"LICENSE-binary-ffi",
"ffi/DEPENDENCIES.rust.tsv",
"jni/DEPENDENCIES.rust.tsv",
):
if not (ROOT / name).is_file():
raise ValueError(f"repository legal file is missing: {name}")
print("Verified binary legal inventory against native Rust dependencies")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--wheel", nargs="+", default=[], help="Wheel path or glob")
parser.add_argument("--jar", nargs="+", default=[], help="JAR path or glob")
parser.add_argument(
"--jar-native",
action="append",
choices=JAR_NATIVE_FILES,
help=(
"Native entry required in a JAR. Repeat for a partial-platform CI build; "
"the default requires every release platform."
),
)
parser.add_argument(
"--source",
action="store_true",
help="Verify LICENSE-binary against native Rust runtime dependencies",
)
args = parser.parse_args()
if not args.wheel and not args.jar and not args.source:
parser.error("at least one --source, --wheel, or --jar is required")
try:
if args.source:
verify_source_legal_inventory()
for artifact in resolve_artifacts(args.wheel):
verify_wheel(artifact)
for artifact in resolve_artifacts(args.jar):
verify_jar(artifact, args.jar_native or JAR_NATIVE_FILES)
except (OSError, ValueError, zipfile.BadZipFile, KeyError) as error:
print(f"Binary artifact verification failed: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())