diff --git a/make_wheels.py b/make_wheels.py index d6b21ae..1fc834e 100644 --- a/make_wheels.py +++ b/make_wheels.py @@ -2,11 +2,13 @@ import logging import io import os +import re import json import hashlib import tarfile +from warnings import warn import urllib.request -from pathlib import Path +from pathlib import Path, PurePath from email.message import EmailMessage from wheel.wheelfile import WheelFile from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED @@ -28,6 +30,8 @@ 'powerpc64le-linux': 'manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le', } +_YELLOW = "\033[93m" + class ReproducibleWheelFile(WheelFile): def writestr(self, zinfo_or_arcname, data, *args, **kwargs): @@ -70,13 +74,17 @@ def write_wheel_file(filename, contents): def write_wheel(out_dir, *, name, version, tag, metadata, description, contents): wheel_name = f'{name}-{version}-{tag}.whl' dist_info = f'{name}-{version}.dist-info' + filtered_metadata = [] + for header, value in metadata: + filtered_metadata.append((header, value)) + return write_wheel_file(os.path.join(out_dir, wheel_name), { **contents, f'{dist_info}/METADATA': make_message([ ('Metadata-Version', '2.4'), ('Name', name), ('Version', version), - *metadata, + *filtered_metadata, ], description), f'{dist_info}/WHEEL': make_message([ ('Wheel-Version', '1.0'), @@ -107,6 +115,55 @@ def write_ziglang_wheel(out_dir, *, version, platform, archive): contents = {} contents['ziglang/__init__.py'] = b'' + license_files = {} + found_license_files = set() + potential_extra_licenses = set() + + # A bunch of standard license file patterns. If a file matches any of + # these, we need to add them to required_license_paths and metadata. + license_patterns = [ + r'COPYING.*', + r'COPYRIGHT.*', + r'COPYLEFT.*', + r'LICEN[CS]E.*', + r'LICEN[CS]E-.*', + r'LICEN[CS]E\..*', + r'PATENTS.*', + r'NOTICE.*', + r'LEGAL.*', + r'AUTHORS.*', + r'RIGHT*', + r'PERMISSION*', + r'THIRD[-_]PARTY[-_]LICENSES?.*', + r'EULA*', + r'MIT*', + r'GPL*', + r'AGPL*', + r'LGPL*', + r'APACHE*', + ] + license_regex = re.compile('|'.join(f'^{pattern}$' for pattern in license_patterns), re.IGNORECASE) + + # These file paths MUST remain in sync with the paths in the official + # Zig tarballs and with the ones defined below in the metadata. The + # script will raise an error if any of these files are not found in + # the archive. + required_license_paths = [ + 'LICENSE', + 'lib/libc/glibc/LICENSES', + 'lib/libc/mingw/COPYING', + 'lib/libc/musl/COPYRIGHT', + 'lib/libc/wasi/LICENSE', + 'lib/libc/wasi/LICENSE-APACHE', + 'lib/libc/wasi/LICENSE-APACHE-LLVM', + 'lib/libc/wasi/LICENSE-MIT', + 'lib/libc/wasi/libc-bottom-half/cloudlibc/LICENSE', + 'lib/libc/wasi/libc-top-half/musl/COPYRIGHT', + 'lib/libcxx/LICENSE.TXT', + 'lib/libcxxabi/LICENSE.TXT', + 'lib/libunwind/LICENSE.TXT', + ] + for entry_name, entry_mode, entry_data in iter_archive_contents(archive): entry_name = '/'.join(entry_name.split('/')[1:]) if not entry_name: @@ -114,10 +171,19 @@ def write_ziglang_wheel(out_dir, *, version, platform, archive): if entry_name.startswith('doc/'): continue + # Check for additional license-like files + potential_license_filename = PurePath(entry_name).name + if license_regex.match(potential_license_filename): + potential_extra_licenses.add(entry_name) + zip_info = ZipInfo(f'ziglang/{entry_name}') zip_info.external_attr = (entry_mode & 0xFFFF) << 16 contents[zip_info] = entry_data + if entry_name in required_license_paths: + license_files[entry_name] = entry_data + found_license_files.add(entry_name) + if entry_name.startswith('zig'): contents['ziglang/__main__.py'] = f'''\ import os, sys @@ -128,9 +194,31 @@ def write_ziglang_wheel(out_dir, *, version, platform, archive): import subprocess; sys.exit(subprocess.call(argv)) '''.encode('ascii') + # 1. Check for missing required licenses paths + missing_licenses = set(required_license_paths) - found_license_files + if missing_licenses: + error_message = ( + f"{_YELLOW}The following required license files were not found in the Zig archive: {', '.join(sorted(missing_licenses))} " + f"\nThis may indicate a change in Zig's license file structure or an error in the listing of license files and/or paths.{_YELLOW}" + ) + raise RuntimeError(error_message) + + # 2. Check for potentially missing license files + extra_licenses = potential_extra_licenses - set(required_license_paths) + if extra_licenses: + error_message = ( + f"{_YELLOW}Found additional potential license files in the Zig archive but not included in the metadata: {', '.join(sorted(extra_licenses))} " + f"\nPlease consider adding these to the license paths if they should be included.{_YELLOW}" + ) + raise RuntimeError(error_message) + with open('README.pypi.md') as f: description = f.read() + dist_info = f'ziglang-{version}.dist-info' + for license_path, license_data in license_files.items(): + contents[f'{dist_info}/licenses/ziglang/{license_path}'] = license_data + return write_wheel(out_dir, name='ziglang', version=version, @@ -138,8 +226,13 @@ def write_ziglang_wheel(out_dir, *, version, platform, archive): metadata=[ ('Summary', 'Zig is a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.'), ('Description-Content-Type', "'text/markdown'; charset=UTF-8; variant=GFM"), + # The license expression and the file paths MUST remain in sync + # with the paths in the official Zig tarballs and with the ones + # defined above in the contents. The difference is that these + # are prefixed with "ziglang/" to match the paths in the wheel + # for metadata compliance. ('License-Expression', 'MIT'), - ('License-File', 'LICENSE'), + ('License-File', 'ziglang/LICENSE'), ('License-File', 'ziglang/lib/libc/glibc/LICENSES'), ('License-File', 'ziglang/lib/libc/mingw/COPYING'), ('License-File', 'ziglang/lib/libc/musl/COPYRIGHT'), @@ -147,6 +240,8 @@ def write_ziglang_wheel(out_dir, *, version, platform, archive): ('License-File', 'ziglang/lib/libc/wasi/LICENSE-APACHE'), ('License-File', 'ziglang/lib/libc/wasi/LICENSE-APACHE-LLVM'), ('License-File', 'ziglang/lib/libc/wasi/LICENSE-MIT'), + ('License-File', 'ziglang/lib/libc/wasi/libc-bottom-half/cloudlibc/LICENSE'), + ('License-File', 'ziglang/lib/libc/wasi/libc-top-half/musl/COPYRIGHT'), ('License-File', 'ziglang/lib/libcxx/LICENSE.TXT'), ('License-File', 'ziglang/lib/libcxxabi/LICENSE.TXT'), ('License-File', 'ziglang/lib/libunwind/LICENSE.TXT'),