Browse Source

makewheel: Allow creation of wheels for different targets than the host,
along with iOS support.

Donny Lawrence 6 years ago
parent
commit
0b68b0678d
1 changed files with 271 additions and 110 deletions
  1. 271 110
      makepanda/makewheel.py

+ 271 - 110
makepanda/makewheel.py

@@ -7,47 +7,90 @@ the `--wheel` parameter.
 Please keep this file work with Panda3D 1.9 until that reaches EOL.
 """
 from __future__ import print_function, unicode_literals
-from distutils.util import get_platform
+import distutils.util
 import json
 
 import sys
 import os
 from os.path import join
-import shutil
 import zipfile
 import hashlib
 import tempfile
 import subprocess
+import configparser
 from distutils.sysconfig import get_config_var
 from optparse import OptionParser
-from makepandacore import ColorText, LocateBinary, ParsePandaVersion, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue
 from base64 import urlsafe_b64encode
 
+cfg_parser = None
+
+def get_metadata_value(key):
+    global cfg_parser
+    if not cfg_parser:
+        # Parse the metadata from the setup.cfg file.
+        cfg_parser = configparser.ConfigParser()
+        path = os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')
+        assert cfg_parser.read(path), "Could not read setup.cfg file."
+
+    value = cfg_parser.get('metadata', key)
+    if key == 'classifiers':
+        value = value.strip().split('\n')
+    return value
+
+def get_host():
+    """Returns the host platform, ie. the one we're compiling on."""
+    if sys.platform == 'win32' or sys.platform == 'cygwin':
+        # sys.platform is win32 on 64-bits Windows as well.
+        return 'windows'
+    elif sys.platform == 'darwin':
+        return 'darwin'
+    elif sys.platform.startswith('linux'):
+        try:
+            # Python seems to offer no built-in way to check this.
+            osname = subprocess.check_output(["uname", "-o"])
+            if osname.strip().lower() == b'android':
+                return 'android'
+            else:
+                return 'linux'
+        except:
+            return 'linux'
+    elif sys.platform.startswith('freebsd'):
+        return 'freebsd'
+    else:
+        exit('Unrecognized sys.platform: %s' % (sys.platform))
+
+def locate_binary(binary):
+    """
+    Searches the system PATH for the binary.
+    :param binary: Name of the binary to locate.
+    :return: The full path to the binary, or None if not found.
+    """
+    if os.path.isfile(binary):
+        return binary
+
+    if "PATH" not in os.environ or os.environ["PATH"] == "":
+        p = os.defpath
+    else:
+        p = os.environ["PATH"]
 
-def get_abi_tag():
-    if sys.version_info >= (3, 0):
-        soabi = get_config_var('SOABI')
-        if soabi and soabi.startswith('cpython-'):
-            return 'cp' + soabi.split('-')[1]
-        elif soabi:
-            return soabi.replace('.', '_').replace('-', '_')
-
-    soabi = 'cp%d%d' % (sys.version_info[:2])
-
-    debug_flag = get_config_var('Py_DEBUG')
-    if (debug_flag is None and hasattr(sys, 'gettotalrefcount')) or debug_flag:
-        soabi += 'd'
+    pathList = p.split(os.pathsep)
+    suffixes = ['']
 
-    malloc_flag = get_config_var('WITH_PYMALLOC')
-    if malloc_flag is None or malloc_flag:
-        soabi += 'm'
+    if get_host() == 'windows':
+        if not binary.lower().endswith('.exe') and not binary.lower().endswith('.bat'):
+            # Append .exe if necessary
+            suffixes = ['.exe', '.bat']
 
-    if sys.version_info < (3, 3):
-        usize = get_config_var('Py_UNICODE_SIZE')
-        if (usize is None and sys.maxunicode == 0x10ffff) or usize == 4:
-            soabi += 'u'
+        # On Windows the current directory is always implicitly
+        # searched before anything else on PATH.
+        pathList = ['.'] + pathList
 
