Skip to content

Commit 9bbd0ca

Browse files
committed
Use PEP 639 license metadata
1 parent d5c40a4 commit 9bbd0ca

File tree

3 files changed

+169
-20
lines changed

3 files changed

+169
-20
lines changed

.github/workflows/buildwheel.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ jobs:
167167
# We don't need to specify ninja as a requirement in pyproject.toml
168168
# because without --no-build-isolation meson-python handles it
169169
# automatically in get_requirements_for_build_wheel().
170-
- run: 'pip install "cython==3.0.11" "meson-python==0.13" "ninja<1.11"'
170+
- run: 'pip install "cython==3.0.11" "meson-python==0.18" "ninja<1.11"'
171171
- run: pip install --no-build-isolation .
172172
- run: python -m flint.test --verbose
173173

bin/cibw_repair_wheel_licenses.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python
2+
"""
3+
Update license information in wheels after running auditwheel etc.
4+
5+
Usage:
6+
cibw_repair_wheel_licenses.py <wheel_file> \
7+
--license MIT --license-file licenses/license_foo_MIT.txt \
8+
--license BSD-3-Clause --license-file licenses/license_bar_BSD-3-Clause.txt \
9+
...
10+
11+
CWD should be at project root (containing pyproject.toml) and license-file
12+
paths must be relative.
13+
14+
The wheel_file argument should be the path to the wheel file to be repaired.
15+
It will be overwritten in place.
16+
"""
17+
import argparse
18+
from pathlib import Path
19+
from subprocess import run
20+
from tempfile import TemporaryDirectory
21+
from shutil import copyfile
22+
from os import makedirs
23+
24+
25+
def main(*args: str):
26+
parser = argparse.ArgumentParser()
27+
parser.add_argument("wheel_file")
28+
parser.add_argument("--license", action="append", default=[])
29+
parser.add_argument("--license-file", action="append", default=[])
30+
31+
parsed = parser.parse_args(args)
32+
wheel_file = Path(parsed.wheel_file)
33+
licenses = parsed.license
34+
license_files = [Path(f) for f in parsed.license_file]
35+
36+
update_licenses_wheel(wheel_file, licenses, license_files)
37+
38+
39+
def update_licenses_wheel(
40+
wheel_file: Path, licenses: list[str], license_files: list[Path]
41+
):
42+
if wheel_file.exists():
43+
print("Found wheel at", wheel_file)
44+
else:
45+
raise ValueError(f"Wheel not found at {wheel_file}")
46+
47+
for license_file in license_files:
48+
if license_file.is_absolute():
49+
raise ValueError("license-file paths must be relative to project root")
50+
elif not license_file.exists():
51+
raise ValueError(f"license-file not found: {license_file}")
52+
53+
# foo/bar-1.0-cp310-cp310-linux_x86_64.whl -> bar-1.0
54+
name, version = wheel_file.stem.split('-')[:2]
55+
base = f"{name}-{version}"
56+
57+
with TemporaryDirectory() as tmpdir:
58+
59+
print("temp dir:", tmpdir)
60+
tmpdir = Path(tmpdir)
61+
62+
run(["wheel", "unpack", "--dest", tmpdir, wheel_file], check=True)
63+
64+
dist_info = tmpdir / base / f"{base}.dist-info"
65+
66+
print(f"Adding licenses in {dist_info}")
67+
update_license_dist_info(dist_info, licenses, license_files)
68+
69+
run(["wheel", "pack", "--dest-dir", tmpdir, tmpdir / base], check=True)
70+
71+
# glob for *.whl in tmpdir
72+
wheels = list(tmpdir.glob(f"{base}-*.whl"))
73+
if len(wheels) != 1:
74+
raise ValueError(f"Expected one wheel in {tmpdir}, got {wheels}")
75+
new_wheel_file = wheels[0]
76+
77+
print(f"Repaired wheel: {new_wheel_file}")
78+
print(f"Copying back to: {wheel_file}")
79+
copyfile(new_wheel_file, wheel_file)
80+
81+
82+
def update_license_dist_info(
83+
dist_info: Path, licenses: list[str], license_files: list[Path]
84+
):
85+
for license_file in license_files:
86+
wheel_license_path = dist_info / "licenses" / license_file.name
87+
if wheel_license_path.exists():
88+
raise ValueError(f"license file already present: {license_file}")
89+
#
90+
# PEP 639 says:
91+
#
92+
# Inside the root license directory, packaging tools MUST reproduce the
93+
# directory structure under which the source license files are located
94+
# relative to the project root.
95+
#
96+
makedirs(dist_info / license_file.parent, exist_ok=True)
97+
copyfile(license_file, wheel_license_path)
98+
print(f"Added license file {license_file}")
99+
100+
metadata_file = dist_info / "METADATA"
101+
102+
with open(metadata_file, "r") as f:
103+
lines = f.readlines()
104+
105+
for n, line in enumerate(lines):
106+
if line.startswith("License-Expression: "):
107+
base_license = line[len("License-Expression: ") :].strip()
108+
all_licenses = [base_license, *licenses]
109+
expression = ' AND '.join([f"({license})" for license in all_licenses])
110+
lines[n] = f"License-Expression: {expression}\n"
111+
break
112+
else:
113+
raise ValueError("Could not find License-Expression in METADATA")
114+
115+
print("Updated License-Expression from")
116+
print(" " + base_license)
117+
print("to")
118+
print(" " + expression)
119+
120+
license_files_lines = [line for line in lines if line.startswith("License-File: ")]
121+
122+
if not license_files_lines:
123+
raise ValueError("Could not find License-File in METADATA")
124+
125+
index = lines.index(license_files_lines[-1]) + 1
126+
new_lines = [f"License-File: {f}\n" for f in license_files]
127+
lines = lines[:index] + new_lines + lines[index:]
128+
129+
print("Writing out METADATA with updated License-Expression and License-File fields")
130+
print("Writing to:", metadata_file)
131+
132+
with open(metadata_file, "w") as f:
133+
f.writelines(lines)
134+
135+
136+
if __name__ == "__main__":
137+
import sys
138+
sys.exit(main(*sys.argv[1:]))

