blob: 4494ee170dccc9b80a3a824a3e1329d5636722e3 [file] [log] [blame]
# Copyright 2018 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import hashlib
import os
import platform
import stat
import subprocess
import unittest
import zipfile
from python.runfiles import runfiles
class WheelTest(unittest.TestCase):
maxDiff = None
def setUp(self):
super().setUp()
self.runfiles = runfiles.Create()
def _get_path(self, filename):
runfiles_path = os.path.join("rules_python/examples/wheel", filename)
path = self.runfiles.Rlocation(runfiles_path)
# The runfiles API can return None if the path doesn't exist or
# can't be resolved.
if not path:
raise AssertionError(f"Runfiles failed to resolve {runfiles_path}")
elif not os.path.exists(path):
# A non-None value doesn't mean the file actually exists, though
raise AssertionError(
f"Path {path} does not exist (from runfiles path {runfiles_path}"
)
else:
return path
def assertFileSha256Equal(self, filename, want):
hash = hashlib.sha256()
with open(filename, "rb") as f:
while True:
buf = f.read(2**20)
if not buf:
break
hash.update(buf)
self.assertEqual(want, hash.hexdigest())
def assertAllEntriesHasReproducibleMetadata(self, zf):
for zinfo in zf.infolist():
self.assertEqual(zinfo.date_time, (1980, 1, 1, 0, 0, 0), msg=zinfo.filename)
self.assertEqual(zinfo.create_system, 3, msg=zinfo.filename)
self.assertEqual(
zinfo.external_attr,
(stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO | stat.S_IFREG) << 16,
msg=zinfo.filename,
)
self.assertEqual(
zinfo.compress_type, zipfile.ZIP_DEFLATED, msg=zinfo.filename
)
def test_py_library_wheel(self):
filename = self._get_path("example_minimal_library-0.0.1-py3-none-any.whl")
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
self.assertEqual(
zf.namelist(),
[
"examples/wheel/lib/module_with_data.py",
"examples/wheel/lib/simple_module.py",
"example_minimal_library-0.0.1.dist-info/WHEEL",
"example_minimal_library-0.0.1.dist-info/METADATA",
"example_minimal_library-0.0.1.dist-info/RECORD",
],
)
self.assertFileSha256Equal(
filename, "79a4e9c1838c0631d5d8fa49a26efd6e9a364f6b38d9597c0f6df112271a0e28"
)
def test_py_package_wheel(self):
filename = self._get_path(
"example_minimal_package-0.0.1-py3-none-any.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
self.assertEqual(
zf.namelist(),
[
"examples/wheel/lib/data,with,commas.txt",
"examples/wheel/lib/data.txt",
"examples/wheel/lib/module_with_data.py",
"examples/wheel/lib/simple_module.py",
"examples/wheel/main.py",
"example_minimal_package-0.0.1.dist-info/WHEEL",
"example_minimal_package-0.0.1.dist-info/METADATA",
"example_minimal_package-0.0.1.dist-info/RECORD",
],
)
self.assertFileSha256Equal(
filename, "82370bf61310e2d3c7b1218368457dc7e161bf5dc1a280d7d45102b5e56acf43"
)
def test_customized_wheel(self):
filename = self._get_path(
"example_customized-0.0.1-py3-none-any.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
self.assertEqual(
zf.namelist(),
[
"examples/wheel/lib/data,with,commas.txt",
"examples/wheel/lib/data.txt",
"examples/wheel/lib/module_with_data.py",
"examples/wheel/lib/simple_module.py",
"examples/wheel/main.py",
"example_customized-0.0.1.dist-info/WHEEL",
"example_customized-0.0.1.dist-info/METADATA",
"example_customized-0.0.1.dist-info/entry_points.txt",
"example_customized-0.0.1.dist-info/NOTICE",
"example_customized-0.0.1.dist-info/README",
"example_customized-0.0.1.dist-info/RECORD",
],
)
record_contents = zf.read("example_customized-0.0.1.dist-info/RECORD")
wheel_contents = zf.read("example_customized-0.0.1.dist-info/WHEEL")
metadata_contents = zf.read("example_customized-0.0.1.dist-info/METADATA")
entry_point_contents = zf.read(
"example_customized-0.0.1.dist-info/entry_points.txt"
)
self.assertEqual(
record_contents,
# The entries are guaranteed to be sorted.
b"""\
"examples/wheel/lib/data,with,commas.txt",sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12
examples/wheel/lib/data.txt,sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12
examples/wheel/lib/module_with_data.py,sha256=8s0Khhcqz3yVsBKv2IB5u4l4TMKh7-c_V6p65WVHPms,637
examples/wheel/lib/simple_module.py,sha256=z2hwciab_XPNIBNH8B1Q5fYgnJvQTeYf0ZQJpY8yLLY,637
examples/wheel/main.py,sha256=sgg5iWN_9inYBjm6_Zw27hYdmo-l24fA-2rfphT-IlY,909
example_customized-0.0.1.dist-info/WHEEL,sha256=sobxWSyDDkdg_rinUth-jxhXHqoNqlmNMJY3aTZn2Us,91
example_customized-0.0.1.dist-info/METADATA,sha256=QYQcDJFQSIqan8eiXqL67bqsUfgEAwf2hoK_Lgi1S-0,559
example_customized-0.0.1.dist-info/entry_points.txt,sha256=pqzpbQ8MMorrJ3Jp0ntmpZcuvfByyqzMXXi2UujuXD0,137
example_customized-0.0.1.dist-info/NOTICE,sha256=Xpdw-FXET1IRgZ_wTkx1YQfo1-alET0FVf6V1LXO4js,76
example_customized-0.0.1.dist-info/README,sha256=WmOFwZ3Jga1bHG3JiGRsUheb4UbLffUxyTdHczS27-o,40
example_customized-0.0.1.dist-info/RECORD,,
""",
)
self.assertEqual(
wheel_contents,
b"""\
Wheel-Version: 1.0
Generator: bazel-wheelmaker 1.0
Root-Is-Purelib: true
Tag: py3-none-any
""",
)
self.assertEqual(
metadata_contents,
b"""\
Metadata-Version: 2.1
Name: example_customized
Author: Example Author with non-ascii characters: \xc5\xbc\xc3\xb3\xc5\x82w
Author-email: example@example.com
Home-page: www.example.com
License: Apache 2.0
Description-Content-Type: text/markdown
Summary: A one-line summary of this test package
Project-URL: Bug Tracker, www.example.com/issues
Project-URL: Documentation, www.example.com/docs
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Intended Audience :: Developers
Requires-Dist: pytest
Version: 0.0.1
This is a sample description of a wheel.
""",
)
self.assertEqual(
entry_point_contents,
b"""\
[console_scripts]
another = foo.bar:baz
customized_wheel = examples.wheel.main:main
[group2]
first = first.main:f
second = second.main:s""",
)
self.assertFileSha256Equal(
filename, "706e8dd45884d8cb26e92869f7d29ab7ed9f683b4e2d08f06c03dbdaa12191b8"
)
def test_filename_escaping(self):
filename = self._get_path(
"file_name_escaping-0.0.1rc1+ubuntu.r7-py3-none-any.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertEqual(
zf.namelist(),
[
"examples/wheel/lib/data,with,commas.txt",
"examples/wheel/lib/data.txt",
"examples/wheel/lib/module_with_data.py",
"examples/wheel/lib/simple_module.py",
"examples/wheel/main.py",
# PEP calls for replacing only in the archive filename.
# Alas setuptools also escapes in the dist-info directory
# name, so let's be compatible.
"file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/WHEEL",
"file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/METADATA",
"file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/RECORD",
],
)
metadata_contents = zf.read(
"file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/METADATA"
)
self.assertEqual(
metadata_contents,
b"""\
Metadata-Version: 2.1
Name: File--Name-Escaping
Version: 0.0.1rc1+ubuntu.r7
UNKNOWN
""",
)
def test_custom_package_root_wheel(self):
filename = self._get_path(
"examples_custom_package_root-0.0.1-py3-none-any.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
self.assertEqual(
zf.namelist(),
[
"wheel/lib/data,with,commas.txt",
"wheel/lib/data.txt",
"wheel/lib/module_with_data.py",
"wheel/lib/simple_module.py",
"wheel/main.py",
"examples_custom_package_root-0.0.1.dist-info/WHEEL",
"examples_custom_package_root-0.0.1.dist-info/METADATA",
"examples_custom_package_root-0.0.1.dist-info/entry_points.txt",
"examples_custom_package_root-0.0.1.dist-info/RECORD",
],
)
record_contents = zf.read(
"examples_custom_package_root-0.0.1.dist-info/RECORD"
).decode("utf-8")
# Ensure RECORD files do not have leading forward slashes
for line in record_contents.splitlines():
self.assertFalse(line.startswith("/"))
self.assertFileSha256Equal(
filename, "568922541703f6edf4b090a8413991f9fa625df2844e644dd30bdbe9deb660be"
)
def test_custom_package_root_multi_prefix_wheel(self):
filename = self._get_path(
"example_custom_package_root_multi_prefix-0.0.1-py3-none-any.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
self.assertEqual(
zf.namelist(),
[
"data,with,commas.txt",
"data.txt",
"module_with_data.py",
"simple_module.py",
"main.py",
"example_custom_package_root_multi_prefix-0.0.1.dist-info/WHEEL",
"example_custom_package_root_multi_prefix-0.0.1.dist-info/METADATA",
"example_custom_package_root_multi_prefix-0.0.1.dist-info/RECORD",
],
)
record_contents = zf.read(
"example_custom_package_root_multi_prefix-0.0.1.dist-info/RECORD"
).decode("utf-8")
# Ensure RECORD files do not have leading forward slashes
for line in record_contents.splitlines():
self.assertFalse(line.startswith("/"))
self.assertFileSha256Equal(
filename, "a8b91ce9d6f570e97b40a357a292a6f595d3470f07c479cb08550257cc9c8306"
)
def test_custom_package_root_multi_prefix_reverse_order_wheel(self):
filename = self._get_path(
"example_custom_package_root_multi_prefix_reverse_order-0.0.1-py3-none-any.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
self.assertEqual(
zf.namelist(),
[
"lib/data,with,commas.txt",
"lib/data.txt",
"lib/module_with_data.py",
"lib/simple_module.py",
"main.py",
"example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/WHEEL",
"example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/METADATA",
"example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/RECORD",
],
)
record_contents = zf.read(
"example_custom_package_root_multi_prefix_reverse_order-0.0.1.dist-info/RECORD"
).decode("utf-8")
# Ensure RECORD files do not have leading forward slashes
for line in record_contents.splitlines():
self.assertFalse(line.startswith("/"))
self.assertFileSha256Equal(
filename, "8f44e940731757c186079a42cfe7ea3d43cd96b526e3fb2ca2a3ea3048a9d489"
)
def test_python_requires_wheel(self):
filename = self._get_path(
"example_python_requires_in_a_package-0.0.1-py3-none-any.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
metadata_contents = zf.read(
"example_python_requires_in_a_package-0.0.1.dist-info/METADATA"
)
# The entries are guaranteed to be sorted.
self.assertEqual(
metadata_contents,
b"""\
Metadata-Version: 2.1
Name: example_python_requires_in_a_package
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
Version: 0.0.1
UNKNOWN
""",
)
self.assertFileSha256Equal(
filename, "ba32493f5e43e481346384aaab9e8fa09c23884276ad057c5f432096a0350101"
)
def test_python_abi3_binary_wheel(self):
arch = "amd64"
if platform.system() != "Windows":
arch = subprocess.check_output(["uname", "-m"]).strip().decode()
# These strings match the strings from py_wheel() in BUILD
os_strings = {
"Linux": "manylinux2014",
"Darwin": "macosx_11_0",
"Windows": "win",
}
os_string = os_strings[platform.system()]
filename = self._get_path(
f"example_python_abi3_binary_wheel-0.0.1-cp38-abi3-{os_string}_{arch}.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
metadata_contents = zf.read(
"example_python_abi3_binary_wheel-0.0.1.dist-info/METADATA"
)
# The entries are guaranteed to be sorted.
self.assertEqual(
metadata_contents,
b"""\
Metadata-Version: 2.1
Name: example_python_abi3_binary_wheel
Requires-Python: >=3.8
Version: 0.0.1
UNKNOWN
""",
)
wheel_contents = zf.read(
"example_python_abi3_binary_wheel-0.0.1.dist-info/WHEEL"
)
self.assertEqual(
wheel_contents.decode(),
f"""\
Wheel-Version: 1.0
Generator: bazel-wheelmaker 1.0
Root-Is-Purelib: false
Tag: cp38-abi3-{os_string}_{arch}
""",
)
def test_rule_creates_directory_and_is_included_in_wheel(self):
filename = self._get_path(
"use_rule_with_dir_in_outs-0.0.1-py3-none-any.whl",
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
self.assertEqual(
zf.namelist(),
[
"examples/wheel/main.py",
"examples/wheel/someDir/foo.py",
"use_rule_with_dir_in_outs-0.0.1.dist-info/WHEEL",
"use_rule_with_dir_in_outs-0.0.1.dist-info/METADATA",
"use_rule_with_dir_in_outs-0.0.1.dist-info/RECORD",
],
)
self.assertFileSha256Equal(
filename, "ac9216bd54dcae1a6270c35fccf8a73b0be87c1b026c28e963b7c76b2f9b722b"
)
def test_rule_expands_workspace_status_keys_in_wheel_metadata(self):
filename = self._get_path(
"example_minimal_library{BUILD_USER}-0.1.{BUILD_TIMESTAMP}-py3-none-any.whl"
)
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
metadata_file = None
for f in zf.namelist():
self.assertNotIn("{BUILD_TIMESTAMP}", f)
self.assertNotIn("{BUILD_USER}", f)
if os.path.basename(f) == "METADATA":
metadata_file = f
self.assertIsNotNone(metadata_file)
version = None
name = None
with zf.open(metadata_file) as fp:
for line in fp:
if line.startswith(b"Version:"):
version = line.decode().split()[-1]
if line.startswith(b"Name:"):
name = line.decode().split()[-1]
self.assertIsNotNone(version)
self.assertIsNotNone(name)
self.assertNotIn("{BUILD_TIMESTAMP}", version)
self.assertNotIn("{BUILD_USER}", name)
def test_requires_file_and_extra_requires_files(self):
filename = self._get_path("requires_files-0.0.1-py3-none-any.whl")
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
metadata_file = None
for f in zf.namelist():
if os.path.basename(f) == "METADATA":
metadata_file = f
self.assertIsNotNone(metadata_file)
requires = []
with zf.open(metadata_file) as fp:
for line in fp:
if line.startswith(b"Requires-Dist:"):
requires.append(line.decode("utf-8").strip())
print(requires)
self.assertEqual(
[
"Requires-Dist: tomli>=2.0.0",
"Requires-Dist: starlark",
"Requires-Dist: pyyaml!=6.0.1,>=6.0.0; extra == 'example'",
'Requires-Dist: toml; ((python_version == "3.11" or python_version == "3.12") and python_version != "3.8") and extra == \'example\'',
'Requires-Dist: wheel; (python_version == "3.11" or python_version == "3.12") and extra == \'example\'',
],
requires,
)
def test_minimal_data_files(self):
filename = self._get_path("minimal_data_files-0.0.1-py3-none-any.whl")
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
metadata_file = None
self.assertEqual(
zf.namelist(),
[
"minimal_data_files-0.0.1.dist-info/WHEEL",
"minimal_data_files-0.0.1.dist-info/METADATA",
"minimal_data_files-0.0.1.data/data/target/path/README.md",
"minimal_data_files-0.0.1.data/scripts/NOTICE",
"minimal_data_files-0.0.1.dist-info/RECORD",
],
)
def test_extra_requires(self):
filename = self._get_path("extra_requires-0.0.1-py3-none-any.whl")
with zipfile.ZipFile(filename) as zf:
self.assertAllEntriesHasReproducibleMetadata(zf)
metadata_file = None
for f in zf.namelist():
if os.path.basename(f) == "METADATA":
metadata_file = f
self.assertIsNotNone(metadata_file)
requires = []
with zf.open(metadata_file) as fp:
for line in fp:
if line.startswith(b"Requires-Dist:"):
requires.append(line.decode("utf-8").strip())
print(requires)
self.assertEqual(
[
"Requires-Dist: tomli>=2.0.0",
"Requires-Dist: starlark",
'Requires-Dist: pytest; python_version != "3.8"',
"Requires-Dist: pyyaml!=6.0.1,>=6.0.0; extra == 'example'",
'Requires-Dist: toml; ((python_version == "3.11" or python_version == "3.12") and python_version != "3.8") and extra == \'example\'',
'Requires-Dist: wheel; (python_version == "3.11" or python_version == "3.12") and extra == \'example\'',
],
requires,
)
if __name__ == "__main__":
unittest.main()