-    return soabi
+    for path in pathList:
+        binpath = os.path.join(os.path.expanduser(path), binary)
+        for suffix in suffixes:
+            if os.access(binpath + suffix, os.X_OK):
+                return os.path.abspath(os.path.realpath(binpath + suffix))
+    return None
 
 
 def is_exe_file(path):
@@ -74,11 +117,6 @@ def is_fat_file(path):
                                         b'\xCA\xFE\xBA\xBF', b'\xBF\xBA\xFE\xCA')
 
 
-def get_python_ext_module_dir():
-    import _ctypes
-    return os.path.dirname(_ctypes.__file__)
-
-
 if sys.platform in ('win32', 'cygwin'):
     is_executable = is_exe_file
 elif sys.platform == 'darwin':
@@ -86,9 +124,117 @@ elif sys.platform == 'darwin':
 else:
     is_executable = is_elf_file
 
+
+class TargetInfo:
+    """
+    Holds information about the system the wheel is being prepared for.
+    """
+    def __init__(self,
+                 platform_tag=None,
+                 soabi='',
+                 python_version=None,
+                 python_root=sys.exec_prefix,
+                 sys_platform=get_host()):
+        """
+        With no arguments, it will be assumed that the target is the same as the
+        host (which will be most cases).
+
+        :param platform_string: The platform tag used in the wheel filename.
+        :param soabi: Value of SOABI from Python's makefile. This isn't really
+                      a thing with Python 2.
+        :param python_version: Version of Python we're bundling. Will be
+                      inferred from SOABI or the system's python version if not
+                      explicitly set.
+        :param python_root: Root of the Python installation containing the
+                      libraries to be bundled in deploy_libs.
+        :param sys_platform:
+        """
+        if platform_tag:
+            self.platform_tag = platform_tag
+        else:
+            self.platform_tag = distutils.util.get_platform()
+            if (self.platform_tag.startswith("linux-")
+                    and os.path.isfile("/lib/libc-2.5.so")
+                    and os.path.isdir("/opt/python")):
+                self.platform_tag = self.platform_tag.replace("linux",
+                                                              "manylinux1")
+
+        self.abi_flags = None
+        self.soabi = soabi
+        self.python_version = python_version
+        self.python_root = python_root
+        self.sys_platform = sys_platform
+
+        self.platform_tag = self.platform_tag.replace('-', '_').replace('.', '_')
+
+        if not self.python_version:
+            if self.soabi:
+                self.python_version = tuple(
+                    int(i) for i in self.soabi.split('-')[1].rstrip('mdu')
+                )
+                print("Inferring Python version %s from provided soabi."
+                      % str(self.python_version))
+            else:
+                self.python_version = sys.version_info[:2]
+                self.soabi = get_config_var('SOABI')
+                print("Inferring Python version %s from system version."
+                      % str(self.python_version))
+        elif type(self.python_version) is str:
+            self.abi_flags = self.python_version.lstrip('0123456789')
+            self.python_version = tuple(int(i) for i in self.python_version.rstrip('mdu'))
+
+    @property
+    def python_ext_module_dir(self):
+        return os.path.join(self.python_root,
+                            'lib/python{}.{}/lib-dynload'
+                                .format(*self.python_version))
+
+    @property
+    def python_tag(self):
+        return "cp{0}{1}".format(*self.python_version)
+
+    @property
+    def abi_tag(self):
+        if sys.version_info >= (3, 0) and self.soabi:
+            if self.soabi.startswith('cpython-'):
+                return 'cp' + self.soabi.split('-')[1]
+            return self.soabi.replace('.', '_').replace('-', '_')
+
+        abi_tag = self.python_tag
+
+        if self.abi_flags:
+            return abi_tag + self.abi_flags
+
+        debug_flag = get_config_var('Py_DEBUG')
+        if (debug_flag is None and hasattr(sys,
+                                           'gettotalrefcount')) or debug_flag:
+            abi_tag += 'd'
+
+        malloc_flag = get_config_var('WITH_PYMALLOC')
+        if malloc_flag is None or malloc_flag:
+            abi_tag += 'm'
+
+        if sys.version_info < (3, 3):
+            usize = get_config_var('Py_UNICODE_SIZE')
+            if (usize is None and sys.maxunicode == 0x10ffff) or usize == 4:
+                abi_tag += 'u'
+
+        return abi_tag
+
+    @property
+    def extension_suffix(self):
+        if 'ios' in self.platform_tag:
+            return '.so'
+        if self.soabi != '':
+            ext = '.pyd' if self.sys_platform == 'win32' else '.so'
+            return '.' + self.soabi + ext
+
+        import _imp
+        return _imp.extension_suffixes()[0]
+
 # Other global parameters
