forked from CadQuery/OCP
-
Notifications
You must be signed in to change notification settings - Fork 1
/
setup.py
318 lines (270 loc) · 11 KB
/
setup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""
Build OCP+VTK wheel with shared library dependencies bundled
*** This proof-of-concept wheel MAY NOT BE DISTRIBUTED ***
*** as it does not include the requisite license texts ***
*** of the bundled libraries. ***
From the directory containing this file, and with an appropriate conda
environment activated:
$ python -m build --no-isolation
will build a manylinux wheel into `dist/`.
A conda environment with `OCP` (and all its dependencies, including
`vtk`), `auditwheel`, and `build` (the PEP 517 compatible Python
package builder) python packages is required, as well as the
`patchelf` binary. Note that the vtk package needs to be bundled to
avoid multiple copies of the VTK shared libraries, which appears to
cause errors.
This setuptools build script works by first adding the installed `OCP`
and `vtk` python package files into a wheel. This wheel is not
portable as library dependencies are missing, so we use auditwheel to
bundle them into the wheel.
Note that auditwheel is a tool used by many packages to produce
`manylinux` python wheels. It may be straightforward to use
`delocate` and `delvewheel`, which are similar to auditwheel, to
produce macOS and Windows wheels.
"""
import OCP
import glob
import json
import os.path
import platform
import re
from setuptools import Extension, setup
import setuptools.command.build_ext
import shutil
import subprocess
import sys
import vtkmodules
import wheel.bdist_wheel
import zipfile
class copy_installed(setuptools.command.build_ext.build_ext):
"""Build by copying files installed by conda"""
def build_extension(self, ext):
# self.build_lib is created when packages are copied. But
# there are no packages, so we have to create it here.
os.mkdir(os.path.dirname(self.build_lib))
os.mkdir(self.build_lib)
# OCP is a single-file extension; just copy it
shutil.copy(OCP.__file__, self.build_lib)
# vtkmodules is a package; copy it while excluding __pycache__
assert vtkmodules.__file__.endswith(os.path.join(os.sep, "vtkmodules", "__init__.py"))
shutil.copytree(
os.path.dirname(vtkmodules.__file__),
os.path.join(self.build_lib, "vtkmodules"),
ignore=shutil.ignore_patterns("__pycache__"),
)
class bdist_wheel_repaired(wheel.bdist_wheel.bdist_wheel):
"""bdist_wheel followed by auditwheel-repair"""
def run(self):
super().run()
dist_files = self.distribution.dist_files
# Exactly one wheel has been created in `self.dist_dir` and
# recorded in `dist_files`
[(_, _, bad_whl)] = dist_files
assert os.path.dirname(bad_whl) == self.dist_dir
with zipfile.ZipFile(bad_whl) as f:
bad_whl_files = set(zi.filename for zi in f.infolist() if not zi.is_dir())
# Conda libraries depend on their location in $conda_prefix because
# relative RPATHs are used find libraries elsewhere in $conda_prefix
# (e.g. [$ORIGIN/../../..:$ORIGIN/../../../]).
#
# `auditwheel` works by expanding the wheel into a temporary
# directory and computing the external shared libraries required.
# But the relative RPATHs are broken, so this fails. Thankfully,
# RPATHs all resolve to $conda_prefix/lib, so we can set
# LD_LIBRARY_PATH to allow `auditwheel` to find them.
lib_path = os.path.join(conda_prefix, "lib")
# Do the repair, placing the repaired wheel into out_dir.
out_dir = os.path.join(self.dist_dir, "repaired")
system = platform.system()
if system == "Linux":
repair_wheel_linux(lib_path, bad_whl, out_dir)
elif system == "Darwin":
repair_wheel_macos(lib_path, bad_whl, out_dir)
elif system == "Windows":
repair_wheel_windows(lib_path, bad_whl, out_dir)
else:
raise Exception(f"unsupported system {system!r}")
# Add licenses of bundled libraries
[repaired_whl] = glob.glob(os.path.join(out_dir, "*.whl"))
with zipfile.ZipFile(repaired_whl) as f:
repaired_whl_files = set(zi.filename for zi in f.infolist() if not zi.is_dir())
added_files = repaired_whl_files - bad_whl_files
add_licenses_bundled(conda_prefix, repaired_whl, added_files)
# Exactly one whl is expected in the dist dir, so delete the
# bad wheel and move the repaired wheel in.
os.unlink(bad_whl)
new_whl = os.path.join(self.dist_dir, os.path.basename(repaired_whl))
shutil.move(repaired_whl, new_whl)
os.rmdir(out_dir)
dist_files[0] = dist_files[0][:-1] + (new_whl,)
def repair_wheel_linux(lib_path, whl, out_dir):
plat = "manylinux_2_31_x86_64"
args = [
"env",
f"LD_LIBRARY_PATH={lib_path}",
sys.executable,
"-m",
"auditwheel",
"show",
whl,
]
subprocess.check_call(args)
args = [
"env",
f"LD_LIBRARY_PATH={lib_path}",
sys.executable,
"-m",
"auditwheel",
"repair",
f"--plat={plat}",
f"--wheel-dir={out_dir}",
whl,
]
subprocess.check_call(args)
def repair_wheel_macos(lib_path, whl, out_dir):
args = [
"env",
f"DYLD_LIBRARY_PATH={lib_path}",
sys.executable,
"-m",
"delocate.cmd.delocate_listdeps",
whl,
]
subprocess.check_call(args)
# Overwrites the wheel in-place by default
args = [
"env",
f"DYLD_LIBRARY_PATH={lib_path}",
sys.executable,
"-m",
"delocate.cmd.delocate_wheel",
f"--wheel-dir={out_dir}",
whl,
]
subprocess.check_call(args)
def repair_wheel_windows(lib_path, whl, out_dir):
args = [sys.executable, "-m", "delvewheel", "show", whl]
subprocess.check_call(args)
args = [
sys.executable,
"-m",
"delvewheel",
"repair",
f"--wheel-dir={out_dir}",
whl,
]
subprocess.check_call(args)
def add_licenses_bundled(conda_prefix, whl, added_files):
"""
Add licenses of bundled libraries
A file called "LICENSES_bundled.txt" will be added into the wheel,
containing the license files of bundled libraries. License
information is taken from metadata in the conda env.
"""
with open("LICENSES_bundled.txt", "w") as f:
f.write("This wheel distribution bundles a number of libraries that\n")
f.write("are compatibly licensed. We list them here.\n")
write_licenses(conda_prefix, whl, ["ocp", "vtk"], added_files, f)
with zipfile.ZipFile(whl, mode="a") as f:
f.write("LICENSES_bundled.txt")
def write_licenses(prefix, whl, always_pkgs, added_files, out):
"""
Write licenses of bundled libraries to out
"""
# Mapping from package name (e.g. "ocp") to metadata
pkgs = {}
# Mapping from name of installed file (e.g. "libfoo.so") to
# packages that may have installed it
name_to_pkgs = {}
# Populate the two maps above
meta_pat = os.path.join(prefix, "conda-meta", "*.json")
for fn in glob.glob(meta_pat):
with open(fn) as f:
meta = json.load(f)
pkgs[meta["name"]] = meta
for p in meta["files"]:
name_to_pkgs.setdefault(os.path.basename(p), set()).add(meta["name"])
# Figure out which packages the added files are from
bundled_pkgs = set()
added_files = sorted(added_files)
not_found = []
print(f"{added_files=}")
for fn in added_files:
n = os.path.basename(fn)
if n in name_to_pkgs:
bundled_pkgs.update(name_to_pkgs[n])
continue
# auditwheel and delvewheel rename bundled libraries to avoid
# clashes. We undo this renaming in order to match to file
# lists in conda.
u = try_unmangle(n)
if u and u in name_to_pkgs:
bundled_pkgs.update(name_to_pkgs[u])
continue
not_found.append(fn)
print(f"{not_found=}")
bundled_pkgs = sorted(bundled_pkgs)
print(f"{bundled_pkgs=}")
for n in sorted(set(always_pkgs) | set(bundled_pkgs)):
m = pkgs[n]
pkg_dir = os.path.normpath(m["extracted_package_dir"])
info_pat = os.path.join(pkg_dir, "info", "[Ll][Ii][Cc][Ee][Nn][CcSs][Ee]*", "**")
share_pat = os.path.join(pkg_dir, "share", "[Ll][Ii][Cc][Ee][Nn][CcSs][Ee]*", "**")
licenses = glob.glob(info_pat, recursive=True) + glob.glob(share_pat, recursive=True)
licenses = [fn for fn in licenses if not os.path.isdir(fn)]
licenses.sort()
print(file=out)
print(f"Conda package: {m['name']}", file=out)
print(f"Download url: {m['url']}", file=out)
print(f"License: {m.get('license', 'unknown')}", file=out)
for i, fn in enumerate(licenses, 1):
if len(licenses) > 1:
desc = f"{i} of {len(licenses)} license files"
else:
desc = "the only license file"
print(f"Contents of {os.path.relpath(fn, pkg_dir)} ({desc}):", file=out)
with open(fn, "rb") as f:
raw = f.read()
for l in raw.decode(errors="replace").splitlines():
print(f"> {l.rstrip()}", file=out)
def try_unmangle(n):
# delvewheel mangling example: "vtkCommonColor-9.0.dll" => "vtkCommonColor-9.0-87ee4902.dll"
m = re.match("^(.+)-[0-9A-Fa-f]{8,}([.]dll)$", n)
if m:
return m.group(1) + m.group(2)
# auditwheel mangling example: "libvtkCommonColor-9.0.so.9.0.1" => "libvtkCommonColor-9-9810eeb7.0.so.9.0.1"
m = re.match("^([^.]+)-[0-9A-Fa-f]{8,}([.].+)$", n)
if m:
return m.group(1) + m.group(2)
# Get the metadata for conda and the `ocp` package.
conda = "conda.bat" if platform.system() == "Windows" else "conda"
args = [conda, "info", "--json"]
info = json.loads(subprocess.check_output(args))
conda_prefix = info["active_prefix"] or info["conda_prefix"]
args = [conda, "list", "--json", "^ocp$"]
[ocp_meta] = json.loads(subprocess.check_output(args))
setup(
name="ocp-vtk",
version=ocp_meta["version"],
description="OCP+VTK wheel with shared library dependencies bundled.",
long_description=open("README.md").read(),
long_description_content_type='text/markdown',
author="fp473, roipoussiere",
url='https://github.com/roipoussiere/OCP',
download_url="https://github.com/roipoussiere/OCP/releases",
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: Apache Software License",
"Operating System :: POSIX",
"Operating System :: MacOS",
"Operating System :: Unix",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Scientific/Engineering"
],
# Dummy extension to trigger build_ext
ext_modules=[Extension("__dummy__", sources=[])],
cmdclass={"bdist_wheel": bdist_wheel_repaired, "build_ext": copy_installed},
)