# SPDX-License-Identifier: BSD-2-Clause # Copyright (c) 2024 Phil Thompson import base64 import hashlib import os import shutil import sys from ..exceptions import UserException from ..pyproject import PyProject from ..version import SIP_VERSION_STR # The wheel format defined in PEP 427. WHEEL_VERSION = '1.0' def distinfo(name, console_scripts, gui_scripts, generator, generator_version, inventory, metadata_overrides, prefix, project_root, requires_dists, wheel_tag): """ Create and populate a .dist-info directory from an inventory file. """ if prefix is None: prefix = '' # Read the list of installed files. with open(inventory) as inventory_f: installed_lines = inventory_f.read().strip() installed = installed_lines.split('\n') if installed_lines else [] # Get the pyproject.toml file. saved = os.getcwd() os.chdir(project_root) pyproject = PyProject() os.chdir(saved) # Get the metadata and update it from the command line. metadata = pyproject.get_metadata() if metadata_overrides is not None: for oride in metadata_overrides: parts = oride.split('=', maxsplit=1) or_name = parts[0].strip() or_value = parts[1].strip() if len(parts) == 2 else '' metadata[or_name] = or_value # Create the directory. create_distinfo(name, wheel_tag, installed, metadata, requires_dists, project_root, console_scripts, gui_scripts, prefix_dir=prefix, generator=generator, generator_version=generator_version) def create_distinfo(distinfo_dir, wheel_tag, installed, metadata, requires_dists, project_root, console_scripts, gui_scripts, prefix_dir='', generator=None, generator_version=None): """ Create and populate a .dist-info directory. """ if generator is None: generator = 'sipbuild' if generator_version is None: generator_version = SIP_VERSION_STR # The prefix directory corresponds to DESTDIR or INSTALL_ROOT. real_distinfo_dir = prefix_dir + distinfo_dir # Make sure we have an empty dist-info directory. Handle exceptions as the # user may be trying something silly with a system directory. if os.path.exists(real_distinfo_dir): try: shutil.rmtree(real_distinfo_dir) except Exception as e: raise UserException( "unable remove old dist-info directory '{}'".format( real_distinfo_dir), str(e)) try: os.mkdir(real_distinfo_dir) except Exception as e: raise UserException( "unable create dist-info directory '{}'".format( real_distinfo_dir), str(e)) # Reproducable builds. installed.sort() if wheel_tag is None: # Create the INSTALLER file. installer_fn = os.path.join(distinfo_dir, 'INSTALLER') installed.append(installer_fn) with open(prefix_dir + installer_fn, 'w') as installer_f: print(generator, file=installer_f) else: # Define any entry points. if console_scripts or gui_scripts: eps_fn = os.path.join(distinfo_dir, 'entry_points.txt') installed.append(eps_fn) with open(prefix_dir + eps_fn, 'w') as eps_f: if console_scripts: eps_f.write( '[console_scripts]\n' + '\n'.join( console_scripts) + '\n') if gui_scripts: eps_f.write( '[gui_scripts]\n' + '\n'.join(gui_scripts) + '\n') # Create the WHEEL file. WHEEL = '''Wheel-Version: {} Generator: {} {} Root-Is-Purelib: false Tag: {} ''' wheel_fn = os.path.join(distinfo_dir, 'WHEEL') installed.append(wheel_fn) with open(prefix_dir + wheel_fn, 'w') as wheel_f: wheel_f.write( WHEEL.format(WHEEL_VERSION, generator, generator_version, wheel_tag)) # Create the METADATA file. metadata_fn = os.path.join(distinfo_dir, 'METADATA') write_metadata(metadata, requires_dists, metadata_fn, project_root, prefix_dir=prefix_dir) installed.append(metadata_fn) # Create the RECORD file. record_fn = os.path.join(distinfo_dir, 'RECORD') distinfo_path, distinfo_base = os.path.split(distinfo_dir) real_distinfo_path = os.path.normcase(prefix_dir + distinfo_path) with open(prefix_dir + record_fn, 'w') as record_f: for name in installed: real_name = prefix_dir + name if os.path.isdir(real_name): all_fns = [] for root, dirs, files in os.walk(real_name): # Reproducable builds. dirs.sort() files.sort() for f in files: all_fns.append(os.path.join(root, f)) if '__pycache__' in dirs: dirs.remove('__pycache__') else: all_fns = [real_name] for fn in all_fns: norm_fn = os.path.normcase(fn) if norm_fn.startswith(real_distinfo_path): fn_name = fn[len(real_distinfo_path) + 1:].replace('\\', '/') elif norm_fn.startswith(prefix_dir + sys.prefix): fn_name = os.path.relpath( fn, real_distinfo_path).replace('\\', '/') else: fn_name = fn[len(prefix_dir):] fn_f = open(fn, 'rb') data = fn_f.read() fn_f.close() digest = base64.urlsafe_b64encode( hashlib.sha256(data).digest()).rstrip(b'=').decode('ascii') record_f.write( '{},sha256={},{}\n'.format(fn_name, digest, len(data))) record_f.write('{}/RECORD,,\n'.format(distinfo_base)) def write_metadata(metadata, requires_dists, metadata_fn, project_root, prefix_dir=''): """ Write the meta-data, with additional requirements to a file. """ if requires_dists: rd = metadata.get('requires-dist', []) if isinstance(rd, str): rd = [rd] metadata['requires-dist'] = requires_dists + rd with open(prefix_dir + metadata_fn, 'w') as metadata_f: description = None # Do these first for cosmetic reasons. for name in ('metadata-version', 'name', 'version', 'requires-python'): _write_metadata_item(name, metadata.pop(name), metadata_f) for name, value in metadata.items(): if name == 'description-file': description = value else: _write_metadata_item(name, value, metadata_f) if description is not None: metadata_f.write('\n') # The description file uses posix separators. description = description.replace('/', os.sep) with open(os.path.join(project_root, description)) as description_f: metadata_f.write(description_f.read()) def _write_metadata_item(name, value, metadata_f): """ Write a single metadata item. """ if isinstance(value, str): value = [value] for v in value: metadata_f.write('{}: {}\n'.format(name.title(), v))