Skip to content

Commit 73d79a3

Browse files
committed
Update build-wheel for new toolchain arrangement, and remove duplication of environment variables
1 parent 077259b commit 73d79a3

File tree

6 files changed

+119
-148
lines changed

6 files changed

+119
-148
lines changed

server/pypi/build-wheel.py

Lines changed: 104 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import argparse
44
from copy import deepcopy
55
import csv
6-
from dataclasses import dataclass, field
6+
from dataclasses import dataclass
77
from email import generator, message, parser
88
from glob import glob
99
import jsonschema
@@ -46,7 +46,7 @@
4646
# TODO: break out the build script fragments which get the actual version numbers from the
4747
# toolchain, and call them here.
4848
COMPILER_LIBS = {
49-
"libc++_shared.so": ("chaquopy-libcxx", "10000"),
49+
"libc++_shared.so": ("chaquopy-libcxx", "11000"),
5050
"libomp.so": ("chaquopy-libomp", "9.0.9"),
5151
}
5252

@@ -55,17 +55,13 @@
5555
class Abi:
5656
name: str # Android ABI name.
5757
tool_prefix: str # GCC target triplet.
58-
cflags: str = field(default="")
59-
ldflags: str = field(default="")
58+
api_level: int
6059

61-
# If any flags are changed, consider also updating target/build-common-tools.sh.
6260
ABIS = {abi.name: abi for abi in [
63-
Abi("armeabi-v7a", "arm-linux-androideabi",
64-
cflags="-march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb", # See standalone
65-
ldflags="-march=armv7-a -Wl,--fix-cortex-a8"), # toolchain docs.
66-
Abi("arm64-v8a", "aarch64-linux-android"),
67-
Abi("x86", "i686-linux-android"),
68-
Abi("x86_64", "x86_64-linux-android"),
61+
Abi("armeabi-v7a", "arm-linux-androideabi", 16),
62+
Abi("arm64-v8a", "aarch64-linux-android", 21),
63+
Abi("x86", "i686-linux-android", 16),
64+
Abi("x86_64", "x86_64-linux-android", 21),
6965
]}
7066

7167

@@ -106,8 +102,12 @@ def main(self):
106102
sys.exit(1)
107103

108104
def unpack_and_build(self):
105+
platform_tag = f"android_{self.api_level}_{self.abi.replace('-', '_')}"
106+
self.non_python_compat_tag = f"py3-none-{platform_tag}"
109107
if self.needs_python:
110108
self.find_python()
109+
python_tag = "cp" + self.python.replace('.', '')
110+
self.compat_tag = f"{python_tag}-{python_tag}-{platform_tag}"
111111
else:
112112
self.compat_tag = self.non_python_compat_tag
113113

@@ -156,17 +156,22 @@ def parse_args(self):
156156

157157
ap.add_argument("--no-reqs", action="store_true", help="Skip extracting requirements "
158158
"(any existing build/.../requirements directory will be reused)")
159-
ap.add_argument("--toolchain", metavar="DIR", type=abspath, required=True,
160-
help="Path to toolchain")
159+
ap.add_argument("--abi", metavar="ABI", required=True, choices=ABIS,
160+
help="Android ABI: choices=%(choices)s")
161+
default_api_level = {abi.name: abi.api_level for abi in ABIS.values()}
162+
ap.add_argument("--api-level", metavar="LEVEL",
163+
help=f"Android API level: default={default_api_level}")
161164
ap.add_argument("--python", metavar="X.Y", help="Python version (required for "
162165
"Python packages)"),
163166
ap.add_argument("package", help=f"Name of a package in {RECIPES_DIR}, or if it "
164167
f"contains a slash, path to a recipe directory")
165168
ap.parse_args(namespace=self)
166169

167-
self.detect_toolchain()
168-
self.platform_tag = f"android_{self.api_level}_{self.abi.replace('-', '_')}"
169-
self.non_python_compat_tag = f"py3-none-{self.platform_tag}"
170+
if not self.api_level:
171+
self.api_level = default_api_level[self.abi]
172+
self.standard_libs = sum((names for min_level, names in STANDARD_LIBS
173+
if self.api_level >= min_level),
174+
start=[])
170175

