diff --git a/README.rst b/README.rst index 12d76d4..cea324e 100644 --- a/README.rst +++ b/README.rst @@ -15,8 +15,6 @@ The toolchain supports: These recipes are not ported to the new toolchain yet: -- openssl -- openssl-link - lxml @@ -56,6 +54,7 @@ You can list the available recipes and their versions with:: ios master kivy ios-poly-arch libffi 3.2.1 + openssl 1.0.2e pyobjus master python 2.7.1 sdl2 iOS-improvements @@ -67,6 +66,14 @@ Then, start the compilation with:: $ ./toolchain.py build kivy +You can build recipes at the same time by adding them as parameters:: + + $ ./toolchain.py build openssl kivy + +Recipe builds can be removed via the clean command e.g.:: + + $ ./toolchain.py clean openssl + The Kivy recipe depends on several others, like the sdl* and python recipes. These may in turn depend on others e.g. sdl2_ttf depends on freetype, etc. You can think of it as follows: the kivy recipe will compile everything @@ -76,12 +83,16 @@ Don't grab a coffee, just do diner. Compiling all the libraries for the first time, 4x over (remember, 4 archs, 2 per platforms), will take time. (TODO: provide a way to not compile for the simulator.). +For a complete list of available commands, type:: + + $ ./toolchain.py + Create the Xcode project ------------------------ The `toolchain.py` can create the initial Xcode project for you:: - $ # ./toolchain.py create <app_directory> + $ ./toolchain.py create <title> <app_directory> $ ./toolchain.py create Touchtracer ~/code/kivy/examples/demo/touchtracer Your app directory must contain a main.py. A directory named `<title>-ios` @@ -92,17 +103,34 @@ You can open the Xcode project using:: Then click on `Play`, and enjoy. -.. notes:: +.. note:: Everytime you press `Play`, your application directory will be synced to the `<title>-ios/YourApp` directory. Don't make changes in the -ios directory directly. +Configuring your App +-------------------- + +When you first build your XCode project, a 'main.m' file is created in your +XCode project folder. This file configures your environment variables and +controls your application startup. You can edit this file to customize your +launch environment. + +.. note:: + + If you wish to restrict your apps orientation, you should do this via + the 'export_orientation' function in 'main.m'. The XCode orientation + settings should be set to support all. + FAQ --- Fatal error: "stdio.h" file not found You need to install the Command line tools: `xcode-select --install` + +You must build with bitcode disabled (Xcode setting ENABLE_BITCODE should be No). + We don't support bitcode. You need to go to the project setting, and disable bitcode. Support ------- diff --git a/recipes/distribute/__init__.py b/recipes/distribute/__init__.py new file mode 100644 index 0000000..fc82a11 --- /dev/null +++ b/recipes/distribute/__init__.py @@ -0,0 +1,21 @@ +from toolchain import PythonRecipe, shprint +from os.path import join +import sh, os + +class DistributeRecipe(PythonRecipe): + version = "0.7.3" + # url = "https://github.com/mitsuhiko/click/archive/{version}.zip" + url = "https://pypi.python.org/packages/source/d/distribute/distribute-{version}.zip" + depends = ["python"] + + def install(self): + arch = list(self.filtered_archs)[0] + build_dir = self.get_build_dir(arch.arch) + os.chdir(build_dir) + hostpython = sh.Command(self.ctx.hostpython) + build_env = arch.get_env() + dest_dir = join(self.ctx.dist_dir, "root", "python") + build_env['PYTHONPATH'] = join(dest_dir, 'lib', 'python2.7', 'site-packages') + shprint(hostpython, "setup.py", "install", "--prefix", dest_dir, _env=build_env) + +recipe = DistributeRecipe() diff --git a/recipes/ffpyplayer/__init__.py b/recipes/ffpyplayer/__init__.py index 8dd2129..eae0580 100644 --- a/recipes/ffpyplayer/__init__.py +++ b/recipes/ffpyplayer/__init__.py @@ -4,7 +4,7 @@ import sh class FFPyplayerRecipe(CythonRecipe): - version = "master" + version = "v3.2" url = "https://github.com/matham/ffpyplayer/archive/{version}.zip" library = "libffpyplayer.a" depends = ["python", "ffmpeg"] diff --git a/recipes/host_setuptools/__init__.py b/recipes/host_setuptools/__init__.py index 95e6ff9..ccd0ffe 100644 --- a/recipes/host_setuptools/__init__.py +++ b/recipes/host_setuptools/__init__.py @@ -8,14 +8,26 @@ import shutil class HostSetuptools(Recipe): depends = ["hostpython"] - archs = 'i386' + archs = ["x86_64"] url = "" def prebuild_arch(self, arch): hostpython = sh.Command(self.ctx.hostpython) sh.curl("-O", "https://bootstrap.pypa.io/ez_setup.py") - shprint(hostpython, "./ez_setup.py") + dest_dir = join(self.ctx.dist_dir, "root", "python") + build_env = arch.get_env() + build_env['PYTHONPATH'] = join(dest_dir, 'lib', 'python2.7', 'site-packages') + # shprint(hostpython, "./ez_setup.py", "--to-dir", dest_dir) + shprint(hostpython, "./ez_setup.py", _env=build_env) + + # def install(self): + # arch = list(self.filtered_archs)[0] + # build_dir = self.get_build_dir(arch.arch) + # os.chdir(build_dir) + # hostpython = sh.Command(self.ctx.hostpython) + # build_env = arch.get_env() + # dest_dir = join(self.ctx.dist_dir, "root", "python") + # build_env['PYTHONPATH'] = join(dest_dir, 'lib', 'python2.7', 'site-packages') + # shprint(hostpython, "setup.py", "install", "--prefix", dest_dir, _env=build_env) recipe = HostSetuptools() - - diff --git a/recipes/hostpython/__init__.py b/recipes/hostpython/__init__.py index b259759..e0e1588 100644 --- a/recipes/hostpython/__init__.py +++ b/recipes/hostpython/__init__.py @@ -48,7 +48,8 @@ class HostpythonRecipe(Recipe): def build_x86_64(self): sdk_path = sh.xcrun("--sdk", "macosx", "--show-sdk-path").strip() build_env = self.ctx.env.copy() - build_env["CC"] = "clang -Qunused-arguments -fcolor-diagnostics" + ccache = (build_env["CCACHE"] + ' ') if 'CCACHE' in build_env else '' + build_env["CC"] = ccache + "clang -Qunused-arguments -fcolor-diagnostics" build_env["LDFLAGS"] = " ".join([ "-lsqlite3", "-lffi", @@ -64,9 +65,9 @@ class HostpythonRecipe(Recipe): "--disable-toolbox-glue", "--without-gcc", _env=build_env) - shprint(sh.make, "-C", self.build_dir, "-j4", "python.exe", "Parser/pgen", + shprint(sh.make, "-C", self.build_dir, "-j4", "python", "Parser/pgen", _env=build_env) - shutil.move("python.exe", "hostpython") + shutil.move("python", "hostpython") shutil.move("Parser/pgen", "Parser/hostpgen") def install(self): @@ -74,6 +75,11 @@ class HostpythonRecipe(Recipe): build_env = arch.get_env() build_dir = self.get_build_dir(arch.arch) build_env["PATH"] = os.environ["PATH"] + # Compiling sometimes looks for Python-ast.py in the 'Python' i.s.o. + # the 'hostpython' folder. Create a symlink to fix. See issue #201 + shprint(sh.ln, "-s", + join(build_dir, "hostpython"), + join(build_dir, "Python")) shprint(sh.make, "-C", build_dir, "bininstall", "inclinstall", diff --git a/recipes/ios/src/ios.pyx b/recipes/ios/src/ios.pyx index 4583320..05e9831 100644 --- a/recipes/ios/src/ios.pyx +++ b/recipes/ios/src/ios.pyx @@ -171,3 +171,43 @@ def get_dpi(): '''Return the approximate DPI of the screen ''' return ios_uiscreen_get_dpi() + + +from pyobjus import autoclass, selector, protocol +from pyobjus.protocols import protocols + +NSNotificationCenter = autoclass('NSNotificationCenter') + +protocols["KeyboardDelegates"] = { + 'keyboardWillShow': ('v16@0:4@8', "v32@0:8@16"), + 'keyboardDidHide': ('v16@0:4@8', "v32@0:8@16")} + + +class IOSKeyboard(object): + '''Get listener for keyboard height. + ''' + + kheight = 0 + + def __init__(self, **kwargs): + super(IOSKeyboard, self).__init__() + NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, selector("keyboardWillShow"), "UIKeyboardWillShowNotification", None) + NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, selector("keyboardDidHide"), "UIKeyboardDidHideNotification", None) + + @protocol('KeyboardDelegates') + def keyboardWillShow(self, notification): + self.kheight = get_scale() * notification.userInfo().objectForKey_( + 'UIKeyboardFrameEndUserInfoKey').CGRectValue().size.height + from kivy.core.window import Window + Window.trigger_keyboard_height() + + @protocol('KeyboardDelegates') + def keyboardDidHide(self, notification): + self.kheight = 0 + from kivy.core.window import Window + Window.trigger_keyboard_height() + +iOSKeyboard = IOSKeyboard() + +def get_kheight(): + return iOSKeyboard.kheight diff --git a/recipes/kivy/__init__.py b/recipes/kivy/__init__.py index 248899a..c941f26 100644 --- a/recipes/kivy/__init__.py +++ b/recipes/kivy/__init__.py @@ -3,7 +3,7 @@ from os.path import join class KivyRecipe(CythonRecipe): - version = "1.9.0" + version = "1.9.1" url = "https://github.com/kivy/kivy/archive/{version}.zip" library = "libkivy.a" depends = ["python", "sdl2", "sdl2_image", "sdl2_mixer", "sdl2_ttf", "ios"] diff --git a/recipes/numpy/__init__.py b/recipes/numpy/__init__.py index a30af2d..3ecdad4 100644 --- a/recipes/numpy/__init__.py +++ b/recipes/numpy/__init__.py @@ -9,6 +9,7 @@ class NumpyRecipe(CythonRecipe): url = "http://pypi.python.org/packages/source/n/numpy/numpy-{version}.tar.gz" library = "libnumpy.a" libraries = ["libnpymath.a", "libnpysort.a"] + include_dir = "numpy/core/include" depends = ["python"] pbx_frameworks = ["Accelerate"] cythonize = False @@ -51,5 +52,3 @@ class NumpyRecipe(CythonRecipe): shutil.rmtree(join(dest_dir, "tests")) recipe = NumpyRecipe() - - diff --git a/recipes/openssl/__init__.py b/recipes/openssl/__init__.py index 07b7cf2..846ae84 100644 --- a/recipes/openssl/__init__.py +++ b/recipes/openssl/__init__.py @@ -10,7 +10,7 @@ arch_mapper = {'i386': 'darwin-i386-cc', class OpensslRecipe(Recipe): - version = "1.0.2d" + version = "1.0.2h" url = "http://www.openssl.org/source/openssl-{version}.tar.gz" libraries = ["libssl.a", "libcrypto.a"] include_dir = "include" diff --git a/recipes/pyyaml/__init__.py b/recipes/pyyaml/__init__.py new file mode 100644 index 0000000..9f06bfa --- /dev/null +++ b/recipes/pyyaml/__init__.py @@ -0,0 +1,21 @@ +# pure-python package, this can be removed when we'll support any python package +import os +import sh +from toolchain import PythonRecipe, shprint + +class PyYamlRecipe(PythonRecipe): + version = "3.11" + url = "https://pypi.python.org/packages/source/P/PyYAML/PyYAML-{version}.tar.gz" + depends = ["python"] + + def install(self): + arch = list(self.filtered_archs)[0] + build_dir = self.get_build_dir(arch.arch) + os.chdir(build_dir) + hostpython = sh.Command(self.ctx.hostpython) + build_env = arch.get_env() + dest_dir = os.path.join(self.ctx.dist_dir, "root", "python") + build_env['PYTHONPATH'] = os.path.join(dest_dir, 'lib', 'python2.7', 'site-packages') + shprint(hostpython, "setup.py", "install", "--prefix", dest_dir, _env=build_env) + +recipe = PyYamlRecipe() diff --git a/recipes/sdl2/__init__.py b/recipes/sdl2/__init__.py index bb016d8..f3040c3 100644 --- a/recipes/sdl2/__init__.py +++ b/recipes/sdl2/__init__.py @@ -19,9 +19,11 @@ class LibSDL2Recipe(Recipe): self.set_marker("patched") def build_arch(self, arch): + env = arch.get_env() shprint(sh.xcodebuild, "ONLY_ACTIVE_ARCH=NO", "ARCHS={}".format(arch.arch), + "CC={}".format(env['CC']), "-sdk", arch.sdk, "-project", "Xcode-iOS/SDL/SDL.xcodeproj", "-target", "libSDL", diff --git a/toolchain.py b/toolchain.py index 58bd2e0..f55a9d8 100755 --- a/toolchain.py +++ b/toolchain.py @@ -122,6 +122,7 @@ class Arch(object): def __init__(self, ctx): super(Arch, self).__init__() self.ctx = ctx + self._ccsh = None def __str__(self): return self.arch @@ -143,7 +144,33 @@ class Arch(object): for d in self.ctx.include_dirs] env = {} - env["CC"] = sh.xcrun("-find", "-sdk", self.sdk, "clang").strip() + ccache = sh.which('ccache') + cc = sh.xcrun("-find", "-sdk", self.sdk, "clang").strip() + if ccache: + ccache = ccache.strip() + use_ccache = environ.get("USE_CCACHE", "1") + if use_ccache != '1': + env["CC"] = cc + else: + if not self._ccsh: + self._ccsh = ccsh = sh.mktemp().strip() + with open(ccsh, 'w') as f: + f.write('#!/bin/sh\n') + f.write(ccache + ' ' + cc + ' "$@"\n') + sh.chmod('+x', ccsh) + else: + ccsh = self._ccsh + env["USE_CCACHE"] = '1' + env["CCACHE"] = ccache + env["CC"] = ccsh + + env.update({k: v for k, v in environ.items() if k.startswith('CCACHE_')}) + env.setdefault('CCACHE_MAXSIZE', '10G') + env.setdefault('CCACHE_HARDLINK', 'true') + env.setdefault('CCACHE_SLOPPINESS', ('file_macro,time_macros,' + 'include_file_mtime,include_file_ctime,file_stat_matches')) + else: + env["CC"] = cc env["AR"] = sh.xcrun("-find", "-sdk", self.sdk, "ar").strip() env["LD"] = sh.xcrun("-find", "-sdk", self.sdk, "ld").strip() env["OTHER_CFLAGS"] = " ".join(include_dirs) @@ -418,6 +445,8 @@ class Recipe(object): print('This is usually caused by a corrupt download. The file' ' will be removed and re-downloaded on the next run.') remove(filename) + return + root = archive.next().path.split("/") return root[0] elif filename.endswith(".zip"): @@ -544,7 +573,7 @@ class Recipe(object): def archive_root(self): key = "{}.archive_root".format(self.name) value = self.ctx.state.get(key) - if not key: + if not value: value = self.get_archive_rootdir(self.archive_fn) self.ctx.state[key] = value return value @@ -781,11 +810,22 @@ class Recipe(object): def get_recipe(cls, name, ctx): if not hasattr(cls, "recipes"): cls.recipes = {} + + if '==' in name: + name, version = name.split('==') + else: + version = None + if name in cls.recipes: - return cls.recipes[name] - mod = importlib.import_module("recipes.{}".format(name)) - recipe = mod.recipe - recipe.recipe_dir = join(ctx.root_dir, "recipes", name) + recipe = cls.recipes[name] + else: + mod = importlib.import_module("recipes.{}".format(name)) + recipe = mod.recipe + recipe.recipe_dir = join(ctx.root_dir, "recipes", name) + + if version: + recipe.version = version + return recipe @@ -795,12 +835,12 @@ class PythonRecipe(Recipe): self.install_python_package() self.reduce_python_package() - def remove_junk(self, d): - exts = ["pyc", "py", "so.lib", "so.o", "sh"] + @staticmethod + def remove_junk(d): + exts = [".pyc", ".py", ".so.lib", ".so.o", ".sh"] for root, dirnames, filenames in walk(d): for fn in filenames: - ext = fn.rsplit(".", 1)[-1] - if ext in exts: + if any([fn.endswith(ext) for ext in exts]): unlink(join(root, fn)) def install_python_package(self, name=None, env=None, is_dir=True): @@ -1020,8 +1060,9 @@ if __name__ == "__main__": usage="""toolchain <command> [<args>] Available commands: - build Build a specific recipe - clean Clean the build + build Build a recipe (compile a library for the required target + architecture) + clean Clean the build of the specified recipe distclean Clean the build and the result recipes List all the available recipes status List all the recipes and their build status @@ -1183,6 +1224,36 @@ Xcode: print("--") print("Project {} updated".format(filename)) + def pip(self): + ctx = Context() + for recipe in Recipe.list_recipes(): + key = "{}.build_all".format(recipe) + if key not in ctx.state: + continue + recipe = Recipe.get_recipe(recipe, ctx) + recipe.init_with_ctx(ctx) + print(ctx.site_packages_dir) + if not hasattr(ctx, "site_packages_dir"): + print("ERROR: python must be compiled before using pip") + sys.exit(1) + + pip_env = { + "CC": "/bin/false", + "CXX": "/bin/false", + "PYTHONPATH": ctx.site_packages_dir, + "PYTHONOPTIMIZE": "2", + "PIP_INSTALL_TARGET": ctx.site_packages_dir + } + print pip_env + pip_path = sh.which("pip") + args = [pip_path] + sys.argv[2:] + if not pip_path: + print("ERROR: pip not found") + sys.exit(1) + import os + print("-- execute pip with: {}".format(args)) + os.execve(pip_path, args, pip_env) + def launchimage(self): import xcassets self._xcassets("LaunchImage", xcassets.launchimage) @@ -1191,6 +1262,21 @@ Xcode: import xcassets self._xcassets("Icon", xcassets.icon) + def xcode(self): + parser = argparse.ArgumentParser(description="Open the xcode project") + parser.add_argument("filename", help="Path to your project or xcodeproj") + args = parser.parse_args(sys.argv[2:]) + filename = args.filename + if not filename.endswith(".xcodeproj"): + # try to find the xcodeproj + from glob import glob + xcodeproj = glob(join(filename, "*.xcodeproj")) + if not xcodeproj: + print("ERROR: Unable to find a xcodeproj in {}".format(filename)) + sys.exit(1) + filename = xcodeproj[0] + sh.open(filename) + def _xcassets(self, title, command): parser = argparse.ArgumentParser( description="Generate {} for your project".format(title)) diff --git a/tools/templates/{{ cookiecutter.project_name }}-ios/main.m b/tools/templates/{{ cookiecutter.project_name }}-ios/main.m index 017a930..99f2325 100644 --- a/tools/templates/{{ cookiecutter.project_name }}-ios/main.m +++ b/tools/templates/{{ cookiecutter.project_name }}-ios/main.m @@ -83,12 +83,25 @@ int main(int argc, char *argv[]) { return ret; } -// This method read available orientations from the Info.plist, and share them -// in an environment variable. Kivy will automatically set the orientation -// according to this environment value, if exist. +// This method reads the available orientations from the Info.plist file and +// shares them via an environment variable. Kivy will automatically set the +// orientation according to this environment value, if it exists. To restrict +// the allowed orientation, please see the comments inside. void export_orientation() { NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; NSArray *orientations = [info objectForKey:@"UISupportedInterfaceOrientations"]; + + // Orientation restrictions + // ======================== + // Comment or uncomment blocks 1-3 in order the limit orientation support + + // 1. Landscape only + // NSString *result = [[NSString alloc] initWithString:@"KIVY_ORIENTATION=LandscapeLeft LandscapeRight"]; + + // 2. Portrait only + // NSString *result = [[NSString alloc] initWithString:@"KIVY_ORIENTATION=Portrait PortraitUpsideDown"]; + + // 3. All orientations NSString *result = [[NSString alloc] initWithString:@"KIVY_ORIENTATION="]; for (int i = 0; i < [orientations count]; i++) { NSString *item = [orientations objectAtIndex:i]; @@ -97,6 +110,7 @@ void export_orientation() { result = [result stringByAppendingString:@" "]; result = [result stringByAppendingString:item]; } + // ======================== putenv((char *)[result UTF8String]); NSLog(@"Available orientation: %@", result);