• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2018 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import hashlib
16import os
17import platform
18import stat
19import subprocess
20import unittest
21import zipfile
22
23from python.runfiles import runfiles
24
25
26class WheelTest(unittest.TestCase):
27    maxDiff = None
28
29    def setUp(self):
30        super().setUp()
31        self.runfiles = runfiles.Create()
32
33    def _get_path(self, filename):
34        runfiles_path = os.path.join("rules_python/examples/wheel", filename)
35        path = self.runfiles.Rlocation(runfiles_path)
36        # The runfiles API can return None if the path doesn't exist or
37        # can't be resolved.
38        if not path:
39            raise AssertionError(f"Runfiles failed to resolve {runfiles_path}")
40        elif not os.path.exists(path):
41            # A non-None value doesn't mean the file actually exists, though
42            raise AssertionError(
43                f"Path {path} does not exist (from runfiles path {runfiles_path}"
44            )
45        else:
46            return path
47
48    def assertFileSha256Equal(self, filename, want):
49        hash = hashlib.sha256()
50        with open(filename, "rb") as f:
51            while True:
52                buf = f.read(2**20)
53                if not buf:
54                    break
55                hash.update(buf)
56        self.assertEqual(want, hash.hexdigest())
57
58    def assertAllEntriesHasReproducibleMetadata(self, zf):
59        for zinfo in zf.infolist():
60            self.assertEqual(zinfo.date_time, (1980, 1, 1, 0, 0, 0), msg=zinfo.filename)
61            self.assertEqual(zinfo.create_system, 3, msg=zinfo.filename)
62            self.assertEqual(
63                zinfo.external_attr,
64                (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO | stat.S_IFREG) << 16,
65                msg=zinfo.filename,
66            )
67            self.assertEqual(
68                zinfo.compress_type, zipfile.ZIP_DEFLATED, msg=zinfo.filename
69            )
70
71    def test_py_library_wheel(self):
72        filename = self._get_path("example_minimal_library-0.0.1-py3-none-any.whl")
73        with zipfile.ZipFile(filename) as zf:
74            self.assertAllEntriesHasReproducibleMetadata(zf)
75            self.assertEqual(
76                zf.namelist(),
77                [
78                    "examples/wheel/lib/module_with_data.py",
79                    "examples/wheel/lib/simple_module.py",
80                    "example_minimal_library-0.0.1.dist-info/WHEEL",
81                    "example_minimal_library-0.0.1.dist-info/METADATA",
82                    "example_minimal_library-0.0.1.dist-info/RECORD",
83                ],
84            )
85        self.assertFileSha256Equal(
86            filename, "79a4e9c1838c0631d5d8fa49a26efd6e9a364f6b38d9597c0f6df112271a0e28"
87        )
88
89    def test_py_package_wheel(self):
90        filename = self._get_path(
91            "example_minimal_package-0.0.1-py3-none-any.whl",
92        )
93        with zipfile.ZipFile(filename) as zf:
94            self.assertAllEntriesHasReproducibleMetadata(zf)
95            self.assertEqual(
96                zf.namelist(),
97                [
98                    "examples/wheel/lib/data,with,commas.txt",
99                    "examples/wheel/lib/data.txt",
100                    "examples/wheel/lib/module_with_data.py",
101                    "examples/wheel/lib/simple_module.py",
102                    "examples/wheel/main.py",
103                    "example_minimal_package-0.0.1.dist-info/WHEEL",
104                    "example_minimal_package-0.0.1.dist-info/METADATA",
105                    "example_minimal_package-0.0.1.dist-info/RECORD",
106                ],
107            )
108        self.assertFileSha256Equal(
109            filename, "82370bf61310e2d3c7b1218368457dc7e161bf5dc1a280d7d45102b5e56acf43"
110        )
111
112    def test_customized_wheel(self):
113        filename = self._get_path(
114            "example_customized-0.0.1-py3-none-any.whl",
115        )
116        with zipfile.ZipFile(filename) as zf:
117            self.assertAllEntriesHasReproducibleMetadata(zf)
118            self.assertEqual(
119                zf.namelist(),
120                [
121                    "examples/wheel/lib/data,with,commas.txt",
122                    "examples/wheel/lib/data.txt",
123                    "examples/wheel/lib/module_with_data.py",
124                    "examples/wheel/lib/simple_module.py",
125                    "examples/wheel/main.py",
126                    "example_customized-0.0.1.dist-info/WHEEL",
127                    "example_customized-0.0.1.dist-info/METADATA",
128                    "example_customized-0.0.1.dist-info/entry_points.txt",
129                    "example_customized-0.0.1.dist-info/NOTICE",
130                    "example_customized-0.0.1.dist-info/README",
131                    "example_customized-0.0.1.dist-info/RECORD",
132                ],
133            )
134            record_contents = zf.read("example_customized-0.0.1.dist-info/RECORD")
135            wheel_contents = zf.read("example_customized-0.0.1.dist-info/WHEEL")
136            metadata_contents = zf.read("example_customized-0.0.1.dist-info/METADATA")
137            entry_point_contents = zf.read(
138                "example_customized-0.0.1.dist-info/entry_points.txt"
139            )
140
141            self.assertEqual(
142                record_contents,
143                # The entries are guaranteed to be sorted.
144                b"""\
145"examples/wheel/lib/data,with,commas.txt",sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12
146examples/wheel/lib/data.txt,sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12
147examples/wheel/lib/module_with_data.py,sha256=8s0Khhcqz3yVsBKv2IB5u4l4TMKh7-c_V6p65WVHPms,637
148examples/wheel/lib/simple_module.py,sha256=z2hwciab_XPNIBNH8B1Q5fYgnJvQTeYf0ZQJpY8yLLY,637
149examples/wheel/main.py,sha256=sgg5iWN_9inYBjm6_Zw27hYdmo-l24fA-2rfphT-IlY,909
150example_customized-0.0.1.dist-info/WHEEL,sha256=sobxWSyDDkdg_rinUth-jxhXHqoNqlmNMJY3aTZn2Us,91
151example_customized-0.0.1.dist-info/METADATA,sha256=QYQcDJFQSIqan8eiXqL67bqsUfgEAwf2hoK_Lgi1S-0,559
152example_customized-0.0.1.dist-info/entry_points.txt,sha256=pqzpbQ8MMorrJ3Jp0ntmpZcuvfByyqzMXXi2UujuXD0,137
153example_customized-0.0.1.dist-info/NOTICE,sha256=Xpdw-FXET1IRgZ_wTkx1YQfo1-alET0FVf6V1LXO4js,76
154example_customized-0.0.1.dist-info/README,sha256=WmOFwZ3Jga1bHG3JiGRsUheb4UbLffUxyTdHczS27-o,40
155example_customized-0.0.1.dist-info/RECORD,,
156""",
157            )
158            self.assertEqual(
159                wheel_contents,
160                b"""\
161Wheel-Version: 1.0
162Generator: bazel-wheelmaker 1.0
163Root-Is-Purelib: true
164Tag: py3-none-any
165""",
166            )
167            self.assertEqual(
168                metadata_contents,
169                b"""\
170Metadata-Version: 2.1
171Name: example_customized
172Author: Example Author with non-ascii characters: \xc5\xbc\xc3\xb3\xc5\x82w
173Author-email: example@example.com
174Home-page: www.example.com
175License: Apache 2.0
176Description-Content-Type: text/markdown
177Summary: A one-line summary of this test package
178Project-URL: Bug Tracker, www.example.com/issues
179Project-URL: Documentation, www.example.com/docs
180Classifier: License :: OSI Approved :: Apache Software License
181Classifier: Intended Audience :: Developers
182Requires-Dist: pytest
183Version: 0.0.1
184
185This is a sample description of a wheel.
186""",
187            )
188            self.assertEqual(
189                entry_point_contents,
190                b"""\
191[console_scripts]
192another = foo.bar:baz
193customized_wheel = examples.wheel.main:main
194
195[group2]
196first = first.main:f
197second = second.main:s""",
198            )
199        self.assertFileSha256Equal(
200            filename, "706e8dd45884d8cb26e92869f7d29ab7ed9f683b4e2d08f06c03dbdaa12191b8"
201        )
202
203    def test_filename_escaping(self):
204        filename = self._get_path(
205            "file_name_escaping-0.0.1rc1+ubuntu.r7-py3-none-any.whl",
206        )
207        with zipfile.ZipFile(filename) as zf:
208            self.assertEqual(
209                zf.namelist(),
210                [
211                    "examples/wheel/lib/data,with,commas.txt",
212                    "examples/wheel/lib/data.txt",
213                    "examples/wheel/lib/module_with_data.py",
214                    "examples/wheel/lib/simple_module.py",
215                    "examples/wheel/main.py",
216                    # PEP calls for replacing only in the archive filename.
217                    # Alas setuptools also escapes in the dist-info directory
218                    # name, so let's be compatible.
219                    "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/WHEEL",
220                    "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/METADATA",
221                    "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/RECORD",
222                ],
223            )
224            metadata_contents = zf.read(
225                "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/METADATA"
226            )
227            self.assertEqual(
228                metadata_contents,
229                b"""\
230Metadata-Version: 2.1
231Name: File--Name-Escaping
232Version: 0.0.1rc1+ubuntu.r7
233
234UNKNOWN
235""",
236            )
237
238    def test_custom_package_root_wheel(self):
239        filename = self._get_path(
240            "examples_custom_package_root-0.0.1-py3-none-any.whl",
241        )
242
243        with zipfile.ZipFile(filename) as zf:
244            self.assertAllEntriesHasReproducibleMetadata(zf)
245            self.assertEqual(
246                zf.namelist(),
247                [
248                    "wheel/lib/data,with,commas.txt",
249                    "wheel/lib/data.txt",
250                    "wheel/lib/module_with_data.py",
251                    "wheel/lib/simple_module.py",
252                    "wheel/main.py",
253                    "examples_custom_package_root-0.0.1.dist-info/WHEEL",
254                    "examples_custom_package_root-0.0.1.dist-info/METADATA",
255                    "examples_custom_package_root-0.0.1.dist-info/entry_points.txt",
256                    "examples_custom_package_root-0.0.1.dist-info/RECORD",
257                ],
258            )
259
260            record_contents = zf.read(
261                "examples_custom_package_root-0.0.1.dist-info/RECORD"
262            ).decode("utf-8")
263
264            # Ensure RECORD files do not have leading forward slashes
265            for line in record_contents.splitlines():
266                self.assertFalse(line.startswith("/"))
267        self.assertFileSha256Equal(
268            filename, "568922541703f6edf4b090a8413991f9fa625df2844e644dd30bdbe9deb660be"
269        )
270
271    def test_custom_package_root_multi_prefix_wheel(self):
272        filename = self._get_path(
273            "example_custom_package_root_multi_prefix-0.0.1-py3-none-any.whl",
274        )
275
276        with zipfile.ZipFile(filename) as zf:
277            self.assertAllEntriesHasReproducibleMetadata(zf)
278            self.assertEqual(
279                zf.namelist(),
280                [
281                    "data,with,commas.txt",
282                    "data.txt",
283                    "module_with_data.py",
284                    "simple_module.py",
285                    "main.py",
286                    "example_custom_package_root_multi_prefix-0.0.1.dist-info/WHEEL",
287                    "example_custom_package_root_multi_prefix-0.0.1.dist-info/METADATA",
288                    "example_custom_package_root_multi_prefix-0.0.1.dist-info/RECORD",
289                ],
290            )
291
292            record_contents = zf.read(
293                "example_custom_package_root_multi_prefix-0.0.1.dist-info/RECORD"
294            ).decode("utf-8")
295
296            # Ensure RECORD files do not have leading forward slashes
297            for line in record_contents.splitlines():
298                self.assertFalse(line.startswith("/"))
299        self.assertFileSha256Equal(
300            filename, "a8b91ce9d6f570e97b40a357a292a6f595d3470f07c479cb08550257cc9c8306"
301        )
302
303    def test_custom_package_root_multi_prefix_reverse_order_wheel(self):
304        filename = self._get_path(
305            "example_custom_package_root_multi_prefix_reverse_order-0.0.1-py3-none-any.whl",
306        )
307
308        with zipfile.ZipFile(filename) as zf:
309            self.assertAllEntriesHasReproducibleMetadata(zf)
310            self.assertEqual(
311                zf.namelist(),
312                [
313                    "lib/data,with,commas.txt",
314                    "lib/data.txt",
315                    "lib/module_with_data.py",
316                    "lib/simple_module.py",
317                    "main.py",
318                    "example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/WHEEL",
319                    "example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/METADATA",
320                    "example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/RECORD",
321                ],
322            )
323
324            record_contents = zf.read(
325                "example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/RECORD"
326            ).decode("utf-8")
327
328            # Ensure RECORD files do not have leading forward slashes
329            for line in record_contents.splitlines():
330                self.assertFalse(line.startswith("/"))
331        self.assertFileSha256Equal(
332            filename, "8f44e940731757c186079a42cfe7ea3d43cd96b526e3fb2ca2a3ea3048a9d489"
333        )
334
335    def test_python_requires_wheel(self):
336        filename = self._get_path(
337            "example_python_requires_in_a_package-0.0.1-py3-none-any.whl",
338        )
339        with zipfile.ZipFile(filename) as zf:
340            self.assertAllEntriesHasReproducibleMetadata(zf)
341            metadata_contents = zf.read(
342                "example_python_requires_in_a_package-0.0.1.dist-info/METADATA"
343            )
344            # The entries are guaranteed to be sorted.
345            self.assertEqual(
346                metadata_contents,
347                b"""\
348Metadata-Version: 2.1
349Name: example_python_requires_in_a_package
350Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
351Version: 0.0.1
352
353UNKNOWN
354""",
355            )
356        self.assertFileSha256Equal(
357            filename, "ba32493f5e43e481346384aaab9e8fa09c23884276ad057c5f432096a0350101"
358        )
359
360    def test_python_abi3_binary_wheel(self):
361        arch = "amd64"
362        if platform.system() != "Windows":
363            arch = subprocess.check_output(["uname", "-m"]).strip().decode()
364        # These strings match the strings from py_wheel() in BUILD
365        os_strings = {
366            "Linux": "manylinux2014",
367            "Darwin": "macosx_11_0",
368            "Windows": "win",
369        }
370        os_string = os_strings[platform.system()]
371        filename = self._get_path(
372            f"example_python_abi3_binary_wheel-0.0.1-cp38-abi3-{os_string}_{arch}.whl",
373        )
374        with zipfile.ZipFile(filename) as zf:
375            self.assertAllEntriesHasReproducibleMetadata(zf)
376            metadata_contents = zf.read(
377                "example_python_abi3_binary_wheel-0.0.1.dist-info/METADATA"
378            )
379            # The entries are guaranteed to be sorted.
380            self.assertEqual(
381                metadata_contents,
382                b"""\
383Metadata-Version: 2.1
384Name: example_python_abi3_binary_wheel
385Requires-Python: >=3.8
386Version: 0.0.1
387
388UNKNOWN
389""",
390            )
391            wheel_contents = zf.read(
392                "example_python_abi3_binary_wheel-0.0.1.dist-info/WHEEL"
393            )
394            self.assertEqual(
395                wheel_contents.decode(),
396                f"""\
397Wheel-Version: 1.0
398Generator: bazel-wheelmaker 1.0
399Root-Is-Purelib: false
400Tag: cp38-abi3-{os_string}_{arch}
401""",
402            )
403
404    def test_rule_creates_directory_and_is_included_in_wheel(self):
405        filename = self._get_path(
406            "use_rule_with_dir_in_outs-0.0.1-py3-none-any.whl",
407        )
408
409        with zipfile.ZipFile(filename) as zf:
410            self.assertAllEntriesHasReproducibleMetadata(zf)
411            self.assertEqual(
412                zf.namelist(),
413                [
414                    "examples/wheel/main.py",
415                    "examples/wheel/someDir/foo.py",
416                    "use_rule_with_dir_in_outs-0.0.1.dist-info/WHEEL",
417                    "use_rule_with_dir_in_outs-0.0.1.dist-info/METADATA",
418                    "use_rule_with_dir_in_outs-0.0.1.dist-info/RECORD",
419                ],
420            )
421        self.assertFileSha256Equal(
422            filename, "ac9216bd54dcae1a6270c35fccf8a73b0be87c1b026c28e963b7c76b2f9b722b"
423        )
424
425    def test_rule_expands_workspace_status_keys_in_wheel_metadata(self):
426        filename = self._get_path(
427            "example_minimal_library{BUILD_USER}-0.1.{BUILD_TIMESTAMP}-py3-none-any.whl"
428        )
429
430        with zipfile.ZipFile(filename) as zf:
431            self.assertAllEntriesHasReproducibleMetadata(zf)
432            metadata_file = None
433            for f in zf.namelist():
434                self.assertNotIn("{BUILD_TIMESTAMP}", f)
435                self.assertNotIn("{BUILD_USER}", f)
436                if os.path.basename(f) == "METADATA":
437                    metadata_file = f
438            self.assertIsNotNone(metadata_file)
439
440            version = None
441            name = None
442            with zf.open(metadata_file) as fp:
443                for line in fp:
444                    if line.startswith(b"Version:"):
445                        version = line.decode().split()[-1]
446                    if line.startswith(b"Name:"):
447                        name = line.decode().split()[-1]
448            self.assertIsNotNone(version)
449            self.assertIsNotNone(name)
450            self.assertNotIn("{BUILD_TIMESTAMP}", version)
451            self.assertNotIn("{BUILD_USER}", name)
452
453    def test_requires_file_and_extra_requires_files(self):
454        filename = self._get_path("requires_files-0.0.1-py3-none-any.whl")
455
456        with zipfile.ZipFile(filename) as zf:
457            self.assertAllEntriesHasReproducibleMetadata(zf)
458            metadata_file = None
459            for f in zf.namelist():
460                if os.path.basename(f) == "METADATA":
461                    metadata_file = f
462            self.assertIsNotNone(metadata_file)
463
464            requires = []
465            with zf.open(metadata_file) as fp:
466                for line in fp:
467                    if line.startswith(b"Requires-Dist:"):
468                        requires.append(line.decode("utf-8").strip())
469
470            print(requires)
471            self.assertEqual(
472                [
473                    "Requires-Dist: tomli>=2.0.0",
474                    "Requires-Dist: starlark",
475                    "Requires-Dist: pyyaml!=6.0.1,>=6.0.0; extra == 'example'",
476                    'Requires-Dist: toml; ((python_version == "3.11" or python_version == "3.12") and python_version != "3.8") and extra == \'example\'',
477                    'Requires-Dist: wheel; (python_version == "3.11" or python_version == "3.12") and extra == \'example\'',
478                ],
479                requires,
480            )
481
482    def test_minimal_data_files(self):
483        filename = self._get_path("minimal_data_files-0.0.1-py3-none-any.whl")
484
485        with zipfile.ZipFile(filename) as zf:
486            self.assertAllEntriesHasReproducibleMetadata(zf)
487            metadata_file = None
488            self.assertEqual(
489                zf.namelist(),
490                [
491                    "minimal_data_files-0.0.1.dist-info/WHEEL",
492                    "minimal_data_files-0.0.1.dist-info/METADATA",
493                    "minimal_data_files-0.0.1.data/data/target/path/README.md",
494                    "minimal_data_files-0.0.1.data/scripts/NOTICE",
495                    "minimal_data_files-0.0.1.dist-info/RECORD",
496                ],
497            )
498
499    def test_extra_requires(self):
500        filename = self._get_path("extra_requires-0.0.1-py3-none-any.whl")
501
502        with zipfile.ZipFile(filename) as zf:
503            self.assertAllEntriesHasReproducibleMetadata(zf)
504            metadata_file = None
505            for f in zf.namelist():
506                if os.path.basename(f) == "METADATA":
507                    metadata_file = f
508            self.assertIsNotNone(metadata_file)
509
510            requires = []
511            with zf.open(metadata_file) as fp:
512                for line in fp:
513                    if line.startswith(b"Requires-Dist:"):
514                        requires.append(line.decode("utf-8").strip())
515
516            print(requires)
517            self.assertEqual(
518                [
519                    "Requires-Dist: tomli>=2.0.0",
520                    "Requires-Dist: starlark",
521                    'Requires-Dist: pytest; python_version != "3.8"',
522                    "Requires-Dist: pyyaml!=6.0.1,>=6.0.0; extra == 'example'",
523                    'Requires-Dist: toml; ((python_version == "3.11" or python_version == "3.12") and python_version != "3.8") and extra == \'example\'',
524                    'Requires-Dist: wheel; (python_version == "3.11" or python_version == "3.12") and extra == \'example\'',
525                ],
526                requires,
527            )
528
529
530if __name__ == "__main__":
531    unittest.main()
532