xref: /aosp_15_r20/external/bazelbuild-rules_python/examples/wheel/wheel_test.py (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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.txt",
99                    "examples/wheel/lib/module_with_data.py",
100                    "examples/wheel/lib/simple_module.py",
101                    "examples/wheel/main.py",
102                    "example_minimal_package-0.0.1.dist-info/WHEEL",
103                    "example_minimal_package-0.0.1.dist-info/METADATA",
104                    "example_minimal_package-0.0.1.dist-info/RECORD",
105                ],
106            )
107        self.assertFileSha256Equal(
108            filename, "b4815a1d3a17cc6a5ce717ed42b940fa7788cb5168f5c1de02f5f50abed7083e"
109        )
110
111    def test_customized_wheel(self):
112        filename = self._get_path(
113            "example_customized-0.0.1-py3-none-any.whl",
114        )
115        with zipfile.ZipFile(filename) as zf:
116            self.assertAllEntriesHasReproducibleMetadata(zf)
117            self.assertEqual(
118                zf.namelist(),
119                [
120                    "examples/wheel/lib/data.txt",
121                    "examples/wheel/lib/module_with_data.py",
122                    "examples/wheel/lib/simple_module.py",
123                    "examples/wheel/main.py",
124                    "example_customized-0.0.1.dist-info/WHEEL",
125                    "example_customized-0.0.1.dist-info/METADATA",
126                    "example_customized-0.0.1.dist-info/entry_points.txt",
127                    "example_customized-0.0.1.dist-info/NOTICE",
128                    "example_customized-0.0.1.dist-info/README",
129                    "example_customized-0.0.1.dist-info/RECORD",
130                ],
131            )
132            record_contents = zf.read("example_customized-0.0.1.dist-info/RECORD")
133            wheel_contents = zf.read("example_customized-0.0.1.dist-info/WHEEL")
134            metadata_contents = zf.read("example_customized-0.0.1.dist-info/METADATA")
135            entry_point_contents = zf.read(
136                "example_customized-0.0.1.dist-info/entry_points.txt"
137            )
138
139            self.assertEqual(
140                record_contents,
141                # The entries are guaranteed to be sorted.
142                b"""\
143examples/wheel/lib/data.txt,sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12
144examples/wheel/lib/module_with_data.py,sha256=8s0Khhcqz3yVsBKv2IB5u4l4TMKh7-c_V6p65WVHPms,637
145examples/wheel/lib/simple_module.py,sha256=z2hwciab_XPNIBNH8B1Q5fYgnJvQTeYf0ZQJpY8yLLY,637
146examples/wheel/main.py,sha256=sgg5iWN_9inYBjm6_Zw27hYdmo-l24fA-2rfphT-IlY,909
147example_customized-0.0.1.dist-info/WHEEL,sha256=sobxWSyDDkdg_rinUth-jxhXHqoNqlmNMJY3aTZn2Us,91
148example_customized-0.0.1.dist-info/METADATA,sha256=QYQcDJFQSIqan8eiXqL67bqsUfgEAwf2hoK_Lgi1S-0,559
149example_customized-0.0.1.dist-info/entry_points.txt,sha256=pqzpbQ8MMorrJ3Jp0ntmpZcuvfByyqzMXXi2UujuXD0,137
150example_customized-0.0.1.dist-info/NOTICE,sha256=Xpdw-FXET1IRgZ_wTkx1YQfo1-alET0FVf6V1LXO4js,76
151example_customized-0.0.1.dist-info/README,sha256=WmOFwZ3Jga1bHG3JiGRsUheb4UbLffUxyTdHczS27-o,40
152example_customized-0.0.1.dist-info/RECORD,,
153""",
154            )
155            self.assertEqual(
156                wheel_contents,
157                b"""\
158Wheel-Version: 1.0
159Generator: bazel-wheelmaker 1.0
160Root-Is-Purelib: true
161Tag: py3-none-any
162""",
163            )
164            self.assertEqual(
165                metadata_contents,
166                b"""\
167Metadata-Version: 2.1
168Name: example_customized
169Author: Example Author with non-ascii characters: \xc5\xbc\xc3\xb3\xc5\x82w
170Author-email: [email protected]
171Home-page: www.example.com
172License: Apache 2.0
173Description-Content-Type: text/markdown
174Summary: A one-line summary of this test package
175Project-URL: Bug Tracker, www.example.com/issues
176Project-URL: Documentation, www.example.com/docs
177Classifier: License :: OSI Approved :: Apache Software License
178Classifier: Intended Audience :: Developers
179Requires-Dist: pytest
180Version: 0.0.1
181
182This is a sample description of a wheel.
183""",
184            )
185            self.assertEqual(
186                entry_point_contents,
187                b"""\
188[console_scripts]
189another = foo.bar:baz
190customized_wheel = examples.wheel.main:main
191
192[group2]
193first = first.main:f
194second = second.main:s""",
195            )
196        self.assertFileSha256Equal(
197            filename, "27f3038be6e768d28735441a1bc567eca2213bd3568d18b22a414e6399a2d48e"
198        )
199
200    def test_filename_escaping(self):
201        filename = self._get_path(
202            "file_name_escaping-0.0.1rc1+ubuntu.r7-py3-none-any.whl",
203        )
204        with zipfile.ZipFile(filename) as zf:
205            self.assertEqual(
206                zf.namelist(),
207                [
208                    "examples/wheel/lib/data.txt",
209                    "examples/wheel/lib/module_with_data.py",
210                    "examples/wheel/lib/simple_module.py",
211                    "examples/wheel/main.py",
212                    # PEP calls for replacing only in the archive filename.
213                    # Alas setuptools also escapes in the dist-info directory
214                    # name, so let's be compatible.
215                    "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/WHEEL",
216                    "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/METADATA",
217                    "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/RECORD",
218                ],
219            )
220            metadata_contents = zf.read(
221                "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/METADATA"
222            )
223            self.assertEqual(
224                metadata_contents,
225                b"""\
226Metadata-Version: 2.1
227Name: File--Name-Escaping
228Version: 0.0.1rc1+ubuntu.r7
229
230UNKNOWN
231""",
232            )
233
234    def test_custom_package_root_wheel(self):
235        filename = self._get_path(
236            "examples_custom_package_root-0.0.1-py3-none-any.whl",
237        )
238
239        with zipfile.ZipFile(filename) as zf:
240            self.assertAllEntriesHasReproducibleMetadata(zf)
241            self.assertEqual(
242                zf.namelist(),
243                [
244                    "wheel/lib/data.txt",
245                    "wheel/lib/module_with_data.py",
246                    "wheel/lib/simple_module.py",
247                    "wheel/main.py",
248                    "examples_custom_package_root-0.0.1.dist-info/WHEEL",
249                    "examples_custom_package_root-0.0.1.dist-info/METADATA",
250                    "examples_custom_package_root-0.0.1.dist-info/entry_points.txt",
251                    "examples_custom_package_root-0.0.1.dist-info/RECORD",
252                ],
253            )
254
255            record_contents = zf.read(
256                "examples_custom_package_root-0.0.1.dist-info/RECORD"
257            ).decode("utf-8")
258
259            # Ensure RECORD files do not have leading forward slashes
260            for line in record_contents.splitlines():
261                self.assertFalse(line.startswith("/"))
262        self.assertFileSha256Equal(
263            filename, "f034b3278781f4df32a33df70d794bb94170b450e477c8bd9cd42d2d922476ae"
264        )
265
266    def test_custom_package_root_multi_prefix_wheel(self):
267        filename = self._get_path(
268            "example_custom_package_root_multi_prefix-0.0.1-py3-none-any.whl",
269        )
270
271        with zipfile.ZipFile(filename) as zf:
272            self.assertAllEntriesHasReproducibleMetadata(zf)
273            self.assertEqual(
274                zf.namelist(),
275                [
276                    "data.txt",
277                    "module_with_data.py",
278                    "simple_module.py",
279                    "main.py",
280                    "example_custom_package_root_multi_prefix-0.0.1.dist-info/WHEEL",
281                    "example_custom_package_root_multi_prefix-0.0.1.dist-info/METADATA",
282                    "example_custom_package_root_multi_prefix-0.0.1.dist-info/RECORD",
283                ],
284            )
285
286            record_contents = zf.read(
287                "example_custom_package_root_multi_prefix-0.0.1.dist-info/RECORD"
288            ).decode("utf-8")
289
290            # Ensure RECORD files do not have leading forward slashes
291            for line in record_contents.splitlines():
292                self.assertFalse(line.startswith("/"))
293        self.assertFileSha256Equal(
294            filename, "ff19f5e4540948247742716338bb4194d619cb56df409045d1a99f265ce8e36c"
295        )
296
297    def test_custom_package_root_multi_prefix_reverse_order_wheel(self):
298        filename = self._get_path(
299            "example_custom_package_root_multi_prefix_reverse_order-0.0.1-py3-none-any.whl",
300        )
301
302        with zipfile.ZipFile(filename) as zf:
303            self.assertAllEntriesHasReproducibleMetadata(zf)
304            self.assertEqual(
305                zf.namelist(),
306                [
307                    "lib/data.txt",
308                    "lib/module_with_data.py",
309                    "lib/simple_module.py",
310                    "main.py",
311                    "example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/WHEEL",
312                    "example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/METADATA",
313                    "example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/RECORD",
314                ],
315            )
316
317            record_contents = zf.read(
318                "example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/RECORD"
319            ).decode("utf-8")
320
321            # Ensure RECORD files do not have leading forward slashes
322            for line in record_contents.splitlines():
323                self.assertFalse(line.startswith("/"))
324        self.assertFileSha256Equal(
325            filename, "4331e378ea8b8148409ae7c02177e4eb24d151a85ef937bb44b79ff5258d634b"
326        )
327
328    def test_python_requires_wheel(self):
329        filename = self._get_path(
330            "example_python_requires_in_a_package-0.0.1-py3-none-any.whl",
331        )
332        with zipfile.ZipFile(filename) as zf:
333            self.assertAllEntriesHasReproducibleMetadata(zf)
334            metadata_contents = zf.read(
335                "example_python_requires_in_a_package-0.0.1.dist-info/METADATA"
336            )
337            # The entries are guaranteed to be sorted.
338            self.assertEqual(
339                metadata_contents,
340                b"""\
341Metadata-Version: 2.1
342Name: example_python_requires_in_a_package
343Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
344Version: 0.0.1
345
346UNKNOWN
347""",
348            )
349        self.assertFileSha256Equal(
350            filename, "b34676828f93da8cd898d50dcd4f36e02fe273150e213aacb999310a05f5f38c"
351        )
352
353    def test_python_abi3_binary_wheel(self):
354        arch = "amd64"
355        if platform.system() != "Windows":
356            arch = subprocess.check_output(["uname", "-m"]).strip().decode()
357        # These strings match the strings from py_wheel() in BUILD
358        os_strings = {
359            "Linux": "manylinux2014",
360            "Darwin": "macosx_11_0",
361            "Windows": "win",
362        }
363        os_string = os_strings[platform.system()]
364        filename = self._get_path(
365            f"example_python_abi3_binary_wheel-0.0.1-cp38-abi3-{os_string}_{arch}.whl",
366        )
367        with zipfile.ZipFile(filename) as zf:
368            self.assertAllEntriesHasReproducibleMetadata(zf)
369            metadata_contents = zf.read(
370                "example_python_abi3_binary_wheel-0.0.1.dist-info/METADATA"
371            )
372            # The entries are guaranteed to be sorted.
373            self.assertEqual(
374                metadata_contents,
375                b"""\
376Metadata-Version: 2.1
377Name: example_python_abi3_binary_wheel
378Requires-Python: >=3.8
379Version: 0.0.1
380
381UNKNOWN
382""",
383            )
384            wheel_contents = zf.read(
385                "example_python_abi3_binary_wheel-0.0.1.dist-info/WHEEL"
386            )
387            self.assertEqual(
388                wheel_contents.decode(),
389                f"""\
390Wheel-Version: 1.0
391Generator: bazel-wheelmaker 1.0
392Root-Is-Purelib: false
393Tag: cp38-abi3-{os_string}_{arch}
394""",
395            )
396
397    def test_rule_creates_directory_and_is_included_in_wheel(self):
398        filename = self._get_path(
399            "use_rule_with_dir_in_outs-0.0.1-py3-none-any.whl",
400        )
401
402        with zipfile.ZipFile(filename) as zf:
403            self.assertAllEntriesHasReproducibleMetadata(zf)
404            self.assertEqual(
405                zf.namelist(),
406                [
407                    "examples/wheel/main.py",
408                    "examples/wheel/someDir/foo.py",
409                    "use_rule_with_dir_in_outs-0.0.1.dist-info/WHEEL",
410                    "use_rule_with_dir_in_outs-0.0.1.dist-info/METADATA",
411                    "use_rule_with_dir_in_outs-0.0.1.dist-info/RECORD",
412                ],
413            )
414        self.assertFileSha256Equal(
415            filename, "ac9216bd54dcae1a6270c35fccf8a73b0be87c1b026c28e963b7c76b2f9b722b"
416        )
417
418    def test_rule_expands_workspace_status_keys_in_wheel_metadata(self):
419        filename = self._get_path(
420            "example_minimal_library{BUILD_USER}-0.1.{BUILD_TIMESTAMP}-py3-none-any.whl"
421        )
422
423        with zipfile.ZipFile(filename) as zf:
424            self.assertAllEntriesHasReproducibleMetadata(zf)
425            metadata_file = None
426            for f in zf.namelist():
427                self.assertNotIn("{BUILD_TIMESTAMP}", f)
428                self.assertNotIn("{BUILD_USER}", f)
429                if os.path.basename(f) == "METADATA":
430                    metadata_file = f
431            self.assertIsNotNone(metadata_file)
432
433            version = None
434            name = None
435            with zf.open(metadata_file) as fp:
436                for line in fp:
437                    if line.startswith(b"Version:"):
438                        version = line.decode().split()[-1]
439                    if line.startswith(b"Name:"):
440                        name = line.decode().split()[-1]
441            self.assertIsNotNone(version)
442            self.assertIsNotNone(name)
443            self.assertNotIn("{BUILD_TIMESTAMP}", version)
444            self.assertNotIn("{BUILD_USER}", name)
445
446    def test_requires_file_and_extra_requires_files(self):
447        filename = self._get_path("requires_files-0.0.1-py3-none-any.whl")
448
449        with zipfile.ZipFile(filename) as zf:
450            self.assertAllEntriesHasReproducibleMetadata(zf)
451            metadata_file = None
452            for f in zf.namelist():
453                if os.path.basename(f) == "METADATA":
454                    metadata_file = f
455            self.assertIsNotNone(metadata_file)
456
457            requires = []
458            with zf.open(metadata_file) as fp:
459                for line in fp:
460                    if line.startswith(b"Requires-Dist:"):
461                        requires.append(line.decode("utf-8").strip())
462
463            print(requires)
464            self.assertEqual(
465                [
466                    "Requires-Dist: tomli>=2.0.0",
467                    "Requires-Dist: starlark",
468                    "Requires-Dist: pyyaml!=6.0.1,>=6.0.0; extra == 'example'",
469                    'Requires-Dist: toml; ((python_version == "3.11" or python_version == "3.12") and python_version != "3.8") and extra == \'example\'',
470                    'Requires-Dist: wheel; (python_version == "3.11" or python_version == "3.12") and extra == \'example\'',
471                ],
472                requires,
473            )
474
475    def test_minimal_data_files(self):
476        filename = self._get_path("minimal_data_files-0.0.1-py3-none-any.whl")
477
478        with zipfile.ZipFile(filename) as zf:
479            self.assertAllEntriesHasReproducibleMetadata(zf)
480            metadata_file = None
481            self.assertEqual(
482                zf.namelist(),
483                [
484                    "minimal_data_files-0.0.1.dist-info/WHEEL",
485                    "minimal_data_files-0.0.1.dist-info/METADATA",
486                    "minimal_data_files-0.0.1.data/data/target/path/README.md",
487                    "minimal_data_files-0.0.1.data/scripts/NOTICE",
488                    "minimal_data_files-0.0.1.dist-info/RECORD",
489                ],
490            )
491
492    def test_extra_requires(self):
493        filename = self._get_path("extra_requires-0.0.1-py3-none-any.whl")
494
495        with zipfile.ZipFile(filename) as zf:
496            self.assertAllEntriesHasReproducibleMetadata(zf)
497            metadata_file = None
498            for f in zf.namelist():
499                if os.path.basename(f) == "METADATA":
500                    metadata_file = f
501            self.assertIsNotNone(metadata_file)
502
503            requires = []
504            with zf.open(metadata_file) as fp:
505                for line in fp:
506                    if line.startswith(b"Requires-Dist:"):
507                        requires.append(line.decode("utf-8").strip())
508
509            print(requires)
510            self.assertEqual(
511                [
512                    "Requires-Dist: tomli>=2.0.0",
513                    "Requires-Dist: starlark",
514                    'Requires-Dist: pytest; python_version != "3.8"',
515                    "Requires-Dist: pyyaml!=6.0.1,>=6.0.0; extra == 'example'",
516                    'Requires-Dist: toml; ((python_version == "3.11" or python_version == "3.12") and python_version != "3.8") and extra == \'example\'',
517                    'Requires-Dist: wheel; (python_version == "3.11" or python_version == "3.12") and extra == \'example\'',
518                ],
519                requires,
520            )
521
522
523if __name__ == "__main__":
524    unittest.main()
525