pyproject.toml

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
name = "python-flint"
33
description = "Bindings for FLINT"
44
version = "0.8.0"
5+
56
# This needs to be in sync with README, and CI config.
67
requires-python = ">= 3.11"
78
authors = [
89
{name = "Fredrik Johansson", email = "[email protected]"},
910
{name = "Oscar Benjamin", email = "[email protected]"},
1011
]
11-
license = {file = "LICENSE"}
12+
13+
license = "MIT"
14+
license-files = ["LICENSE"]
15+
1216
classifiers = [
1317
"Topic :: Scientific/Engineering :: Mathematics",
1418
]
@@ -37,7 +41,7 @@ content-type = "text/markdown"
3741
# fine. It is not possible to have a separate version constraint here for the
3842
# freethreading build only though.
3943
#
40-
requires = ["meson-python >= 0.13", "cython >=3.1,<3.2"]
44+
requires = ["meson-python >= 0.18", "cython >=3.1,<3.2"]
4145
build-backend = "mesonpy"
4246

4347
[tool.cython-lint]
@@ -82,9 +86,10 @@ package = "flint"
8286
[tool.cibuildwheel]
8387
skip = "*-win32 *-manylinux_i686 *-manylinux_armv7l *-musllinux_*"
8488

85-
# Enable building for free-threaded CPython builds
8689
enable = [
87-
"cpython-prerelease", # for 3.14rc1 (remove this later)
90+
# Uncomment this to test beta versions of CPython in CI (but comment out
91+
# again if releasing while CPython is in beta):
92+
# "cpython-prerelease",
8893
"cpython-freethreading",
8994
"pypy",
9095
]
@@ -108,24 +113,30 @@ PKG_CONFIG_PATH = "$(pwd)/.local/lib/pkgconfig"
108113

109114
[tool.cibuildwheel.linux]
110115
before-all = "bin/cibw_before_all_linux_$(uname -m).sh"
116+
before-build = "pip install wheel auditwheel"
117+
repair-wheel-command = [
118+
"""bin/cibw_repair_wheel_licenses.py {wheel} \
119+
--license-file wheels/LICENSE_linux_wheels.txt \
120+
""",
121+
"auditwheel repair -w {dest_dir} {wheel}",
122+
]
111123

112124
[tool.cibuildwheel.macos]
113125
before-all = "bin/cibw_before_all_macosx_$(uname -m).sh"
126+
before-build = "pip install wheel delocate"
127+
repair-wheel-command = [
128+
"""bin/cibw_repair_wheel_licenses.py {wheel} \
129+
--license-file wheels/LICENSE_macos_wheels.txt \
130+
""",
131+
"delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}",
132+
]
114133

115134
[tool.cibuildwheel.windows]
116135
before-all = "C:\\msys64\\usr\\bin\\bash bin/cibw_before_all_windows.sh"
117-
before-build = "pip install delvewheel"
118-
repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel} --add-path .local/bin"
119-
120-
# Previously with setuptools and MinGW it was necessary to run
121-
# bin/cibw_before_build_windows.sh before building the wheel to create the
122-
# libpython*.a files. This is no longer necessary now meson is used:
123-
#
124-
# before-build = "pip install delvewheel && C:\\msys64\\usr\\bin\\bash bin/cibw_before_build_windows.sh"
125-
#
126-
# Previously a custom delvewheel command was needed because delvewheel would
127-
# reject binaries created with MinGW unless they had been stripped. This is not
128-
# needed any more with newer versions of delvewheel:
129-
#
130-
# repair-wheel-command = "bin\\cibw_repair_wheel_command_windows.bat {dest_dir} {wheel}"
131-
#
136+
before-build = "pip install wheel delvewheel"
137+
repair-wheel-command = [
138+
"""python bin/cibw_repair_wheel_licenses.py {wheel} \
139+
--license-file wheels/LICENSE_windows_wheels.txt \
140+
""",
141+
"delvewheel repair -w {dest_dir} {wheel} --add-path .local/bin",
142+
]

0 commit comments

Comments
 (0)