1# Copyright (c) 2020, Riverbank Computing Limited 2# All rights reserved. 3# 4# This copy of SIP is licensed for use under the terms of the SIP License 5# Agreement. See the file LICENSE for more details. 6# 7# This copy of SIP may also used under the terms of the GNU General Public 8# License v2 or v3 as published by the Free Software Foundation which can be 9# found in the files LICENSE-GPL2 and LICENSE-GPL3 included in this package. 10# 11# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 12# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 13# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 14# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 15# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 16# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 17# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 18# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 19# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 20# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 21# POSSIBILITY OF SUCH DAMAGE. 22 23 24import base64 25import hashlib 26import os 27import shutil 28import sys 29 30from ..exceptions import UserException 31from ..pyproject import PyProject 32from ..version import SIP_VERSION_STR 33 34 35# The wheel format defined in PEP 427. 36WHEEL_VERSION = '1.0' 37 38 39def distinfo(name, console_scripts, gui_scripts, generator, inventory, 40 metadata_overrides, prefix, project_root, requires_dists, wheel_tag): 41 """ Create and populate a .dist-info directory from an inventory file. """ 42 43 if prefix is None: 44 prefix = '' 45 46 # Read the list of installed files. 47 with open(inventory) as inventory_f: 48 installed_lines = inventory_f.read().strip() 49 installed = installed_lines.split('\n') if installed_lines else [] 50 51 # Get the pyproject.toml file. 52 saved = os.getcwd() 53 os.chdir(project_root) 54 pyproject = PyProject() 55 os.chdir(saved) 56 57 # Get the metadata and update it from the command line. 58 metadata = pyproject.get_metadata() 59 60 if metadata_overrides is not None: 61 for oride in metadata_overrides: 62 parts = oride.split('=', maxsplit=1) 63 or_name = parts[0].strip() 64 or_value = parts[1].strip() if len(parts) == 2 else '' 65 metadata[or_name] = or_value 66 67 # Create the directory. 68 create_distinfo(name, wheel_tag, installed, metadata, requires_dists, 69 project_root, console_scripts, gui_scripts, prefix_dir=prefix, 70 generator=generator) 71 72 73def create_distinfo(distinfo_dir, wheel_tag, installed, metadata, 74 requires_dists, project_root, console_scripts, gui_scripts, 75 prefix_dir='', generator=None): 76 """ Create and populate a .dist-info directory. """ 77 78 if generator is None: 79 generator = os.path.basename(sys.argv[0]) 80 81 # The prefix directory corresponds to DESTDIR or INSTALL_ROOT. 82 real_distinfo_dir = prefix_dir + distinfo_dir 83 84 # Make sure we have an empty dist-info directory. Handle exceptions as the 85 # user may be trying something silly with a system directory. 86 if os.path.exists(real_distinfo_dir): 87 try: 88 shutil.rmtree(real_distinfo_dir) 89 except Exception as e: 90 raise UserException( 91 "unable remove old dist-info directory '{}'".format( 92 real_distinfo_dir), 93 str(e)) 94 95 try: 96 os.mkdir(real_distinfo_dir) 97 except Exception as e: 98 raise UserException( 99 "unable create dist-info directory '{}'".format( 100 real_distinfo_dir), 101 str(e)) 102 103 # Reproducable builds. 104 installed.sort() 105 106 if wheel_tag is None: 107 # Create the INSTALLER file. 108 installer_fn = os.path.join(distinfo_dir, 'INSTALLER') 109 installed.append(installer_fn) 110 111 with open(prefix_dir + installer_fn, 'w') as installer_f: 112 print(generator, file=installer_f) 113 else: 114 # Define any entry points. 115 if console_scripts or gui_scripts: 116 eps_fn = os.path.join(distinfo_dir, 'entry_points.txt') 117 installed.append(eps_fn) 118 119 with open(prefix_dir + eps_fn, 'w') as eps_f: 120 if console_scripts: 121 eps_f.write( 122 '[console_scripts]\n' + '\n'.join( 123 console_scripts) + '\n') 124 125 if gui_scripts: 126 eps_f.write( 127 '[gui_scripts]\n' + '\n'.join(gui_scripts) + '\n') 128 129 # Create the WHEEL file. 130 WHEEL = '''Wheel-Version: {} 131Generator: {} {} 132Root-Is_Purelib: false 133Tag: {} 134''' 135 136 wheel_fn = os.path.join(distinfo_dir, 'WHEEL') 137 installed.append(wheel_fn) 138 139 with open(prefix_dir + wheel_fn, 'w') as wheel_f: 140 wheel_f.write( 141 WHEEL.format(WHEEL_VERSION, generator, SIP_VERSION_STR, 142 wheel_tag)) 143 144 # Create the METADATA file. 145 metadata_fn = os.path.join(distinfo_dir, 'METADATA') 146 write_metadata(metadata, requires_dists, metadata_fn, project_root, 147 prefix_dir=prefix_dir) 148 installed.append(metadata_fn) 149 150 # Create the RECORD file. 151 record_fn = os.path.join(distinfo_dir, 'RECORD') 152 153 distinfo_path, distinfo_base = os.path.split(distinfo_dir) 154 real_distinfo_path = os.path.normcase(prefix_dir + distinfo_path) 155 156 with open(prefix_dir + record_fn, 'w') as record_f: 157 for name in installed: 158 real_name = prefix_dir + name 159 if os.path.isdir(real_name): 160 all_fns = [] 161 162 for root, dirs, files in os.walk(real_name): 163 # Reproducable builds. 164 dirs.sort() 165 files.sort() 166 167 for f in files: 168 all_fns.append(os.path.join(root, f)) 169 170 if '__pycache__' in dirs: 171 dirs.remove('__pycache__') 172 else: 173 all_fns = [real_name] 174 175 for fn in all_fns: 176 norm_fn = os.path.normcase(fn) 177 178 if norm_fn.startswith(real_distinfo_path): 179 fn_name = fn[len(real_distinfo_path) + 1:].replace('\\', '/') 180 elif norm_fn.startswith(prefix_dir + sys.prefix): 181 fn_name = os.path.relpath( 182 fn, real_distinfo_path).replace('\\', '/') 183 else: 184 fn_name = fn[len(prefix_dir):] 185 186 fn_f = open(fn, 'rb') 187 data = fn_f.read() 188 fn_f.close() 189 190 digest = base64.urlsafe_b64encode( 191 hashlib.sha256(data).digest()).rstrip(b'=').decode('ascii') 192 193 record_f.write( 194 '{},sha256={},{}\n'.format(fn_name, digest, len(data))) 195 196 record_f.write('{}/RECORD,,\n'.format(distinfo_base)) 197 198 199def write_metadata(metadata, requires_dists, metadata_fn, project_root, 200 prefix_dir=''): 201 """ Write the meta-data, with additional requirements to a file. """ 202 203 if requires_dists: 204 rd = metadata.get('requires-dist', []) 205 if isinstance(rd, str): 206 rd = [rd] 207 208 metadata['requires-dist'] = requires_dists + rd 209 210 with open(prefix_dir + metadata_fn, 'w') as metadata_f: 211 description = None 212 213 for name, value in metadata.items(): 214 if name == 'description-file': 215 description = value 216 else: 217 if isinstance(value, str): 218 value = [value] 219 220 for v in value: 221 metadata_f.write('{}: {}\n'.format(name.title(), v)) 222 223 if description is not None: 224 metadata_f.write('\n') 225 226 # The description file uses posix separators. 227 description = description.replace('/', os.sep) 228 229 with open(os.path.join(project_root, description)) as description_f: 230 metadata_f.write(description_f.read()) 231