171176
def find_python(self):
172177
if self.python is None:
@@ -185,17 +190,16 @@ def find_python(self):
185190
except ValueError:
186191
raise ERROR
187192

188-
self.python_include_dir = f"{self.toolchain}/sysroot/usr/include/python{self.python}"
189-
assert_isdir(self.python_include_dir)
190-
libpython = f"libpython{self.python}.so"
191-
self.python_lib = f"{self.toolchain}/sysroot/usr/lib/{libpython}"
192-
assert_exists(self.python_lib)
193-
self.standard_libs.append(libpython)
193+
target_dir = abspath(f"{PYPI_DIR}/../../maven/com/chaquo/python/target")
194+
versions = [ver for ver in os.listdir(target_dir) if ver.startswith(self.python)]
195+
if not versions:
196+
raise CommandError(f"Can't find Python {self.python} in {target_dir}")
197+
max_ver = max(versions, key=lambda ver: map(int, re.split(r"[.-]", ver)))
198+
self.python_maven_dir = f"{target_dir}/{max_ver}"
194199

200+
# Many setup.py scripts will behave differently depending on the Python version,
201+
# so we run pip with a matching version.
195202
self.pip = f"python{self.python} -m pip --disable-pip-version-check"
196-
self.compat_tag = (f"cp{self.python.replace('.', '')}-"
197-
f"cp{self.python.replace('.', '')}-"
198-
f"{self.platform_tag}")
199203

200204
def unpack_source(self):
201205
source = self.meta["source"]
@@ -309,11 +313,13 @@ def build_wheel(self):
309313

310314
def extract_requirements(self):
311315
ensure_empty(self.reqs_dir)
312-
reqs = self.get_requirements("host")
313-
if not reqs:
314-
return
316+
for subdir in ["include", "lib"]:
317+
ensure_dir(f"{self.reqs_dir}/chaquopy/{subdir}")
318+
self.create_dummy_libs()
319+
if self.needs_python:
320+
self.extract_python()
315321

316-
for package, version in reqs:
322+
for package, version in self.get_requirements("host"):
317323
dist_dir = f"{PYPI_DIR}/dist/{normalize_name_pypi(package)}"
318324
matches = []
319325
if exists(dist_dir):
@@ -352,14 +358,34 @@ def extract_requirements(self):
352358
(r"^(lib.*?)\d+\.so$", r"\1.so"), # e.g. libpng
353359
(r"^(lib.*)_chaquopy\.so$", r"\1.so")] # e.g. libjpeg
354360
reqs_lib_dir = f"{self.reqs_dir}/chaquopy/lib"
355-
if exists(reqs_lib_dir):
356-
for filename in os.listdir(reqs_lib_dir):
357-
for pattern, repl in SONAME_PATTERNS:
358-
link_filename = re.sub(pattern, repl, filename)
359-
if link_filename in self.standard_libs:
360-
continue # e.g. torch has libc10.so, which would become libc.so.
361-
if link_filename != filename:
362-
run(f"ln -s {filename} {reqs_lib_dir}/{link_filename}")
361+
for filename in os.listdir(reqs_lib_dir):
362+
for pattern, repl in SONAME_PATTERNS:
363+
link_filename = re.sub(pattern, repl, filename)
364+
if link_filename in self.standard_libs:
365+
continue # e.g. torch has libc10.so, which would become libc.so.
366+
if link_filename != filename:
367+
run(f"ln -s {filename} {reqs_lib_dir}/{link_filename}")
368+
369+
# On Android, some libraries are incorporated into libc. Create empty .a files so we
370+
# don't have to patch everything that links against them.
371+
def create_dummy_libs(self):
372+
for name in ["pthread", "rt"]:
373+
run(f"ar rc {self.reqs_dir}/chaquopy/lib/lib{name}.a")
374+
375+
def extract_python(self):
376+
run(f"unzip -q -d {self.reqs_dir}/chaquopy "
377+
f"{self.python_maven_dir}/target-*-{self.abi}.zip "
378+
f"include/* jniLibs/*")
379+
run(f"mv {self.reqs_dir}/chaquopy/jniLibs/{self.abi}/* {self.reqs_dir}/chaquopy/lib",
380+
shell=True)
381+
run(f"rm -r {self.reqs_dir}/chaquopy/jniLibs")
382+
383+
self.python_include_dir = f"{self.reqs_dir}/chaquopy/include/python{self.python}"
384+
assert_exists(self.python_include_dir)
385+
libpython = f"libpython{self.python}.so"
386+
self.python_lib = f"{self.reqs_dir}/chaquopy/lib/{libpython}"
387+
assert_exists(self.python_lib)
388+
self.standard_libs.append(libpython)
363389