-PY_VERSION = "cp{0}{1}".format(*sys.version_info)
-ABI_TAG = get_abi_tag()
+# PY_VERSION = "cp{0}{1}".format(*sys.version_info)
+# ABI_TAG = get_abi_tag()
 EXCLUDE_EXT = [".pyc", ".pyo", ".N", ".prebuilt", ".xcf", ".plist", ".vcproj", ".sln"]
 
 # Plug-ins to install.
@@ -119,15 +265,15 @@ Tag: {0}-{1}-{2}
 """
 
 METADATA = {
-    "license": GetMetadataValue('license'),
-    "name": GetMetadataValue('name'),
+    "license": get_metadata_value('license'),
+    "name": get_metadata_value('name'),
     "metadata_version": "2.0",
     "generator": "makepanda",
-    "summary": GetMetadataValue('description'),
+    "summary": get_metadata_value('description'),
     "extensions": {
         "python.details": {
             "project_urls": {
-                "Home": GetMetadataValue('url'),
+                "Home": get_metadata_value('url'),
             },
             "document_names": {
                 "license": "LICENSE.txt"
@@ -135,13 +281,13 @@ METADATA = {
             "contacts": [
                 {
                     "role": "author",
-                    "name": GetMetadataValue('author'),
-                    "email": GetMetadataValue('author_email'),
+                    "name": get_metadata_value('author'),
+                    "email": get_metadata_value('author_email'),
                 }
             ]
         }
     },
-    "classifiers": GetMetadataValue('classifiers'),
+    "classifiers": get_metadata_value('classifiers'),
 }
 
 DESCRIPTION = """
@@ -257,13 +403,13 @@ def parse_dependencies_unix(data):
     return filenames
 
 
-def scan_dependencies(pathname):
+def scan_dependencies(pathname, target_info):
     """ Checks the named file for DLL dependencies, and adds any appropriate
     dependencies found into pluginDependencies and dependentFiles. """
 
-    if sys.platform == "darwin":
+    if target_info.sys_platform in ("darwin", "ios"):
         command = ['otool', '-XL', pathname]
-    elif sys.platform in ("win32", "cygwin"):
+    elif target_info.sys_platform in ("win32", "cygwin"):
         command = ['dumpbin', '/dependents', pathname]
     else:
         command = ['ldd', pathname]
@@ -275,7 +421,7 @@ def scan_dependencies(pathname):
         raise subprocess.CalledProcessError(retcode, command[0], output=output)
     filenames = None
 
-    if sys.platform in ("win32", "cygwin"):
+    if target_info.sys_platform in ("win32", "cygwin"):
         filenames = parse_dependencies_windows(output)
     else:
         filenames = parse_dependencies_unix(output)
@@ -283,7 +429,7 @@ def scan_dependencies(pathname):
     if filenames is None:
         sys.exit("Unable to determine dependencies from %s" % (pathname))
 
-    if sys.platform == "darwin" and len(filenames) > 0:
+    if target_info.sys_platform in ("darwin", "ios") and len(filenames) > 0:
         # Filter out the library ID.
         if os.path.basename(filenames[0]).split('.', 1)[0] == os.path.basename(pathname).split('.', 1)[0]:
             del filenames[0]
@@ -292,13 +438,13 @@ def scan_dependencies(pathname):
 
 
 class WheelFile(object):
-    def __init__(self, name, version, platform):
+    def __init__(self, name, version, target_info):
         self.name = name
         self.version = version
