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