Skip to content

Commit 1bb4155

Browse files
committed
license: use glob for copying license files
1 parent 9bbd0ca commit 1bb4155

File tree

2 files changed

+112
-27
lines changed

2 files changed

+112
-27
lines changed

bin/cibw_repair_wheel_licenses.py

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,41 @@
44
55
Usage:
66
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 \
7+
--license MIT \
8+
--license-file ./licenses/license_foo_MIT.txt:foo/LICENSE.txt \
9+
--license-file ./licenses/license_bar_MIT.txt:bar/LICENSE.txt \
910
...
1011
11-
CWD should be at project root (containing pyproject.toml) and license-file
12-
paths must be relative.
13-
1412
The wheel_file argument should be the path to the wheel file to be repaired.
1513
It will be overwritten in place.
14+
15+
The --license and --license-file arguments are multi-use. The --license
16+
argument should be a SPDX license expression. It will be combined with the
17+
existing License-Expression field in the wheel's METADATA file.
18+
19+
The --license-file argument should be a pair of :-separated paths. The first
20+
path is the path to the license file and the second is the relative path to
21+
put the license file under the wheel's .dist-info/licenses directory. The first
22+
path can be a glob pattern. The second path can also be a glob pattern in which
23+
case the * is replaced in the same way. This makes it possible to do
24+
25+
--license-file ./src/foo-*/LICENSE:libs/foo-*/LICENSE
26+
27+
which would find the LICENSE file src/foo-1.0/LICENSE and copy it to the
28+
matched .dist-info/licenses/libs/foo-1.0/LICENSE path in the wheel.
29+
30+
PEP 639 says:
31+
32+
Inside the root license directory, packaging tools MUST reproduce the
33+
directory structure under which the source license files are located
34+
relative to the project root.
35+
36+
It is not clear what that means though if the licenses are coming from code
37+
that is not vendored in the repo/sdist. If they are vendored then presumably
38+
the argument here should be like:
39+
40+
--license-file ./vendored-foo/LICENSE:vendored-foo/LICENSE
41+
1642
"""
1743
import argparse
1844
from pathlib import Path
@@ -31,13 +57,48 @@ def main(*args: str):
3157
parsed = parser.parse_args(args)
3258
wheel_file = Path(parsed.wheel_file)
3359
licenses = parsed.license
34-
license_files = [Path(f) for f in parsed.license_file]
60+
license_files = dict(arg_to_paths(f) for f in parsed.license_file)
3561

3662
update_licenses_wheel(wheel_file, licenses, license_files)
3763

3864

65+
def arg_to_paths(license_file_arg: str) -> tuple[Path, Path]:
66+
"""
67+
Convert a --license-file argument to a pair of Paths.
68+
"""
69+
paths = license_file_arg.strip().split(":")
70+
if len(paths) != 2:
71+
raise ValueError("license-file argument must be in the form of <path>:<path>"
72+
f" but got {license_file_arg}")
73+
glob1_str, glob2_str = paths
74+
paths1 = list(Path().glob(glob1_str))
75+
if len(paths1) != 1:
76+
raise ValueError(f"Expected one path from glob pattern {glob1_str}"
77+
f" but got {paths1}")
78+
[path1] = paths1
79+
80+
if '*' not in glob2_str:
81+
path2_str = glob2_str
82+
else:
83+
# Replace * in glob2_str with the part of path1 that matches glob1_str:
84+
index1 = glob1_str.index('*')
85+
part1_glob = glob1_str[:index1]
86+
part2_glob = glob1_str[index1+1:]
87+
path1_str = str(path1)
88+
if len(part2_glob) != 0:
89+
wildcard = path1_str[len(part1_glob):-len(part2_glob)]
90+
else:
91+
wildcard = path1_str[len(part1_glob):]
92+
assert path1_str.startswith(part1_glob)
93+
assert path1_str.endswith(part2_glob)
94+
assert path1_str == part1_glob + wildcard + part2_glob
95+
path2_str = glob2_str.replace('*', wildcard)
96+
97+
return path1, Path(path2_str)
98+
99+
39100
def update_licenses_wheel(
40-
wheel_file: Path, licenses: list[str], license_files: list[Path]
101+
wheel_file: Path, licenses: list[str], license_files: dict[Path, Path]
41102
):
42103
if wheel_file.exists():
43104
print("Found wheel at", wheel_file)
@@ -80,33 +141,39 @@ def update_licenses_wheel(
80141

81142

82143
def update_license_dist_info(
83-
dist_info: Path, licenses: list[str], license_files: list[Path]
144+
dist_info: Path, licenses: list[str], license_files: dict[Path, Path]
84145
):
85-
for license_file in license_files:
86-
wheel_license_path = dist_info / "licenses" / license_file.name
146+
for src, dst in license_files.items():
147+
wheel_license_path = dist_info / "licenses" / dst
87148
if wheel_license_path.exists():
88-
raise ValueError(f"license file already present: {license_file}")
149+
raise ValueError(f"license file already present: {wheel_license_path}")
89150
#
90151
# PEP 639 says:
91152
#
92153
# Inside the root license directory, packaging tools MUST reproduce the
93154
# directory structure under which the source license files are located
94155
# relative to the project root.
95156
#
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}")
157+
makedirs(wheel_license_path.parent, exist_ok=True)
158+
copyfile(src, wheel_license_path)
159+
print(f"Copied license file {src} to {wheel_license_path}")
99160

100161
metadata_file = dist_info / "METADATA"
101162

102163
with open(metadata_file, "r") as f:
103164
lines = f.readlines()
104165

166+
def brackets(s: str) -> str:
167+
if ' ' not in s:
168+
return s
169+
else:
170+
return f"({s})"
171+
105172
for n, line in enumerate(lines):
106173
if line.startswith("License-Expression: "):
107174
base_license = line[len("License-Expression: ") :].strip()
108175
all_licenses = [base_license, *licenses]
109-
expression = ' AND '.join([f"({license})" for license in all_licenses])
176+
expression = ' AND '.join([brackets(license) for license in all_licenses])
110177
lines[n] = f"License-Expression: {expression}\n"
111178
break
112179
else:

pyproject.toml

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,28 +115,46 @@ PKG_CONFIG_PATH = "$(pwd)/.local/lib/pkgconfig"
115115
before-all = "bin/cibw_before_all_linux_$(uname -m).sh"
116116
before-build = "pip install wheel auditwheel"
117117
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}",
118+
"""bin/cibw_repair_wheel_licenses.py {wheel} \
119+
--license LGPL-3.0-or-later \
120+
--license-file '.local/src/gmp-*/COPYING:python-flint.libs/gmp-*/COPYING' \
121+
--license-file '.local/src/gmp-*/COPYING.LESSERv3:python-flint.libs/gmp-*/COPYING.LESSERv3' \
122+
--license-file '.local/src/mpfr-*/COPYING:python-flint.libs/mpfr-*/COPYING' \
123+
--license-file '.local/src/mpfr-*/COPYING.LESSER:python-flint.libs/mpfr-*/COPYING.LESSER' \
124+
--license-file '.local/src/flint-*/COPYING:python-flint.libs/flint-*/COPYING' \
125+
--license-file '.local/src/flint-*/COPYING.LESSER:python-flint.libs/flint-*/COPYING.LESSER' \
126+
""",
127+
"auditwheel repair -w {dest_dir} {wheel}",
122128
]
123129

124130
[tool.cibuildwheel.macos]
125131
before-all = "bin/cibw_before_all_macosx_$(uname -m).sh"
126132
before-build = "pip install wheel delocate"
127133
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}",
134+
"""bin/cibw_repair_wheel_licenses.py {wheel} \
135+
--license LGPL-3.0-or-later \
136+
--license-file '.local/src/gmp-*/COPYING:python-flint.libs/gmp-*/COPYING' \
137+
--license-file '.local/src/gmp-*/COPYING.LESSERv3:python-flint.libs/gmp-*/COPYING.LESSERv3' \
138+
--license-file '.local/src/mpfr-*/COPYING:python-flint.libs/mpfr-*/COPYING' \
139+
--license-file '.local/src/mpfr-*/COPYING.LESSER:python-flint.libs/mpfr-*/COPYING.LESSER' \
140+
--license-file '.local/src/flint-*/COPYING:python-flint.libs/flint-*/COPYING' \
141+
--license-file '.local/src/flint-*/COPYING.LESSER:python-flint.libs/flint-*/COPYING.LESSER' \
142+
""",
143+
"delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}",
132144
]
133145

134146
[tool.cibuildwheel.windows]
135147
before-all = "C:\\msys64\\usr\\bin\\bash bin/cibw_before_all_windows.sh"
136148
before-build = "pip install wheel delvewheel"
137149
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",
150+
"""python bin/cibw_repair_wheel_licenses.py {wheel} \
151+
--license LGPL-3.0-or-later \
152+
--license-file '.local/src/gmp-*/COPYING:python-flint.libs/gmp-*/COPYING' \
153+
--license-file '.local/src/gmp-*/COPYING.LESSERv3:python-flint.libs/gmp-*/COPYING.LESSERv3' \
154+
--license-file '.local/src/mpfr-*/COPYING:python-flint.libs/mpfr-*/COPYING' \
155+
--license-file '.local/src/mpfr-*/COPYING.LESSER:python-flint.libs/mpfr-*/COPYING.LESSER' \
156+
--license-file '.local/src/flint-*/COPYING:python-flint.libs/flint-*/COPYING' \
157+
--license-file '.local/src/flint-*/COPYING.LESSER:python-flint.libs/flint-*/COPYING.LESSER' \
158+
""",
159+
"delvewheel repair -w {dest_dir} {wheel} --add-path .local/bin",
142160
]

0 commit comments

Comments
 (0)