├── requirements.txt ├── example └── example_project │ ├── build │ └── hello_world │ ├── __init__.py │ └── main.py ├── .gitignore ├── LICENSE ├── zapper ├── __init__.py ├── templates │ └── __main__.py.j2 ├── utils.py ├── cli.py └── zapper.py ├── setup.py ├── bin └── zapper └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==2.8 2 | MarkupSafe==0.23 3 | PyYAML==3.11 4 | -------------------------------------------------------------------------------- /example/example_project/build: -------------------------------------------------------------------------------- 1 | zapper: 2 | entry_point: hello_world.main:main 3 | app_name: my_first_zipapp 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | led / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | venv/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask instance folder 58 | instance/ 59 | 60 | # Sphinx documentation 61 | docs/_build/ 62 | 63 | # PyBuilder 64 | target/ 65 | 66 | # IPython Notebook 67 | .ipynb_checkpoints 68 | 69 | # pyenv 70 | .python-version 71 | 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 2 | # 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | # disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /zapper/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 2 | # 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | # disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /example/example_project/hello_world/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 3 | # 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 7 | # following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 10 | # disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 13 | # disclaimer in the documentation and/or other materials provided with the distribution. 14 | # 15 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 16 | # products derived from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 19 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /example/example_project/hello_world/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 3 | # 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 7 | # following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 10 | # disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 13 | # disclaimer in the documentation and/or other materials provided with the distribution. 14 | # 15 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 16 | # products derived from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 19 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | """ 26 | hello_world.py -- Print Hello World to the screen. 27 | """ 28 | 29 | 30 | def main(): 31 | print('Hello, world!') 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 3 | # 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 7 | # following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 10 | # disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 13 | # disclaimer in the documentation and/or other materials provided with the distribution. 14 | # 15 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 16 | # products derived from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 19 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import os 27 | from setuptools import setup 28 | 29 | 30 | def read(fname): 31 | """ 32 | Read a file and return its contents. 33 | 34 | Args: 35 | fname (str): The file to read. 36 | """ 37 | 38 | with open(os.path.join(os.path.dirname(__file__), fname), 'r') as f: 39 | return f.read() 40 | 41 | # Install Zapper! 42 | setup(name='zapper', 43 | version='1.0', 44 | description='Tool to generate ptyhon zipapps.', 45 | long_description=read('README.md'), 46 | author='Corwin Brown', 47 | author_email='corwin.brown@maxpoint.com', 48 | packages=['zapper'], 49 | scripts=['bin/zapper'], 50 | install_requires=['jinja2==2.8', 'pyyaml==3.11', 'markupsafe==0.23'], 51 | platform='all') 52 | -------------------------------------------------------------------------------- /bin/zapper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 3 | # 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 7 | # following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 10 | # disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 13 | # disclaimer in the documentation and/or other materials provided with the distribution. 14 | # 15 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 16 | # products derived from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 19 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | """ 26 | zapper.py -- A tool to build Python Zipapps. 27 | 28 | The zipapp tool is introduced in python 3.5, but this script is an 29 | attempt to provide some of that functionality in 2.6. 30 | 31 | the idea is to read a 'build' file that has an entry point, then 32 | create a '__main__.py' file that calls that entry point. 33 | """ 34 | from __future__ import absolute_import 35 | 36 | import sys 37 | 38 | from zapper.cli import main 39 | from zapper.zapper import ZapperError 40 | from zapper.utils import print_err 41 | 42 | 43 | if __name__ == '__main__': 44 | # Call Zapper and exit 45 | try: 46 | sys.exit(main()) 47 | except KeyboardInterrupt: 48 | print_err('Stopping!') 49 | sys.exit(1) 50 | except (IOError, OSError, ZapperError) as e: 51 | print_err('Error: {0}'.format(e)) 52 | sys.exit(1) 53 | except ValueError as e: 54 | print_err('Config Error: {0}'.format(e)) 55 | sys.exit(1) 56 | -------------------------------------------------------------------------------- /zapper/templates/__main__.py.j2: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 2 | # 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | # disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | """ 25 | __main__.py -- Main entry point for this application. 26 | 27 | This file has been automatically generated by zapper. 28 | """ 29 | 30 | import os 31 | import sys 32 | import site 33 | 34 | 35 | def get_path(fname=None): 36 | """ 37 | Return the absolute path of a file. If fname is None, return 38 | the path to __file__. 39 | 40 | Args: 41 | fname (str): The file to get the absolute path of. 42 | 43 | Returns: 44 | str: The path to 'fname'. 45 | """ 46 | 47 | fname = __file__ if not fname else fname 48 | 49 | return os.path.dirname(os.path.abspath(fname)) 50 | 51 | 52 | def add_vendor_directories(): 53 | """ 54 | Adds any packages installed to a "vendor" directory to the sys.path. 55 | """ 56 | 57 | # Save the state of our current path. 58 | prev_sys_path = list(sys.path) 59 | 60 | # The first item in 'sys.path' is the directory fo the module, 61 | # generally that isn't super useful for zipapps, but it's come 62 | # to my attention that people like having it there. 63 | my_path = prev_sys_path[0] 64 | 65 | # List of paths we want prepended to syspath. 66 | paths_to_add = [ 67 | os.path.join(get_path(__file__), 'vendor') 68 | ] 69 | 70 | # Loop through the pathes to add, check if that directory exists and 71 | # is readable, and if so, add it to our path. 72 | for path in paths_to_add: 73 | if not (os.path.exists(path) or 74 | os.path.isdir(path) or 75 | os.access(path, os.R_OK) or 76 | path): 77 | 78 | continue 79 | 80 | site.addsitedir(path) 81 | 82 | # Percolate new paths to the top. 83 | new_sys_path = [] 84 | for path in list(sys.path): 85 | if path not in prev_sys_path: 86 | new_sys_path.append(path) 87 | sys.path.remove(path) 88 | 89 | # Prepend our re-ordered path. 90 | sys.path[:0] = [my_path] + new_sys_path 91 | 92 | 93 | if __name__ == '__main__': 94 | 95 | add_vendor_directories() 96 | 97 | # Import our entry point. 98 | from {{ module_path }} import {{ entry_point }} 99 | 100 | # Call our application. 101 | {% if parameters is defined and parameters %} 102 | sys.exit({{ entry_point }}({{ parameters }})) 103 | {% else %} 104 | sys.exit({{ entry_point }}()) 105 | {% endif %} 106 | 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zapper 2 | 3 | Script to create Python Zipapps. 4 | 5 | In python 3.5 theres a tool added that leverages an under-advertised feature of python, which is that the interpreter has the ability to read in, and potentially execute a zipfile. Python 3.5 adds zipapp, which simplifies a lot of the process for you. Since most Linux distros are well behind at 2.7, I wrote a stop gap script that seems to work pretty well for me. 6 | 7 | Essentially it: 8 | 9 | 1. Reads a 'build' file that contains some metadata about the project. 10 | 2. Creates a `__main__.py` inside of the project directory that references some entry_point. 11 | 3. Installs any dependencies found in a requirements.txt 12 | 4. Zips up the package. 13 | 5. Write a python shebang into the zipfile. 14 | 6. chmod +x 's it. 15 | 16 | Then boom, you can directly execute that script/application. 17 | 18 | I've included a sample project that can easily be compiled down into a zipapp. 19 | 20 | ## Why? 21 | 22 | This tool was built to fit some requirements I have when deploying code. Deploying python projects is kind of a pain. You have to copy over some flat file structure (or a zip, and unpack it somewhere), then you have to install requirements, preferably in a virtualenv, and some of those requirements will require compile headers... 23 | 24 | It's just kind of a pain. As much as I love Python, I really wanted something like a jar. Pex is a great solution, but is kind of clumsy outside of pants. Pyz is a great solution, but required apps to be formatted a certain way, and would require a lot of education for other teams. 25 | 26 | Enter Zapper. Zapper is something I can easily deploy as a Jenkins build job, and only really needs to know about where to find your main entry point. Assuming you've formatted the project like a sane python project, it should just kind of work. 27 | 28 | ## Usage 29 | 30 | ```bash 31 | zapper SRC_PATH [DEST_PATH] 32 | ``` 33 | 34 | For zapper to work, the source path you point it at must have a yaml formatted file called build. Generally, it will look a little like: 35 | 36 | ```yaml 37 | zapper: 38 | entry_point: zapper.cli:main 39 | ``` 40 | 41 | The reason I require a parent key for Zapper is that this tool was built for work, and our build system will have other things that read this same file. 42 | 43 | Here's a list of all the current supported options: 44 | 45 | ```yaml 46 | zapper: 47 | entry_point: module.path:entry_point comma,separated,parameters 48 | app_name: over_write_default_app_name 49 | python_shebang: #!/usr/bin/env/python # Defaults to "#!/usr/bin/env python" 50 | requirements: 51 | - list 52 | - of 53 | - requirements 54 | requirements_txt: path/to/requirements.txt 55 | ignore: 56 | - list 57 | - of 58 | - files 59 | - to 60 | - ignore 61 | clean_pyc: True 62 | ``` 63 | 64 | If you have an application that you want to generate multiple zipapps from, you can supply a yaml list instead: 65 | 66 | ```yaml 67 | zapper: 68 | - entry_point: my.app.cli:main 69 | app_name: main_app 70 | ignore: 71 | - utils 72 | 73 | - entry_point: my.app.cli:secondary 74 | app_name: second_app 75 | 76 | - entry_point: my.app.util:ternary someParameter 77 | app_name: utility_app 78 | requirements: 79 | - requests 80 | - jinj2 81 | ``` 82 | 83 | Any specified requirements will be loaded into a 'vendor' directory inside of the package. The generated `__main__.py` will then add that directory to the sys.path at run time. Note that you will still require a system level interpreter, and you aren't ENTIRELY isolated from system level packages, but that was outside of the scope of this project. 84 | 85 | ## Assumptions 86 | 87 | Zapper makes a few assumptions about both your project and your environment. 88 | 89 | * Pip must be installed. 90 | - Zapper uses Pip to install any dependencies, without it, zapper will complain. 91 | * Zapper assumes you have a 'requirements.txt' file within your project. If you do not, no dependencies will be installed. 92 | - A 'requirements.txt' file is a file included in your project that lists out all the lirbraries (and the version of that library) that your application is dependent upon. This is generally a fairly common practice in Python. Read more about it [here](https://pip.readthedocs.org/en/1.1/requirements.html). 93 | 94 | ## Build Example Project 95 | 96 | There's a very basic example "Hello, World" project inside the "example" directory. If you want to create a zipapp from that project, just run: 97 | 98 | ```bash 99 | zipapp example/example_project example -v 100 | ``` 101 | 102 | 103 | # License 104 | 105 | Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 106 | 107 | All rights reserved. 108 | 109 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 110 | following conditions are met: 111 | 112 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 113 | disclaimer. 114 | 115 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 116 | disclaimer in the documentation and/or other materials provided with the distribution. 117 | 118 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 119 | products derived from this software without specific prior written permission. 120 | 121 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 122 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 123 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 124 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 125 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 126 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 127 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 128 | -------------------------------------------------------------------------------- /zapper/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 2 | # 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | # disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | """ 25 | utils.py -- Conveincence functions for zapper. 26 | """ 27 | from __future__ import absolute_import, print_function 28 | 29 | import os 30 | import sys 31 | import subprocess 32 | 33 | from jinja2 import Environment, PackageLoader, TemplateNotFound, UndefinedError 34 | 35 | 36 | def get_file_path(fname): 37 | """ 38 | Return the path of a provided file. 39 | 40 | Args: 41 | fname (str): File to get path to. 42 | 43 | Returns: 44 | str 45 | """ 46 | 47 | return os.path.dirname(os.path.realpath(fname)) 48 | 49 | 50 | def file_exists(fpath): 51 | """ 52 | Check if a file exists and is readable. 53 | 54 | Args: 55 | fpath (str): The file path to check. 56 | """ 57 | 58 | if fpath is None: 59 | return False 60 | 61 | if os.path.exists(fpath) and os.access(fpath, os.R_OK): 62 | return True 63 | 64 | return False 65 | 66 | 67 | def list_files(path): 68 | """ 69 | Generator to Emulate something like: 70 | find ./ -f 71 | 72 | This will blow apart a directory tree and allow me to easily 73 | search for a file. Since this is a generator, it should 74 | be mitigate a lot of the ineffiency of walking the whold 75 | directory. 76 | 77 | Args: 78 | path (str): The path to traverse. 79 | 80 | Returns: 81 | str: files in the path. 82 | """ 83 | 84 | for root, folders, files in os.walk(path): 85 | for filename in folders + files: 86 | yield os.path.join(root, filename) 87 | 88 | 89 | def render_template(template_name, **kwargs): 90 | """ 91 | Simple utility function to render out a specified template, using 92 | **kwargs to fill in variables. 93 | 94 | Args: 95 | template_path (str): The directory where we can find the template. 96 | template_name (str): The actual name of the template we want to 97 | render. 98 | **kwargs (dict): Key Value pairs of any variables we want rendered 99 | out into the template. 100 | 101 | Raises: 102 | AncillaryFileNotFound: If we cannot find the template. 103 | AncillaryUndefinedError: If we run across an undefined variable. 104 | 105 | """ 106 | 107 | # Attempt to load a Tempalte file from within the 'Zapper' package 108 | # and raise an IOError if I'm unable to find it. 109 | try: 110 | env = Environment(loader=PackageLoader('zapper', 'templates')) 111 | template = env.get_template(template_name) 112 | except TemplateNotFound: 113 | raise IOError('Unable to find template {} in zapper!' 114 | .format(template_name)) 115 | 116 | # Attempt to render our template, and raise a Value Error if we 117 | # run into any undefined variables. 118 | try: 119 | template_data = template.render(**kwargs) 120 | except UndefinedError as e: 121 | raise ValueError('Undefined variable found in {}! Error: {}' 122 | .format(template_name, e)) 123 | 124 | return template_data 125 | 126 | 127 | def file_executable(fpath): 128 | """ 129 | Check if a provided file path exists and is executable. 130 | 131 | Args: 132 | fpath (str): The path to the file in question. 133 | """ 134 | 135 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 136 | 137 | 138 | def which(program): 139 | """ 140 | Basically a Python implementation of 'which'. 141 | 142 | Search all PATHs in a users environment and see if a 143 | program of "program_name" exists there. Shutil 144 | has something for this in Python 3.3, but alas, I'm 145 | targeting 2.7+. 146 | 147 | Args: 148 | program (str): The name of the program to search for. 149 | """ 150 | 151 | # If I'm given a fully qualified path, just test that, 152 | # otherwise, search the os's PATH environment variable 153 | program_path, program_name = os.path.split(program) 154 | if program_path: 155 | if file_executable(program): 156 | return program 157 | else: 158 | for path in os.environ['PATH'].split(os.pathsep): 159 | path = path.strip('"') 160 | exe_file = os.path.join(path, program) 161 | if file_executable(exe_file): 162 | return exe_file 163 | 164 | return None 165 | 166 | 167 | def print_err(*args, **kwargs): 168 | """ 169 | Print to stderr 170 | Args: 171 | *args to pass down to print function. 172 | *kwargs to pass down to print function. 173 | """ 174 | print(*args, file=sys.stderr, **kwargs) 175 | -------------------------------------------------------------------------------- /zapper/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 2 | # 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | # disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | """ 25 | zapper.py -- A tool to build Python Zipapps. 26 | 27 | The zipapp tool is introduced in python 3.5, but this script is an 28 | attempt to provide some of that functionality now. 29 | 30 | the idea is to read a 'build' file that has an entry point, then 31 | create a '__main__.py' file that calls that entry point. 32 | """ 33 | from __future__ import absolute_import 34 | 35 | import os 36 | import argparse 37 | 38 | import yaml 39 | 40 | from zapper.zapper import Zapper 41 | from zapper.utils import file_exists 42 | 43 | 44 | def _parse_args(): 45 | """ 46 | Parse Command Line Args. 47 | 48 | Returns: 49 | argparse object. 50 | 51 | Raises: 52 | ValueError if 'src' path is invalid. 53 | """ 54 | 55 | # Set up our argument parser. 56 | parser = argparse.ArgumentParser( 57 | description='A tool to build python zipapps.', 58 | usage='%(prog)s SRC_PATH [DEST_PATH]', 59 | epilog='NOTE: Any options specified on the command line will override ALL defined builds.' 60 | 'This means if you specify "app_name" on the command line, and you have multiple ' 61 | 'builds specied in the "build_file", that "app_name" will be applied to all resulting ' 62 | 'artifacts.' 63 | ) 64 | 65 | parser.add_argument('src_path', 66 | nargs=1, 67 | type=str, 68 | help='Path to the app to build.') 69 | parser.add_argument('dest_path', 70 | nargs='?', 71 | type=str, 72 | help='The desired destination.') 73 | parser.add_argument('-b', '--build-file', 74 | type=argparse.FileType(), 75 | help='Path to a build file.') 76 | parser.add_argument('-n', '--name', 77 | type=str, 78 | help='Specify a name for the application. By default basename is used.') 79 | parser.add_argument('-e', '--entry-point', 80 | type=str, 81 | help='The entry point for the application.') 82 | parser.add_argument('-r', '--requirements', 83 | type=str, 84 | help='Comma separated .ist of requirements. ' 85 | 'Prepend with an "@" sign to specify a requirements file') 86 | parser.add_argument('--ignore', 87 | type=str, 88 | help='Comma separated list of files/directories to ignore.') 89 | parser.add_argument('--leave-pyc', 90 | action='store_true', 91 | default=False, 92 | help='Toggles NOT cleaning out any pyc files.') 93 | parser.add_argument('--python-shebang', 94 | type=str, 95 | help='Specify a nonstandard python-shebang.') 96 | parser.add_argument('-v', '--verbose', 97 | action='store_true', 98 | default=False, 99 | help='Toggle verbosity.') 100 | 101 | args = parser.parse_args() 102 | 103 | # Ensure the provided Source Path exists, and raise a 104 | # ValueError if it does not. 105 | args.src_path = args.src_path[0] 106 | if not file_exists(args.src_path): 107 | raise ValueError('"{0}" does not exist or is not readable!' 108 | .format(args.src_path)) 109 | 110 | return args 111 | 112 | 113 | def _find_build_file(src_path): 114 | """ 115 | Search for a 'build' file. 116 | 117 | Args: 118 | src_path (str): The path to the search. 119 | 120 | Returns: 121 | str containing full path to a build file. 122 | 123 | Raises: 124 | ValueError if we can't find a build file. 125 | """ 126 | 127 | # Possible names for the build file. 'build' and 'build.yml' 128 | # seemed sensible enough. 129 | possible_build_file_names = ['build', 'build.yml', 'build.yaml'] 130 | 131 | # Look for a file named either 'build' or 'build.yml' 132 | build_file = None 133 | for build_file_name in possible_build_file_names: 134 | build_file_path = os.path.join(src_path, build_file_name) 135 | if file_exists(build_file_path): 136 | build_file = build_file_path 137 | break 138 | 139 | # If we get here, and don't have a build file, just return None. 140 | return build_file 141 | 142 | 143 | def _read_build_file(build_file_path): 144 | """ 145 | Read the 'build' file. 146 | 147 | Args: 148 | build_file_path (str): The path to the build file 149 | 150 | Returns: 151 | dict containing contents of 'build' file. 152 | 153 | Raises: 154 | IOError if the provided build file doesn't exist. 155 | ValueError if build file does not contain 'zapper' key. 156 | """ 157 | 158 | # Check if we're given a file handle. If we are, just read it. 159 | # otherwise, do all our normal file operations. 160 | if isinstance(build_file_path, file): 161 | build_data = yaml.load(build_file_path.read()) 162 | 163 | else: 164 | if not file_exists(build_file_path): 165 | return {} 166 | 167 | # Read a build file located in the source path. 168 | with open(build_file_path, 'r') as f: 169 | build_data = yaml.load(f.read()) 170 | 171 | # We require that the build file have a zapper "root" key, 172 | # so we'll raise a ValueError if it does not. 173 | if 'zapper' not in build_data: 174 | raise ValueError('"{0}" does not contain a "zapper" key!'.format(build_file_path)) 175 | 176 | return build_data['zapper'] 177 | 178 | 179 | def _parse_options_from_cmd_args(args): 180 | """ 181 | Parse out build options from the command line. 182 | 183 | Args: 184 | args (Argparse obj): The output of 'ArgumentParser.parse_args()' 185 | 186 | Returns: 187 | dict of options. 188 | """ 189 | 190 | opts = {} 191 | if args.name is not None: 192 | opts['app_name'] = args.name 193 | 194 | if args.entry_point is not None: 195 | opts['entry_point'] = args.entry_point 196 | 197 | if args.requirements is not None: 198 | # Check if we're given a file. 199 | if args.requirements.startswith('@'): 200 | opts['requirements_txt'] = args.requirements 201 | else: 202 | # Try and break it apart. 203 | opts['requirements'] = args.requirements.split(',') 204 | 205 | if args.ignore is not None: 206 | opts['ignore'] = args.ignore.split(',') 207 | 208 | if args.leave_pyc: 209 | opts['clean_pyc'] = False 210 | 211 | if args.python_shebang: 212 | opts['python_shebang'] = args.python_shebang 213 | 214 | return opts 215 | 216 | 217 | def _zap(src, dest, opts, verbose): 218 | """ 219 | Run Zapper! 220 | 221 | Args: 222 | opts (dict): Zapper opts 223 | """ 224 | 225 | zapper = Zapper(src_directory=src, 226 | dest=dest, 227 | python_shebang=opts.get('python_shebang'), 228 | entry_point=opts.get('entry_point'), 229 | app_name=opts.get('app_name'), 230 | requirements=opts.get('requirements'), 231 | requirements_txt=opts.get('requirements_txt'), 232 | ignore=opts.get('ignore'), 233 | clean_pyc=opts.get('clean_pyc'), 234 | debug=verbose) 235 | return zapper 236 | 237 | 238 | def main(): 239 | """ 240 | Main 241 | """ 242 | 243 | # Gather up our arguments and options 244 | args = _parse_args() 245 | 246 | # Parse out our CLI options. 247 | cli_opts = _parse_options_from_cmd_args(args) 248 | 249 | # Get our config file. 250 | zapper_opts = {} 251 | if args.build_file: 252 | zapper_opts = _read_build_file(args.build_file) 253 | else: 254 | build_file_path = _find_build_file(args.src_path) 255 | zapper_opts = _read_build_file(build_file_path) 256 | 257 | # If we have multiple entries in the 'zapper' portion of 258 | # the build config, loop through them all. Otherwise 259 | # just run once. 260 | if isinstance(zapper_opts, list): 261 | for instance_opts in zapper_opts: 262 | instance_opts.update(cli_opts) 263 | zapper = _zap(args.src_path, args.dest_path, instance_opts, args.verbose) 264 | else: 265 | zapper_opts.update(cli_opts) 266 | zapper = _zap(args.src_path, args.dest_path, zapper_opts, args.verbose) 267 | 268 | zapper.build() 269 | -------------------------------------------------------------------------------- /zapper/zapper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2016 MaxPoint Interactive, Inc. 2 | # 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | # disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | """ 25 | zapper.py -- Class for main zapper code. 26 | 27 | The idea is to read a 'build' file that has an entry point, then 28 | create a '__main__.py' file that calls that entry point. 29 | """ 30 | from __future__ import absolute_import 31 | 32 | import os 33 | import zipfile 34 | import subprocess 35 | from shutil import rmtree 36 | 37 | from zapper.utils import render_template, file_exists, list_files, which 38 | 39 | # Check if the user has zlib installed 40 | try: 41 | import zlib 42 | has_zlib = True 43 | except ImportError: 44 | has_zlib = False 45 | 46 | 47 | class ZapperError(Exception): 48 | pass 49 | 50 | 51 | class Zapper(object): 52 | """ 53 | Create a Python ZipApp 54 | """ 55 | 56 | # The Standard Python Shebang header. 57 | default_python_shebang = '#!/usr/bin/env python' 58 | 59 | # List to keep track of any files we create. 60 | files_created = [] 61 | 62 | def __init__(self, 63 | src_directory, 64 | entry_point, 65 | python_shebang=None, 66 | dest=None, 67 | app_name=None, 68 | requirements=None, 69 | requirements_txt=None, 70 | ignore=None, 71 | clean_pyc=True, 72 | debug=False): 73 | """ 74 | Constructor 75 | 76 | Args: 77 | src_directory (str): The path to the artifact. 78 | entry_point (str): The entrypoint for the zipapp. 79 | app_name (str): Override default app name. 80 | requirements (list): Requirements for the package. 81 | requirements_txt (str): Path to a requirements.txt file. 82 | ignore (list): Files to ignore. 83 | clean_pyc (bool): If True, clean out *.pyc files before 84 | creating zipapp. 85 | debug (bool): Debug mode. 86 | 87 | Raises: 88 | ValueError when unable to resolve 'app_name'. 89 | """ 90 | 91 | self.python_shebang = python_shebang or self.default_python_shebang 92 | self.src_directory = src_directory 93 | self.dest = dest 94 | self.entry_point = entry_point 95 | self.requirements = requirements 96 | self.debug = debug 97 | self.dest = dest or os.path.dirname(self.src_directory) 98 | self.app_name = app_name or self._deduce_app_name() 99 | self.ignore = ignore or ['venv', 'env'] # Standard generic virtualenv names. 100 | self.clean_pyc = clean_pyc 101 | self.vendor_path = os.path.join(src_directory, 'vendor') 102 | 103 | # If we're given a path to a requirements.txt file, ensure we have 104 | # an absolute path. 105 | # If a path is not provided, we'll assume it exists in the 106 | # src_directory, and verify that later. 107 | if requirements_txt: 108 | if not os.path.isabs(requirements_txt): 109 | self.requirements_txt = os.path.join(self.src_directory, requirements_txt) 110 | else: 111 | self.requirements_txt = requirements_txt 112 | else: 113 | self.requirements_txt = os.path.join(self.src_directory, 'requirements.txt') 114 | 115 | self._debug('requirements_txt set to: "{0}"'.format(self.requirements_txt)) 116 | self._debug('ignore set to: "{0}"'.format(self.ignore)) 117 | self._debug('dest set to: "{0}"'.format(self.dest)) 118 | self._debug('app name set to: "{0}"'.format(self.app_name)) 119 | self.dest_path = os.path.join(self.dest, self.app_name) 120 | 121 | def __del__(self): 122 | """ 123 | Destructor 124 | """ 125 | 126 | self._clean() 127 | 128 | def _debug(self, msg): 129 | """ 130 | Check if debug is toggled, and if so, print a message. 131 | 132 | Args: 133 | msg (str): The message to display. 134 | """ 135 | 136 | if self.debug: 137 | print(msg) 138 | 139 | def _deduce_app_name(self): 140 | """ 141 | If an app name is not provided, try and figure one out. 142 | """ 143 | 144 | # Check and see if we can use the destination as a valid 145 | # name. 146 | use_dest = True 147 | if file_exists(self.dest) and os.path.isdir(self.dest): 148 | use_dest = False 149 | elif file_exists(self.dest) and os.path.isfile(self.dest): 150 | # Confirm overwrite 151 | use_dest = True 152 | 153 | if use_dest: 154 | app_name = os.path.basename(self.dest) 155 | self.dest = os.path.dirname(self.dest) 156 | else: 157 | if self.src_directory.endswith('/'): 158 | tmp_src_directory = self.src_directory[:-1] 159 | else: 160 | tmp_src_directory = self.src_directory 161 | 162 | app_name = '{0}.pyz'.format(os.path.basename(tmp_src_directory)) 163 | 164 | if not app_name: 165 | raise ZapperError('Unable to deduce app name!') 166 | 167 | return app_name 168 | 169 | def _prepend_shebang(self): 170 | """ 171 | Open a file and write a python shebang as the first line. 172 | """ 173 | 174 | self._debug('Prepending python shebang "{0}" to "{1}"' 175 | .format(self.python_shebang, self.dest_path)) 176 | 177 | # Read in the file, rewind to the top, write our header line, then 178 | # write all the contents back. 179 | with open(self.dest_path, 'r+') as f: 180 | file_content = f.read() 181 | f.seek(0, 0) 182 | f.write('{0}\n{1}'.format(self.python_shebang, file_content)) 183 | 184 | def _create_main(self): 185 | """ 186 | Template out and write a __main__ file. 187 | 188 | Template out and write a '__main__.py' file that calls the 189 | entry point specified in the 'build' file like so: 190 | 191 | sys.exit(entry_point(parameters)) 192 | 193 | Raises: 194 | ValueError if the supplied entry point isn't formatted 195 | as: "module_name:main_function params" 196 | """ 197 | 198 | self._debug('Creating __main__.py') 199 | 200 | # Attempt to parse any paremeters out of the entry point. 201 | parameters = None 202 | try: 203 | self.entry_point, parameters = self.entry_point.split() 204 | except ValueError: 205 | pass 206 | 207 | self._debug('Parameters Found: "{0}"' 208 | .format(parameters if parameters else 'None')) 209 | 210 | # Attempt to parse out the module path and entry_point from 211 | # self.entry_point. 212 | try: 213 | module_path, entry_point = self.entry_point.split(':') 214 | except ValueError: 215 | raise ZapperError('"{0}" is a malformed entry point! ' 216 | 'It should be formatted like: ' 217 | '"module_name:main_function"' 218 | .format(self.entry_point)) 219 | 220 | self._debug('Module Path: "{0}"\nEntry Point: "{1}"' 221 | .format(module_path, entry_point)) 222 | 223 | # Path to place our '__main__.py' file. 224 | main_path = os.path.join(self.src_directory, '__main__.py') 225 | 226 | # Write out our templated __main__.py file. 227 | self._debug('Writing "{0}"'.format(main_path)) 228 | with open(main_path, 'w') as f: 229 | f.write(render_template('__main__.py.j2', 230 | module_path=module_path, 231 | entry_point=entry_point, 232 | parameters=parameters)) 233 | 234 | # Note that we created a file at 'main_path'. 235 | self.files_created.append(main_path) 236 | 237 | def _install_requirements(self): 238 | """ 239 | Install specified dependencies to a 'vendor' directory. 240 | 241 | If requirements are defined in the build file, install those to 242 | a 'vendor' directory. If they are not defined, read the 243 | 'requirements.txt' file (if it exists) and install those to 244 | a 'vendor' directory. 245 | 246 | Raises: 247 | OSError if Pip is not installed. 248 | """ 249 | 250 | # On windows, pip is 'pip.exe', but everywhere else its pip. 251 | if os.name == 'posix': 252 | pip_name = 'pip' 253 | else: 254 | pip_name = 'pip.exe' 255 | 256 | # Test if Pip is installed and bomb if it's not. 257 | pip_cmd = which(pip_name) 258 | if not pip_cmd: 259 | raise ZapperError('Required program "pip" not installed!') 260 | 261 | # Check if our vendor path exists, and if it doesn't, 262 | # create it. This way if a project already has one, 263 | # I don't step on too many toes. 264 | self._debug('Installing Dependencies.') 265 | if not file_exists(self.vendor_path): 266 | self._debug('Creating "{0}"'.format(self.vendor_path)) 267 | os.makedirs(self.vendor_path) 268 | self.files_created.append(self.vendor_path) 269 | 270 | # Loop through provided requirements and install 271 | # run 'pip install' 272 | if self.requirements: 273 | self._debug('Requrements List provided.') 274 | for requirement in self.requirements: 275 | cmd = [ 276 | pip_cmd, 277 | 'install', 278 | '{0}'.format(requirement), 279 | '--target={0}'.format(self.vendor_path), 280 | ] 281 | 282 | self._debug('Installing "{0}"" with command "{1}"' 283 | .format(requirement, self.vendor_path)) 284 | 285 | try: 286 | output = subprocess.check_output(cmd) 287 | except subprocess.CalledProcessError as e: 288 | raise ZapperError( 289 | 'Failed while installing dependencies! Error: "{0}"' 290 | .format(e)) 291 | self._debug('{0}'.format(output)) 292 | 293 | # If a requirements.txt file is provided, feed it to 'pip' 294 | # and install everything to the vendor directory. 295 | if self.requirements_txt: 296 | if not file_exists(self.requirements_txt): 297 | self._debug('"requirements.txt" not found at: "{0}"' 298 | .format(self.requirements_txt)) 299 | return 300 | 301 | cmd = [ 302 | 'pip', 303 | 'install', 304 | '-r', 305 | '{0}'.format(self.requirements_txt), 306 | '--target={0}'.format(self.vendor_path), 307 | ] 308 | 309 | self._debug('Running command: "{0}"'.format(cmd)) 310 | 311 | try: 312 | output = subprocess.check_output(cmd) 313 | except subprocess.CalledProcessError as e: 314 | raise ZapperError( 315 | 'Failed while installing dependencies! Error: "{0}"' 316 | .format(e)) 317 | self._debug('{0}'.format(output)) 318 | 319 | def _ignored(self, fpath): 320 | """ 321 | Check if a file or path is in the ignore list. 322 | 323 | Args: 324 | fpath (str): Filepath or file name 325 | """ 326 | 327 | for ignore_file in self.ignore: 328 | # Check for a perfect match 329 | if fpath in ignore_file: 330 | return True 331 | 332 | # Check if a relative path matches 333 | rel_path = os.path.relpath(fpath, self.src_directory) 334 | if rel_path in ignore_file: 335 | return True 336 | 337 | # Check if the filename/basename matches. 338 | fname = os.path.basename(fpath) 339 | if fname in ignore_file: 340 | return True 341 | 342 | # Check if we're in an ignored directory 343 | try: 344 | path_parts = rel_path.split('/') 345 | except: 346 | continue 347 | for part in path_parts: 348 | if part == ignore_file: 349 | return True 350 | 351 | return False 352 | 353 | def _zip_directory(self): 354 | """ 355 | Recursively zip a diorectory. 356 | """ 357 | 358 | # Check if we have zlib installed -- which allows us to 359 | # compress the resulting zip file. Otherwise just 360 | # store it. 361 | if has_zlib: 362 | zmode = zipfile.ZIP_DEFLATED 363 | self._debug('Using ZIP_DEFLATED') 364 | else: 365 | zmode = zipfile.ZIP_STORED 366 | self._debug('Using ZIP_STORED') 367 | 368 | with zipfile.ZipFile(self.dest_path, 'w', zmode) as z: 369 | for f in list_files(self.src_directory): 370 | # Check ignore 371 | if self._ignored(f): 372 | self._debug('Ignoring file "{0}"'.format(f)) 373 | continue 374 | 375 | rel_path = os.path.relpath(f, self.src_directory) 376 | 377 | self._debug('Writing "{0}" to zip archive.'.format(rel_path)) 378 | 379 | z.write(f, rel_path) 380 | 381 | def _clean(self): 382 | """ 383 | Clean up after myself. 384 | """ 385 | 386 | self._debug('Cleaning up') 387 | 388 | # Loop through all files we've created, and attempt 389 | # to remove them. 390 | for fpath in self.files_created: 391 | if not file_exists(fpath): 392 | continue 393 | 394 | self._debug('Removing "{0}"'.format(fpath)) 395 | if os.path.isdir(fpath): 396 | rmtree(fpath) 397 | else: 398 | os.remove(fpath) 399 | 400 | def _clean_pyc(self): 401 | """ 402 | Remove all files ending in '.pyc'. 403 | """ 404 | 405 | self._debug('Cleaning out ".pyc" files') 406 | 407 | # Search for all files that end with the extension '.pyc' 408 | # and remove them. 409 | for f in list_files(self.src_directory): 410 | if f.endswith('.pyc'): 411 | self._debug('Removing "{0}"'.format(f)) 412 | os.remove(f) 413 | 414 | def build(self): 415 | """ 416 | Build a zipapp. 417 | """ 418 | 419 | # Clean out any pyc's we have. 420 | if self.clean_pyc: 421 | self._clean_pyc() 422 | 423 | # Create __main__.py. 424 | self._create_main() 425 | 426 | # Install Dependencies. 427 | self._install_requirements() 428 | 429 | # Zip up package. 430 | self._zip_directory() 431 | 432 | # write header into file. 433 | self._prepend_shebang() 434 | 435 | # enable execute flag on resulting artifact. 436 | self._debug('Setting execute flag on "{0}"'.format(self.dest_path)) 437 | os.chmod(self.dest_path, 0755) 438 | --------------------------------------------------------------------------------