-        self.platform = platform
+        self.target_info = target_info
 
         wheel_name = "{0}-{1}-{2}-{3}-{4}.whl".format(
-            name, version, PY_VERSION, ABI_TAG, platform)
+            name, version, target_info.python_tag, target_info.abi_tag, target_info.platform_tag)
 
         print("Writing %s" % (wheel_name))
         self.zip_file = zipfile.ZipFile(wheel_name, 'w', zipfile.ZIP_DEFLATED)
@@ -322,15 +468,15 @@ class WheelFile(object):
 
         if dep in self.ignore_deps or dep.lower().startswith("python") or os.path.basename(dep).startswith("libpython"):
             # Don't include the Python library, or any other explicit ignore.
-            if GetVerbose():
+            if verbose:
                 print("Ignoring {0} (explicitly ignored)".format(dep))
             return
 
-        if sys.platform == "darwin" and dep.endswith(".so"):
+        if target_info.sys_platform in ("darwin", "ios") and dep.endswith(".so"):
             # Temporary hack for 1.9, which had link deps on modules.
             return
 
-        if sys.platform == "darwin" and dep.startswith("/System/"):
+        if target_info.sys_platform in ("darwin", "ios") and dep.startswith("/System/"):
             return
 
         if dep.startswith('/'):
@@ -350,7 +496,7 @@ class WheelFile(object):
 
         if not source_path:
             # Couldn't find library in the panda3d lib dir.
-            if GetVerbose():
+            if verbose:
                 print("Ignoring {0} (not in search path)".format(dep))
             return
 
@@ -371,7 +517,7 @@ class WheelFile(object):
 
             # Scan Unix dependencies.
             if target_path not in IGNORE_UNIX_DEPS_OF:
-                deps = scan_dependencies(source_path)
+                deps = scan_dependencies(source_path, target_info)
             else:
                 deps = []
 
@@ -384,14 +530,14 @@ class WheelFile(object):
             temp = tempfile.NamedTemporaryFile(suffix=suffix, prefix='whl', delete=False)
 
             # On macOS, if no fat wheel was requested, extract the right architecture.
-            if sys.platform == "darwin" and is_fat_file(source_path) \
-                and not self.platform.endswith("_intel") \
-                and "_fat" not in self.platform:
+            if target_info.sys_platform == "darwin" and is_fat_file(source_path) \
+                and not target_info.platform_tag.endswith("_intel") \
+                and "_fat" not in target_info.platform_tag:
 
-                if self.platform.endswith("_x86_64"):
+                if target_info.platform_tag.endswith("_x86_64"):
                     arch = 'x86_64'
                 else:
-                    arch = self.platform.split('_')[-1]
+                    arch = target_info.platform_tag.split('_')[-1]
                 subprocess.call(['lipo', source_path, '-extract', arch, '-output', temp.name])
             else:
                 # Otherwise, just copy it over.
@@ -401,7 +547,7 @@ class WheelFile(object):
             os.chmod(temp.name, os.stat(temp.name).st_mode | 0o711)
 
             # Now add dependencies.  On macOS, fix @loader_path references.
-            if sys.platform == "darwin":
+            if target_info.sys_platform in ("darwin", "ios"):
                 if source_path.endswith('deploy-stubw'):
                     deps_path = '@executable_path/../Frameworks'
                 else:
@@ -411,7 +557,7 @@ class WheelFile(object):
                     if dep.endswith('/Python'):
                         # If this references the Python framework, change it
                         # to reference libpython instead.
-                        new_dep = deps_path + '/libpython{0}.{1}.dylib'.format(*sys.version_info)
+                        new_dep = deps_path + '/libpython{0}.{1}.dylib'.format(*target_info.python_version)
 
                     elif '@loader_path' in dep:
                         dep_path = dep.replace('@loader_path', '.')
@@ -422,6 +568,12 @@ class WheelFile(object):
                             continue
                         new_dep = os.path.join(deps_path, os.path.relpath(target_dep, os.path.dirname(target_path)))
 