364390
def build_with_script(self, build_script):
365391
prefix_dir = f"{self.build_dir}/prefix"
@@ -379,13 +405,21 @@ def build_with_pip(self):
379405
wheel_filename, = glob("*.whl") # Note comma
380406
return abspath(wheel_filename)
381407

382-
# The environment variables set in this function are used for native builds by
383-
# distutils.sysconfig.customize_compiler. To make builds as consistent as possible, we
384-
# define values for all environment variables used by distutils in any supported Python
385-
# version. We also define some common variables like LD and STRIP which aren't used
386-
# by distutils, but might be used by custom build scripts.
387408
def update_env(self):
388409
env = {}
410+
for line in run(
411+
f"abi={self.abi}; api_level={self.api_level}; prefix={self.reqs_dir}/chaquopy; "
412+
f". {PYPI_DIR}/../../target/build-common.sh; export",
413+
shell=True, text=True, capture_output=True
414+
).stdout.splitlines():
415+
match = re.search(r"^export (\w+)='(.*)'$", line)
416+
if match:
417+
key, value = match.groups()
418+
if os.environ.get(key) != value:
419+
env[key] = value
420+
421+
# See env/bin/pkg-config.
422+
del env["PKG_CONFIG"]
389423

390424
env_dir = f"{PYPI_DIR}/env"
391425
env["PATH"] = os.pathsep.join([
@@ -400,72 +434,16 @@ def update_env(self):
400434
pythonpath.append(os.environ["PYTHONPATH"])
401435
env["PYTHONPATH"] = os.pathsep.join(pythonpath)
402436

403-
abi = ABIS[self.abi]
404-
for tool in ["ar", "as", ("cc", "gcc"), ("cxx", "g++"),
405-
("fc", "gfortran"), # Used by openblas
406-
("f77", "gfortran"), ("f90", "gfortran"), # Used by numpy.distutils
407-
"ld", "nm", "ranlib", "readelf", "strip"]:
408-
var, suffix = (tool, tool) if isinstance(tool, str) else tool
409-
filename = f"{self.toolchain}/bin/{abi.tool_prefix}-{suffix}"
410-
if suffix != "gfortran": # Only required for SciPy and OpenBLAS.
411-
assert_exists(filename)
412-
env[var.upper()] = filename
413-
env["LDSHARED"] = f"{env['CC']} -shared"
437+
# This flag often catches errors in .so files which would otherwise be delayed
438+
# until runtime. (Some of the more complex build.sh scripts need to remove this, or
439+
# use it more selectively.)
440+
env["LDFLAGS"] += " -Wl,--no-undefined"
414441

415-
# If any flags are changed, consider also updating target/build-common-tools.sh.
416-
gcc_flags = " ".join([
417-
"-fPIC", # See standalone toolchain docs, and note below about -pie
418-
abi.cflags])
419-
env["CFLAGS"] = gcc_flags
420-
env["FARCH"] = gcc_flags # Used by numpy.distutils Fortran compilation.
421-
422-
# If any flags are changed, consider also updating target/build-common-tools.sh.
423-
#
424-
# Not including -pie despite recommendation in standalone toolchain docs, because it
425-
# causes the linker to forget it's supposed to be building a shared library
426-
# (https://lists.debian.org/debian-devel/2016/05/msg00302.html). It can be added
427-
# manually for packages which require it (e.g. hdf5).
428-
env["LDFLAGS"] = " ".join([
429-
# This flag often catches errors in .so files which would otherwise be delayed
430-
# until runtime. (Some of the more complex build.sh scripts need to remove this, or
431-
# use it more selectively.)
432-
#
433-
# I tried also adding -Werror=implicit-function-declaration to CFLAGS, but that
434-
# breaks too many things (e.g. `has_function` in distutils.ccompiler).
435-
"-Wl,--no-undefined",
436-
437-
# This currently only affects armeabi-v7a, but could affect other ABIs if the
438-
# unwinder implementation changes in a future NDK version
439-
# (https://android.googlesource.com/platform/ndk/+/ndk-release-r21/docs/BuildSystemMaintainers.md#Unwinding).
440-
# See also comment in build-fortran.sh.
441-
"-Wl,--exclude-libs,libgcc.a", # NDK r18
442-
"-Wl,--exclude-libs,libgcc_real.a", # NDK r19 and later
443-
"-Wl,--exclude-libs,libunwind.a",
444-
445-
# Many packages get away with omitting this on standard Linux.
446-
"-lm",
447-
448-
abi.ldflags])
449-
450-
reqs_prefix = f"{self.reqs_dir}/chaquopy"
451-
if exists(reqs_prefix):
452-
env["PKG_CONFIG_LIBDIR"] = f"{reqs_prefix}/lib/pkgconfig"
453-
env["CFLAGS"] += f" -I{reqs_prefix}/include"
454-
455-
# --rpath-link only affects arm64, because it's the only ABI which uses ld.bfd. The
456-
# others all use ld.gold, which doesn't try to resolve transitive shared library
457-
# dependencies. When we upgrade to a later version of the NDK which uses LLD, we
458-
# can probably remove this flag, along with all requirements in meta.yaml files
459-
# which are tagged with "ld.bfd".
460-
env["LDFLAGS"] += (f" -L{reqs_prefix}/lib"
461-
f" -Wl,--rpath-link,{reqs_prefix}/lib")
462-
463-
env["ARFLAGS"] = "rc"
464-
465-
# Set all unused overridable variables to the empty string to prevent the host Python
466-
# values (if any) from taking effect.
467-
for var in ["CPPFLAGS", "CXXFLAGS"]:
468-
env[var] = ""
442+
# Set all other variables used by distutils to prevent the host Python values (if
443+
# any) from taking effect.
444+
env["CPPFLAGS"] = ""
445+
env["CXXFLAGS"] = ""
446+
env["LDSHARED"] = f"{env['CC']} -shared"
469447

470448
# Use -idirafter so that package-specified -I directories take priority (e.g. in grpcio
471449
# and typed-ast).
@@ -498,9 +476,16 @@ def update_env(self):
498476
if self.needs_cmake:
499477
self.generate_cmake_toolchain()
500478

501-
# Define the minimum necessary to keep CMake happy. To avoid duplication, we still want to
502-
# configure as much as possible via update_env.
503479
def generate_cmake_toolchain(self):
480+
raise CommandError("TODO: CMake support needs to be updated.")
481+
# TODO: Generate a toolchain file which sets ANDROID_ABI, ANDROID_PLATFORM, and
482+
# any other necessary variables (see
483+
# https://developer.android.com/ndk/guides/cmake#build-command -- though these
484+
# might not all be necessary with the current NDK), and then includes the
485+
# toolchain file from the NDK. To avoid needing to patch every package that uses
486+
# CMake, we can then set the CMAKE_TOOLCHAIN_FILE environment variable, which was
487+
# added in CMake 3.21.
488+
504489
# See build/cmake/android.toolchain.cmake in the NDK.
505490
CMAKE_PROCESSORS = {
506491
"armeabi-v7a": "armv7-a",
@@ -510,6 +495,9 @@ def generate_cmake_toolchain(self):
510495
}
511496
clang_target = f"{ABIS[self.abi].tool_prefix}{self.api_level}".replace("arm-", "armv7a-")
512497

498+
# Define the minimum necessary to keep CMake happy. To avoid confusion about where
499+
# settings are coming from, we still want to configure as much as possible via
500+
# environment variables.
513501
toolchain_filename = join(self.build_dir, "chaquopy.toolchain.cmake")
514502
log(f"Generating {toolchain_filename}")
515503
with open(toolchain_filename, "w") as toolchain_file:
@@ -555,31 +543,6 @@ def generate_cmake_toolchain(self):
555543
SET(PYTHON_MODULE_EXTENSION .so)
556544
"""), file=toolchain_file)
557545

558-
def detect_toolchain(self):
559-
clang = f"{self.toolchain}/bin/clang"
560-
for word in open(clang).read().split():
561-
if word.startswith("--target"):
562-
match = re.search(r"^--target=(.+?)(\d+)$", word)
563-
if not match:
564-
raise CommandError(f"Couldn't parse '{word}' in {clang}")
565-
566-
for abi in ABIS.values():
567-
if match[1] == abi.tool_prefix.replace("arm-", "armv7a-"):
568-
self.abi = abi.name
569-
break
570-
else:
571-
raise CommandError(f"Unknown triplet '{match[1]}' in {clang}")
572-
573-
self.api_level = int(match[2])
574-
self.standard_libs = sum((names for min_level, names in STANDARD_LIBS
575-
if self.api_level >= min_level),
576-
start=[])
577-
break
578-
else:
579-
raise CommandError(f"Couldn't find target in {clang}")
580-
581-
log(f"Toolchain ABI: {self.abi}, API level: {self.api_level}")
582-
583546
def fix_wheel(self, in_filename):
584547
tmp_dir = f"{self.build_dir}/fix_wheel"
585548
ensure_empty(tmp_dir)
@@ -796,10 +759,15 @@ def normalize_version(version):
796759
return str(pkg_resources.parse_version(version))
797760

798761

799-
def run(command, check=True):
762+
def run(command, **kwargs):
800763
log(command)
764+
kwargs.setdefault("check", True)
765+
kwargs.setdefault("shell", False)
766+
767+
if isinstance(command, str) and not kwargs["shell"]:
768+
command = shlex.split(command)
801769
try:
802-
return subprocess.run(shlex.split(command), check=check)
770+
return subprocess.run(command, **kwargs)
803771
except subprocess.CalledProcessError as e:
804772
raise CommandError(f"Command returned exit status {e.returncode}")
805773

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package:
22
name: chaquopy-libcxx
3-
version: 10000 # See PKG_VERSION in build.sh, and COMPILER_LIBS in build-wheel.py.
3+
version: 11000 # See PKG_VERSION in build.sh, and COMPILER_LIBS in build-wheel.py.
44

55
source: null

server/pypi/packages/numpy/meta.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ requirements:
77
- Cython 0.29.24
88
host:
99
- chaquopy-openblas 0.2.20
10-
- chaquopy-libgfortran 4.9 # Required by ld.bfd on arm64 (see build-wheel.py)

server/pypi/packages/torch/meta.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,5 @@ requirements:
1717
- typing-extensions 3.10.0.0
1818
host:
1919
- python
20-
- chaquopy-libgfortran 4.9 # arm64-v8a uses ld.bfd, which transitively resolves all symbols.
2120
- chaquopy-openblas 0.2.20
2221
- numpy 1.17.4

server/pypi/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# These packages are included by default in a new virtualenv, but since we use them directly,
22
# specify exact versions for reproducible builds.
3-
pip==18.1
3+
pip==19.2.3
44
setuptools==46.4.0
55
wheel==0.33.6
66

0 commit comments

Comments
 (0)