308 lines
9.3 KiB
Python
308 lines
9.3 KiB
Python
|
"""
|
||
|
Tool for compiling iOS toolchain
|
||
|
================================
|
||
|
|
||
|
This tool intend to replace all the previous tools/ in shell script.
|
||
|
"""
|
||
|
|
||
|
import sys
|
||
|
from sys import stdout
|
||
|
from os.path import join, dirname, realpath, exists, isdir, basename
|
||
|
from os import listdir, unlink, makedirs, environ
|
||
|
import zipfile
|
||
|
import tarfile
|
||
|
import importlib
|
||
|
import sh
|
||
|
import shutil
|
||
|
try:
|
||
|
from urllib.request import FancyURLopener
|
||
|
except ImportError:
|
||
|
from urllib import FancyURLopener
|
||
|
|
||
|
|
||
|
def shprint(command, *args, **kwargs):
|
||
|
kwargs["_iter"] = True
|
||
|
kwargs["_out_bufsize"] = 1
|
||
|
#kwargs["_err_to_out"] = True
|
||
|
for line in command(*args, **kwargs):
|
||
|
stdout.write(line)
|
||
|
|
||
|
|
||
|
class ChromeDownloader(FancyURLopener):
|
||
|
version = (
|
||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
||
|
'(KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36')
|
||
|
|
||
|
urlretrieve = ChromeDownloader().retrieve
|
||
|
|
||
|
|
||
|
class Context(object):
|
||
|
env = environ.copy()
|
||
|
root_dir = None
|
||
|
cache_dir = None
|
||
|
build_dir = None
|
||
|
dist_dir = None
|
||
|
install_dir = None
|
||
|
ccache = None
|
||
|
cython = None
|
||
|
sdkver = None
|
||
|
sdksimver = None
|
||
|
|
||
|
def __init__(self):
|
||
|
super(Context, self).__init__()
|
||
|
ok = True
|
||
|
|
||
|
sdks = sh.xcodebuild("-showsdks").splitlines()
|
||
|
|
||
|
# get the latest iphoneos
|
||
|
iphoneos = [x for x in sdks if "iphoneos" in x]
|
||
|
if not iphoneos:
|
||
|
print("No iphone SDK installed")
|
||
|
ok = False
|
||
|
else:
|
||
|
iphoneos = iphoneos[0].split()[-1]
|
||
|
self.sdkver = iphoneos
|
||
|
|
||
|
# get the latest iphonesimulator version
|
||
|
iphonesim = [x for x in sdks if "iphonesimulator" in x]
|
||
|
if not iphoneos:
|
||
|
ok = False
|
||
|
print("Error: No iphonesimulator SDK installed")
|
||
|
else:
|
||
|
iphonesim = iphonesim[0].split()[-1]
|
||
|
self.sdksimver = iphonesim
|
||
|
|
||
|
# get the path for Developer
|
||
|
self.devroot = "{}/Platforms/iPhoneOS.platform/Developer".format(
|
||
|
sh.xcode_select("-print-path").strip())
|
||
|
|
||
|
# path to the iOS SDK
|
||
|
self.iossdkroot = "{}/SDKs.iPhoneOS{}.sdk".format(
|
||
|
self.devroot, self.sdkver)
|
||
|
|
||
|
# root of the toolchain
|
||
|
self.root_dir = realpath(dirname(__file__))
|
||
|
self.build_dir = "{}/build".format(self.root_dir)
|
||
|
self.cache_dir = "{}/.cache".format(self.root_dir)
|
||
|
self.dist_dir = "{}/dist".format(self.root_dir)
|
||
|
self.install_dir = "{}/dist/root".format(self.root_dir)
|
||
|
self.archs = ("i386", "armv7", "armv7s", "arm64")
|
||
|
|
||
|
# path to some tools
|
||
|
self.ccache = sh.which("ccache")
|
||
|
if not self.ccache:
|
||
|
print("ccache is missing, the build will not be optimized in the future.")
|
||
|
for cython_fn in ("cython-2.7", "cython"):
|
||
|
cython = sh.which(cython_fn)
|
||
|
if cython:
|
||
|
self.cython = cython
|
||
|
break
|
||
|
if not self.cython:
|
||
|
ok = False
|
||
|
print("Missing requirement: cython is not installed")
|
||
|
|
||
|
# check the basic tools
|
||
|
for tool in ("pkg-config", "autoconf", "automake", "libtool", "hg"):
|
||
|
if not sh.which(tool):
|
||
|
print("Missing requirement: {} is not installed".format(
|
||
|
tool))
|
||
|
|
||
|
if not ok:
|
||
|
sys.exit(1)
|
||
|
|
||
|
ensure_dir(self.root_dir)
|
||
|
ensure_dir(self.build_dir)
|
||
|
ensure_dir(self.cache_dir)
|
||
|
ensure_dir(self.dist_dir)
|
||
|
ensure_dir(self.install_dir)
|
||
|
|
||
|
|
||
|
class Recipe(object):
|
||
|
version = None
|
||
|
url = None
|
||
|
|
||
|
# API available for recipes
|
||
|
def download_file(self, url, filename, cwd=None):
|
||
|
"""
|
||
|
Download an `url` to `outfn`
|
||
|
"""
|
||
|
def report_hook(index, blksize, size):
|
||
|
if size <= 0:
|
||
|
progression = '{0} bytes'.format(index * blksize)
|
||
|
else:
|
||
|
progression = '{0:.2f}%'.format(
|
||
|
index * blksize * 100. / float(size))
|
||
|
stdout.write('- Download {}\r'.format(progression))
|
||
|
stdout.flush()
|
||
|
|
||
|
if cwd:
|
||
|
filename = join(cwd, filename)
|
||
|
if exists(filename):
|
||
|
unlink(filename)
|
||
|
|
||
|
print('Downloading {0}'.format(url))
|
||
|
urlretrieve(url, filename, report_hook)
|
||
|
return filename
|
||
|
|
||
|
def extract_file(self, filename, cwd):
|
||
|
"""
|
||
|
Extract the `filename` into the directory `cwd`.
|
||
|
"""
|
||
|
print("Extract {} into {}".format(filename, cwd))
|
||
|
if filename.endswith(".tgz") or filename.endswith(".tar.gz"):
|
||
|
shprint(sh.tar, "-C", cwd, "-xvzf", filename)
|
||
|
|
||
|
elif filename.endswith(".tbz2") or filename.endswith(".tar.bz2"):
|
||
|
shprint(sh.tar, "-C", cwd, "-xvjf", filename)
|
||
|
|
||
|
elif filename.endswith(".zip"):
|
||
|
zf = zipfile.ZipFile(filename)
|
||
|
zf.extractall(path=cwd)
|
||
|
zf.close()
|
||
|
|
||
|
else:
|
||
|
print("Error: cannot extract, unreconized extension for {}".format(
|
||
|
filename))
|
||
|
raise Exception()
|
||
|
|
||
|
def get_archive_rootdir(self, filename):
|
||
|
if filename.endswith(".tgz") or filename.endswith(".tar.gz") or \
|
||
|
filename.endswith(".tbz2") or filename.endswith(".tar.bz2"):
|
||
|
archive = tarfile.open(filename)
|
||
|
root = archive.next().path.split("/")
|
||
|
return root[0]
|
||
|
else:
|
||
|
print("Error: cannot detect root direction")
|
||
|
print("Unrecognized extension for {}".format(filename))
|
||
|
raise Exception()
|
||
|
|
||
|
def apply_patch(self, filename):
|
||
|
"""
|
||
|
Apply a patch from the current recipe directory into the current
|
||
|
build directory.
|
||
|
"""
|
||
|
print("Apply patch {}".format(filename))
|
||
|
filename = join(self.recipe_dir, filename)
|
||
|
sh.patch("-t", "-d", self.build_dir, "-p1", "-i", filename)
|
||
|
|
||
|
def copy_file(self, filename, dest):
|
||
|
filename = join(self.recipe_dir, filename)
|
||
|
dest = join(self.build_dir, dest)
|
||
|
shutil.copy(filename, dest)
|
||
|
|
||
|
def has_marker(self, marker):
|
||
|
"""
|
||
|
Return True if the current build directory has the marker set
|
||
|
"""
|
||
|
return exists(join(self.build_dir, ".{}".format(marker)))
|
||
|
|
||
|
def set_marker(self, marker):
|
||
|
"""
|
||
|
Set a marker info the current build directory
|
||
|
"""
|
||
|
with open(join(self.build_dir, ".{}".format(marker)), "w") as fd:
|
||
|
fd.write("ok")
|
||
|
|
||
|
def marker(self, marker):
|
||
|
"""
|
||
|
Return a context that will be executed only if the marker has been set
|
||
|
"""
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
modname = self.__class__.__module__
|
||
|
return modname.split(".", 1)[-1]
|
||
|
|
||
|
@property
|
||
|
def archive_fn(self):
|
||
|
if hasattr(self, "ext"):
|
||
|
ext = self.ext
|
||
|
else:
|
||
|
ext = basename(self.url).split(".", 1)[-1]
|
||
|
fn = "{}/{}.{}".format(
|
||
|
self.ctx.cache_dir,
|
||
|
self.name, ext)
|
||
|
return fn
|
||
|
|
||
|
# Public Recipe API to be subclassed if needed
|
||
|
|
||
|
def execute(self):
|
||
|
print("Download {}".format(self.name))
|
||
|
self.download()
|
||
|
print("Extract {}".format(self.name))
|
||
|
self.extract()
|
||
|
print("Build {}".format(self.name))
|
||
|
self.build_all()
|
||
|
|
||
|
def download(self):
|
||
|
fn = self.archive_fn
|
||
|
if not exists(fn):
|
||
|
self.download_file(self.url.format(version=self.version), fn)
|
||
|
|
||
|
def extract(self):
|
||
|
# recipe tmp directory
|
||
|
archive_root = self.get_archive_rootdir(self.archive_fn)
|
||
|
for arch in self.ctx.archs:
|
||
|
print("Extract {} for {}".format(self.name, arch))
|
||
|
build_dir = join(self.ctx.build_dir, arch)
|
||
|
if exists(join(build_dir, archive_root)):
|
||
|
continue
|
||
|
ensure_dir(build_dir)
|
||
|
self.extract_file(self.archive_fn, build_dir)
|
||
|
|
||
|
def build_all(self):
|
||
|
archive_root = self.get_archive_rootdir(self.archive_fn)
|
||
|
for arch in self.ctx.archs:
|
||
|
self.build_dir = join(self.ctx.build_dir, arch, archive_root)
|
||
|
self.prebuild_arch(arch)
|
||
|
self.build_arch(arch)
|
||
|
self.postbuild_arch(arch)
|
||
|
|
||
|
def prebuild_arch(self, arch):
|
||
|
print("Prebuild {} for {}".format(self.name, arch))
|
||
|
prebuild = "prebuild_{}".format(arch)
|
||
|
if hasattr(self, prebuild):
|
||
|
getattr(self, prebuild)()
|
||
|
|
||
|
def build_arch(self, arch):
|
||
|
print("Build {} for {}".format(self.name, arch))
|
||
|
build = "build_{}".format(arch)
|
||
|
if hasattr(self, build):
|
||
|
getattr(self, build)()
|
||
|
|
||
|
def postbuild_arch(self, arch):
|
||
|
print("Postbuild {} for {}".format(self.name, arch))
|
||
|
postbuild = "postbuild_{}".format(arch)
|
||
|
if hasattr(self, postbuild):
|
||
|
getattr(self, postbuild)()
|
||
|
|
||
|
|
||
|
def list_recipes():
|
||
|
recipes_dir = join(dirname(__file__), "recipes")
|
||
|
for name in listdir(recipes_dir):
|
||
|
fn = join(recipes_dir, name)
|
||
|
if isdir(fn):
|
||
|
yield name
|
||
|
|
||
|
def compile_recipe(name, ctx):
|
||
|
mod = importlib.import_module("recipes.{}".format(name))
|
||
|
recipe = mod.recipe
|
||
|
recipe.recipe_dir = join(ctx.root_dir, "recipes", name)
|
||
|
recipe.ctx = ctx
|
||
|
recipe.execute()
|
||
|
|
||
|
|
||
|
def ensure_dir(filename):
|
||
|
if not exists(filename):
|
||
|
makedirs(filename)
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
import argparse
|
||
|
parser = argparse.ArgumentParser(
|
||
|
description='Compile Python and others extensions for iOS')
|
||
|
args = parser.parse_args()
|
||
|
ctx = Context()
|
||
|
print list(list_recipes())
|
||
|
compile_recipe("hostpython", ctx)
|