+                    elif '@rpath' in dep:
+                        dep_path = dep.replace('@rpath', '.')
+                        target_dep = os.path.dirname(target_path) + '/' + os.path.basename(dep)
+                        self.consider_add_dependency(target_dep, dep_path)
+                        continue
+
                     elif dep.startswith('/Library/Frameworks/Python.framework/'):
                         # Add this dependency if it's in the Python directory.
                         target_dep = os.path.dirname(target_path) + '/' + os.path.basename(dep)
@@ -433,7 +585,7 @@ class WheelFile(object):
 
                     else:
                         if '/' in dep:
-                            if GetVerbose():
+                            if verbose:
                                 print("Ignoring dependency %s" % (dep))
                         continue
 
@@ -454,7 +606,7 @@ class WheelFile(object):
         ext = ext.lower()
         if ext in ('.dll', '.pyd', '.exe'):
             # Scan and add Win32 dependencies.
-            for dep in scan_dependencies(source_path):
+            for dep in scan_dependencies(source_path, target_info):
                 target_dep = os.path.dirname(target_path) + '/' + dep
                 self.consider_add_dependency(target_dep, dep)
 
@@ -474,7 +626,7 @@ class WheelFile(object):
         digest = digest.rstrip('=')
         self.records.append("{0},sha256={1},{2}\n".format(target_path, digest, size))
 
-        if GetVerbose():
+        if verbose:
             print("Adding {0} from {1}".format(target_path, orig_source_path))
         self.zip_file.write(source_path, target_path)
 
@@ -490,7 +642,7 @@ class WheelFile(object):
         digest = digest.rstrip('=')
         self.records.append("{0},sha256={1},{2}\n".format(target_path, digest, len(source_data)))
 
-        if GetVerbose():
+        if verbose:
             print("Adding %s from data" % target_path)
         self.zip_file.writestr(target_path, source_data)
 
@@ -516,26 +668,11 @@ class WheelFile(object):
         self.zip_file.close()
 
 
-def makewheel(version, output_dir, platform=None):
-    if sys.platform not in ("win32", "darwin") and not sys.platform.startswith("cygwin"):
-        if not LocateBinary("patchelf"):
+def makewheel(version, output_dir, target_info):
+    if target_info.sys_platform not in ("win32", "darwin", "ios") and not target_info.sys_platform.startswith("cygwin"):
+        if not locate_binary("patchelf"):
             raise Exception("patchelf is required when building a Linux wheel.")
 
-    if platform is None:
-        # Determine the platform from the build.
-        platform_dat = os.path.join(output_dir, 'tmp', 'platform.dat')
-        if os.path.isfile(platform_dat):
-            platform = open(platform_dat, 'r').read().strip()
-        else:
-            print("Could not find platform.dat in build directory")
-            platform = get_platform()
-            if platform.startswith("linux-"):
-                # Is this manylinux1?
-                if os.path.isfile("/lib/libc-2.5.so") and os.path.isdir("/opt/python"):
-                    platform = platform.replace("linux", "manylinux1")
-
-    platform = platform.replace('-', '_').replace('.', '_')
-
     # Global filepaths
     panda3d_dir = join(output_dir, "panda3d")
     pandac_dir = join(output_dir, "pandac")
@@ -543,7 +680,7 @@ def makewheel(version, output_dir, platform=None):
     models_dir = join(output_dir, "models")
     etc_dir = join(output_dir, "etc")
     bin_dir = join(output_dir, "bin")
-    if sys.platform == "win32":
+    if target_info.sys_platform == "win32":
         libs_dir = join(output_dir, "bin")
     else:
         libs_dir = join(output_dir, "lib")
@@ -567,24 +704,24 @@ def makewheel(version, output_dir, platform=None):
         "Home-page: {0}\n".format(homepage),
         "Author: {0}\n".format(author),
         "Author-email: {0}\n".format(email),
-        "Platform: {0}\n".format(platform),
+        "Platform: {0}\n".format(target_info.platform_tag),
     ] + ["Classifier: {0}\n".format(c) for c in METADATA['classifiers']])
 
     metadata += '\n' + DESCRIPTION.strip() + '\n'
 
     # Zip it up and name it the right thing
-    whl = WheelFile('panda3d', version, platform)
+    whl = WheelFile('panda3d', version, target_info)
     whl.lib_path = [libs_dir]
 
-    if sys.platform == "win32":
+    if target_info.sys_platform == 'win32':
         whl.lib_path.append(join(output_dir, "python", "DLLs"))
 
-    if platform.startswith("manylinux"):
+    if target_info.platform_tag.startswith("manylinux"):
         # On manylinux1, we pick up all libraries except for the ones specified
         # by the manylinux1 ABI.
         whl.lib_path.append("/usr/local/lib")
 
-        if platform.endswith("_x86_64"):
+        if target_info.platform_tag.endswith("_x86_64"):
             whl.lib_path += ["/lib64", "/usr/lib64"]
         else:
             whl.lib_path += ["/lib", "/usr/lib"]
@@ -601,7 +738,7 @@ def makewheel(version, output_dir, platform=None):
 __version__ = '{0}'
 """.format(version)
 
-    if '27' in ABI_TAG:
+    if (2, 7) in target_info.python_version:
         p3d_init += """
 if __debug__:
     import sys
@@ -614,8 +751,7 @@ if __debug__:
 
     whl.write_file_data('panda3d/__init__.py', p3d_init)
 
-    # Copy the extension modules from the panda3d directory.
-    ext_suffix = GetExtensionSuffix()
+    ext_suffix = target_info.extension_suffix
 
     for file in os.listdir(panda3d_dir):
         if file == '__init__.py':
@@ -623,7 +759,7 @@ if __debug__:
         elif file.endswith('.py') or (file.endswith(ext_suffix) and '.' not in file[:-len(ext_suffix)]):
             source_path = os.path.join(panda3d_dir, file)
 
-            if file.endswith('.pyd') and platform.startswith('cygwin'):
+            if file.endswith('.pyd') and target_info.sys_platform == 'cygwin':
                 # Rename it to .dll for cygwin Python to be able to load it.
                 target_path = 'panda3d/' + os.path.splitext(file)[0] + '.dll'
             else:
@@ -633,27 +769,28 @@ if __debug__:
 
     # And copy the extension modules from the Python installation into the
     # deploy_libs directory, for use by deploy-ng.
-    ext_suffix = '.pyd' if sys.platform in ('win32', 'cygwin') else '.so'
-    ext_mod_dir = get_python_ext_module_dir()
+    ext_suffix = '.pyd' if target_info.sys_platform in ('win32', 'cygwin') else '.so'
+    ext_mod_dir = target_info.python_ext_module_dir
 
-    for file in os.listdir(ext_mod_dir):
-        if file.endswith(ext_suffix):
-            source_path = os.path.join(ext_mod_dir, file)
+    if not 'ios' in target_info.platform_tag:
+        for file in os.listdir(ext_mod_dir):
+            if file.endswith(ext_suffix):
+                source_path = os.path.join(ext_mod_dir, file)
 
-            if file.endswith('.pyd') and platform.startswith('cygwin'):
-                # Rename it to .dll for cygwin Python to be able to load it.
-                target_path = 'deploy_libs/' + os.path.splitext(file)[0] + '.dll'
-            else:
-                target_path = 'deploy_libs/' + file
+                if file.endswith('.pyd') and target_info.sys_platform == 'cygwin':
+                    # Rename it to .dll for cygwin Python to be able to load it.
+                    target_path = 'deploy_libs/' + os.path.splitext(file)[0] + '.dll'
+                else:
+                    target_path = 'deploy_libs/' + file
 
-            whl.write_file(target_path, source_path)
+                whl.write_file(target_path, source_path)
 
     # Add plug-ins.
     for lib in PLUGIN_LIBS:
         plugin_name = 'lib' + lib
-        if sys.platform in ('win32', 'cygwin'):
+        if target_info.sys_platform in ('win32', 'cygwin'):
             plugin_name += '.dll'
-        elif sys.platform == 'darwin':
+        elif target_info.sys_platform in ('darwin', 'ios'):
             plugin_name += '.dylib'
         else:
             plugin_name += '.so'
@@ -707,18 +844,34 @@ if __debug__:
     whl.write_file_data(info_dir + '/entry_points.txt', entry_points)
     whl.write_file_data(info_dir + '/metadata.json', json.dumps(METADATA, indent=4, separators=(',', ': ')))
     whl.write_file_data(info_dir + '/METADATA', metadata)
-    whl.write_file_data(info_dir + '/WHEEL', WHEEL_DATA.format(PY_VERSION, ABI_TAG, platform))
+    whl.write_file_data(info_dir + '/WHEEL', WHEEL_DATA.format(target_info.python_tag, target_info.abi_tag, target_info.platform_tag))
     whl.write_file(info_dir + '/LICENSE.txt', license_src)
     whl.write_file(info_dir + '/README.md', readme_src)
     whl.write_file_data(info_dir + '/top_level.txt', 'direct\npanda3d\npandac\npanda3d_tools\n')
 
     # Add libpython for deployment
-    if sys.platform in ('win32', 'cygwin'):
+    if target_info.sys_platform in ('win32', 'cygwin'):
         pylib_name = 'python{0}{1}.dll'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('BINDIR'), pylib_name)
-    elif sys.platform == 'darwin':
+    elif target_info.sys_platform == 'darwin':
         pylib_name = 'libpython{0}.{1}.dylib'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
+    elif target_info.sys_platform == 'ios':
+        archs = ['arm64', 'x86_64']
+        arch = ''
+        for arch_str in archs:
+            if arch_str in target_info.platform_tag:
+                arch = arch_str
+                break
+            else:
+                raise Exception('Platform string does not specify one of ' + archs)
+
+        python_dir = os.path.abspath(join('thirdparty', 'ios-libs-%s' % arch, 'python', 'lib'))
+        pylib_name = ''
+        for filename in os.listdir(python_dir):
+            if os.path.isfile(os.path.join(python_dir, filename)) and 'libpython' in filename and filename.endswith('dylib'):
+                pylib_name = filename
+        pylib_path = join(python_dir, pylib_name)
     else:
         pylib_name = get_config_var('LDLIBRARY')
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
@@ -728,14 +881,22 @@ if __debug__:
 
 
 if __name__ == "__main__":
-    version = ParsePandaVersion("dtool/PandaVersion.pp")
+    version = get_metadata_value('version')
 
     parser = OptionParser()
     parser.add_option('', '--version', dest = 'version', help = 'Panda3D version number (default: %s)' % (version), default = version)
     parser.add_option('', '--outputdir', dest = 'outputdir', help = 'Makepanda\'s output directory (default: built)', default = 'built')
     parser.add_option('', '--verbose', dest = 'verbose', help = 'Enable verbose output', action = 'store_true', default = False)
-    parser.add_option('', '--platform', dest = 'platform', help = 'Override platform tag', default = None)
+    parser.add_option('', '--platform', dest = 'platform_tag', help = 'Override platform tag', default = None)
+    parser.add_option('', '--soabi', dest = 'soabi', help = 'SOABI, used for extension suffixes.')
+    parser.add_option('', '--pyver', dest = 'python_version', help = 'Custom Python version we\'re making the wheel for.')
+    parser.add_option('', '--pyroot', dest = 'python_root', help = 'Custom root of Python installation.', default = sys.exec_prefix)
+    parser.add_option('', '--sysplatform', dest = 'sys_platform', help = 'Output of "sys.platform" on the target', default = get_host())
     (options, args) = parser.parse_args()
 
-    SetVerbose(options.verbose)
-    makewheel(options.version, options.outputdir, options.platform)
+    ti_opts = {key: options.__dict__[key] for key in ('platform_tag', 'soabi', 'python_version', 'python_root', 'sys_platform')}
+    target_info = TargetInfo(**ti_opts)
+
+    global verbose
+    verbose = options.verbose
+    makewheel(options.version, options.outputdir, target_info)