├── examples ├── PIL │ ├── requirements.txt │ ├── pyproject.toml │ ├── setup.py │ └── hello.py ├── pygame │ ├── requirements.txt │ ├── data │ │ ├── bomb.gif │ │ ├── boom.wav │ │ ├── shot.gif │ │ ├── alien1.gif │ │ ├── alien2.gif │ │ ├── alien3.gif │ │ ├── car_door.wav │ │ ├── house_lo.wav │ │ ├── player1.gif │ │ ├── background.gif │ │ └── explosion1.gif │ ├── English.lproj │ │ ├── aliens.icns │ │ └── MainMenu.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── JavaCompiling.plist │ │ │ ├── classes.nib │ │ │ └── info.nib │ ├── pyproject.toml │ ├── setup.py │ └── aliens_bootstrap.py ├── PySide │ ├── requirements.txt │ ├── pyproject.toml │ ├── setup.py │ └── hello.py ├── PySide6 │ ├── requirements.txt │ ├── pyproject.toml │ ├── setup.py │ └── hello.py ├── PyQt │ ├── hello_app │ │ ├── requirements.txt │ │ ├── pyproject.toml │ │ ├── setup.py │ │ └── hello.py │ └── view_app │ │ ├── requirements.txt │ │ ├── pyproject.toml │ │ ├── setup.py │ │ ├── view.qml │ │ └── main.py ├── PyObjC │ ├── ICSharingWatcher │ │ ├── requirements.txt │ │ ├── pyproject.toml │ │ ├── setup.py │ │ ├── MainMenu.nib │ │ │ ├── classes.nib │ │ │ └── info.nib │ │ ├── ICSharingWatcher.py │ │ ├── leases.py │ │ └── TableModelAppDelegate.py │ └── TinyTinyEdit │ │ ├── requirements.txt │ │ ├── MainMenu.nib │ │ ├── objects.nib │ │ ├── classes.nib │ │ └── info.nib │ │ ├── TinyTinyDocument.nib │ │ ├── objects.nib │ │ ├── classes.nib │ │ └── info.nib │ │ ├── pyproject.toml │ │ ├── setup.py │ │ └── TinyTinyEdit.py ├── multiprocessing │ ├── pyproject.toml │ ├── main.py │ ├── proc.py │ └── setup.py └── Tkinter │ └── hello_tk │ ├── pyproject.toml │ ├── setup.py │ └── hello.py ├── stubs ├── PIL │ └── JpegPresets.pyi ├── modulegraph │ ├── {zipio}.pyi │ ├── util.pyi │ ├── zipio.pyi │ └── find_modules.pyi ├── macholib │ ├── __init__.pyi │ ├── framework.pyi │ ├── dyld.pyi │ ├── MachO.pyi │ ├── mach_o.py │ ├── util.pyi │ ├── MachOStandalone.pyi │ └── MachOGraph.pyi ├── ctypes │ └── macholib │ │ └── dyld.pyi └── README.txt ├── py2app_tests ├── basic_app │ ├── package2 │ │ ├── sub │ │ │ ├── data.dat │ │ │ └── __init__.py │ │ └── __init__.py │ ├── package3 │ │ └── mod.py │ ├── package1 │ │ ├── __init__.py │ │ └── subpackage │ │ │ ├── __init__.py │ │ │ └── module.py │ ├── setup.py │ └── main.py ├── basic_app2 │ ├── package2 │ │ ├── sub │ │ │ ├── data.dat │ │ │ └── __init__.py │ │ └── __init__.py │ ├── package1 │ │ ├── __init__.py │ │ └── subpackage │ │ │ ├── __init__.py │ │ │ └── module.py │ ├── setup.py │ └── main-script ├── __init__.py ├── app_with_data │ ├── data1 │ │ ├── file1.txt │ │ ├── file2.txt │ │ └── file3.sh │ ├── data2 │ │ └── source.c │ ├── data3 │ │ └── source.c │ ├── main.icns │ ├── setup.py │ └── main.py ├── pkg_script_app │ ├── quot │ │ ├── queue.py │ │ └── __init__.py │ ├── setup.py │ └── quot.py ├── plugin_with_scripts │ ├── helper1.py │ ├── helper2.py │ ├── main.py │ └── setup.py ├── basic_app_with_encoding │ ├── package1 │ │ ├── __init__.py │ │ └── subpackage │ │ │ ├── __init__.py │ │ │ └── module.py │ ├── package2 │ │ ├── __init__.py │ │ └── sub │ │ │ ├── __init__.py │ │ │ └── data.dat │ ├── package3 │ │ └── mod.py │ ├── setup.py │ └── main.py ├── basic_app_with_plugin │ ├── plugin.c │ ├── main.py │ └── setup.py ├── app_with_scripts │ ├── helper1.py │ ├── src │ │ ├── libfoo.h │ │ ├── libfoo.c │ │ └── modfoo.c │ ├── subdir │ │ └── helper2.py │ ├── setup.py │ ├── main.py │ └── presetup.py ├── app_with_email │ ├── setup-all.py │ ├── setup-plain.py │ ├── setup-compat.py │ ├── main-plain.py │ ├── main-all.py │ └── main-compat.py ├── resource_compile_app │ ├── setup.py │ └── main.py ├── app_with_sharedlib │ ├── src │ │ ├── sharedlib.h │ │ └── sharedlib.c │ ├── main.py │ └── mod.c ├── app_with_shared_ctypes │ ├── src │ │ ├── sharedlib.h │ │ └── sharedlib.c │ ├── main.py │ └── setup.py ├── basic_plugin │ ├── main.py │ └── setup.py ├── argv_app │ ├── setup.py │ ├── main.py │ └── setup-with-urlscheme.py ├── shell_app │ ├── setup.py │ └── main.py ├── app_with_environment │ ├── main.py │ └── setup.py ├── test_env_health.py ├── test_recipe_imports.py ├── tools.py ├── test_filters.py ├── test_setup.py ├── test_py2applet.py ├── bundle_loader.m ├── test_compile_resources.py ├── test_shell_environment.py ├── test_utils.py ├── test_lsenvironment.py └── test_explicit_includes.py ├── src └── py2app │ ├── bootstrap │ ├── __init__.py │ ├── argv_inject.py │ ├── setup_ctypes.py │ ├── boot_aliasapp.py │ ├── boot_plugin.py │ ├── boot_aliasplugin.py │ ├── boot_app.py │ ├── _setup_importlib.py │ ├── setup_included_subpackages.py │ ├── _disable_linecache.py │ ├── virtualenv.py │ ├── virtualenv_site_packages.py │ ├── site_packages.py │ └── emulate_shell_environment.py │ ├── converters │ ├── __init__.py │ ├── coredata.py │ └── nibfile.py │ ├── recipes │ ├── qt.conf │ ├── matplotlib_prescript.py │ ├── pandas.py │ ├── zmq.py │ ├── gcloud.py │ ├── shiboken2.py │ ├── shiboken6.py │ ├── sphinx.py │ ├── pygame.py │ ├── pyopengl.py │ ├── _types.py │ ├── pylsp.py │ ├── wx.py │ ├── __init__.py │ ├── pydantic.py │ ├── pyenchant.py │ ├── lxml.py │ ├── PIL │ │ ├── prescript.py │ │ └── __init__.py │ ├── multiprocessing.py │ ├── sqlalchemy.py │ ├── matplotlib.py │ ├── qt6.py │ ├── qt5.py │ ├── pyside.py │ ├── pyside2.py │ ├── pyside6.py │ ├── black.py │ └── setuptools.py │ ├── apptemplate │ ├── __init__.py │ ├── lib │ │ └── __error__.sh │ └── plist_template.py │ ├── _recipedefs │ ├── __init__.py │ ├── opencv.py │ ├── sphinx.py │ ├── platformdirs.py │ └── truststore.py │ ├── __init__.py │ ├── _stubs │ └── __main__.py │ ├── progress.py │ ├── filters.py │ ├── _bundlepaths.py │ └── _progress.py ├── doc ├── license.rst ├── requirements.txt ├── supported-platforms.rst ├── Makefile ├── install.rst ├── environment.rst ├── debugging.rst ├── index.rst ├── command-line.rst ├── examples.rst ├── faq.rst └── tweaking.rst ├── .readthedocs.yml ├── .codespellrc ├── Makefile ├── .github └── workflows │ ├── extract-notes.py │ ├── pre-commit.yml │ └── release_to_pypi.yml ├── .gitignore ├── .flake8 ├── README.rst ├── LICENSE.txt ├── tox.ini ├── .pre-commit-config.yaml └── pyproject.toml /examples/PIL/requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | -------------------------------------------------------------------------------- /examples/pygame/requirements.txt: -------------------------------------------------------------------------------- 1 | pygame 2 | -------------------------------------------------------------------------------- /stubs/PIL/JpegPresets.pyi: -------------------------------------------------------------------------------- 1 | presets: dict 2 | -------------------------------------------------------------------------------- /stubs/modulegraph/{zipio}.pyi: -------------------------------------------------------------------------------- 1 | """ """ 2 | -------------------------------------------------------------------------------- /examples/PySide/requirements.txt: -------------------------------------------------------------------------------- 1 | PySide2 2 | -------------------------------------------------------------------------------- /examples/PySide6/requirements.txt: -------------------------------------------------------------------------------- 1 | PySide6 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/package2/sub/data.dat: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /py2app_tests/basic_app2/package2/sub/data.dat: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stubs/macholib/__init__.pyi: -------------------------------------------------------------------------------- 1 | """ stub """ 2 | -------------------------------------------------------------------------------- /examples/PyQt/hello_app/requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5 2 | -------------------------------------------------------------------------------- /examples/PyQt/view_app/requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5 2 | -------------------------------------------------------------------------------- /py2app_tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ py2app tests """ 2 | -------------------------------------------------------------------------------- /py2app_tests/app_with_data/data1/file1.txt: -------------------------------------------------------------------------------- 1 | FILE1.TXT 2 | -------------------------------------------------------------------------------- /py2app_tests/app_with_data/data1/file2.txt: -------------------------------------------------------------------------------- 1 | FILE2.TXT 2 | -------------------------------------------------------------------------------- /py2app_tests/app_with_data/data2/source.c: -------------------------------------------------------------------------------- 1 | SOURCE.C 2 | -------------------------------------------------------------------------------- /py2app_tests/app_with_data/data3/source.c: -------------------------------------------------------------------------------- 1 | SOURCE3.C 2 | -------------------------------------------------------------------------------- /examples/PyObjC/ICSharingWatcher/requirements.txt: -------------------------------------------------------------------------------- 1 | pyobjc 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/package2/sub/__init__.py: -------------------------------------------------------------------------------- 1 | foo = 42 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/package3/mod.py: -------------------------------------------------------------------------------- 1 | """ package3.mod """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app2/package2/sub/__init__.py: -------------------------------------------------------------------------------- 1 | foo = 42 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/package1/__init__.py: -------------------------------------------------------------------------------- 1 | """ package1 """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/package2/__init__.py: -------------------------------------------------------------------------------- 1 | """ package2 """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app2/package1/__init__.py: -------------------------------------------------------------------------------- 1 | """ package1 """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app2/package2/__init__.py: -------------------------------------------------------------------------------- 1 | """ package2 """ 2 | -------------------------------------------------------------------------------- /py2app_tests/pkg_script_app/quot/queue.py: -------------------------------------------------------------------------------- 1 | """ quot.queue """ 2 | -------------------------------------------------------------------------------- /py2app_tests/plugin_with_scripts/helper1.py: -------------------------------------------------------------------------------- 1 | print("Helper 1") 2 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/__init__.py: -------------------------------------------------------------------------------- 1 | """ py2app bootstrap files """ 2 | -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/requirements.txt: -------------------------------------------------------------------------------- 1 | pyobjc-framework-Cocoa 2 | -------------------------------------------------------------------------------- /py2app_tests/pkg_script_app/quot/__init__.py: -------------------------------------------------------------------------------- 1 | """ quot package """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/package1/__init__.py: -------------------------------------------------------------------------------- 1 | """ package1 """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/package2/__init__.py: -------------------------------------------------------------------------------- 1 | """ package2 """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/package3/mod.py: -------------------------------------------------------------------------------- 1 | """ package3.mod """ 2 | -------------------------------------------------------------------------------- /src/py2app/converters/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Resource data converters 3 | """ 4 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/package1/subpackage/__init__.py: -------------------------------------------------------------------------------- 1 | """ package1.subpackage """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app2/package1/subpackage/__init__.py: -------------------------------------------------------------------------------- 1 | """ package1.subpackage """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/package2/sub/__init__.py: -------------------------------------------------------------------------------- 1 | """ package2.sub """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/package1/subpackage/module.py: -------------------------------------------------------------------------------- 1 | """ package1.subpackage.module """ 2 | -------------------------------------------------------------------------------- /py2app_tests/basic_app2/package1/subpackage/module.py: -------------------------------------------------------------------------------- 1 | """ package1.subpackage.module """ 2 | -------------------------------------------------------------------------------- /doc/license.rst: -------------------------------------------------------------------------------- 1 | py2app license 2 | ============== 3 | 4 | .. literalinclude:: ../LICENSE.txt 5 | -------------------------------------------------------------------------------- /py2app_tests/app_with_data/data1/file3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Hello world" 4 | exit 0 5 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_plugin/plugin.c: -------------------------------------------------------------------------------- 1 | int plugin(void) 2 | { 3 | return 42; 4 | } 5 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/package1/subpackage/__init__.py: -------------------------------------------------------------------------------- 1 | """ package1.subpackage """ 2 | -------------------------------------------------------------------------------- /py2app_tests/plugin_with_scripts/helper2.py: -------------------------------------------------------------------------------- 1 | import code # noqa: F401 2 | 3 | print("Helper 2") 4 | -------------------------------------------------------------------------------- /src/py2app/recipes/qt.conf: -------------------------------------------------------------------------------- 1 | ; Qt Configuration file 2 | [Paths] 3 | Plugins = Resources/qt_plugins 4 | -------------------------------------------------------------------------------- /examples/PIL/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | name = "Hello" 3 | script = "hello.py" 4 | -------------------------------------------------------------------------------- /examples/PySide/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | name = "Hello" 3 | script = "hello.py" 4 | -------------------------------------------------------------------------------- /examples/PySide6/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | name = "Hello" 3 | script = "hello.py" 4 | -------------------------------------------------------------------------------- /py2app_tests/app_with_scripts/helper1.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | print(f"Helper 1: {curses.__name__}") 4 | -------------------------------------------------------------------------------- /py2app_tests/app_with_scripts/src/libfoo.h: -------------------------------------------------------------------------------- 1 | /* Helper module */ 2 | 3 | extern int square(int value); 4 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/package1/subpackage/module.py: -------------------------------------------------------------------------------- 1 | """ package1.subpackage.module """ 2 | -------------------------------------------------------------------------------- /examples/PyQt/hello_app/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | name = "Hello" 3 | script = "hello.py" 4 | -------------------------------------------------------------------------------- /examples/pygame/data/bomb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/bomb.gif -------------------------------------------------------------------------------- /examples/pygame/data/boom.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/boom.wav -------------------------------------------------------------------------------- /examples/pygame/data/shot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/shot.gif -------------------------------------------------------------------------------- /examples/multiprocessing/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | name = "MPExample" 3 | script = "main.py" 4 | -------------------------------------------------------------------------------- /examples/pygame/data/alien1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/alien1.gif -------------------------------------------------------------------------------- /examples/pygame/data/alien2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/alien2.gif -------------------------------------------------------------------------------- /examples/pygame/data/alien3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/alien3.gif -------------------------------------------------------------------------------- /examples/pygame/data/car_door.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/car_door.wav -------------------------------------------------------------------------------- /examples/pygame/data/house_lo.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/house_lo.wav -------------------------------------------------------------------------------- /examples/pygame/data/player1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/player1.gif -------------------------------------------------------------------------------- /src/py2app/apptemplate/__init__.py: -------------------------------------------------------------------------------- 1 | from . import plist_template # noqa: F401 2 | from . import setup # noqa: F401 3 | -------------------------------------------------------------------------------- /examples/pygame/data/background.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/background.gif -------------------------------------------------------------------------------- /examples/pygame/data/explosion1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/data/explosion1.gif -------------------------------------------------------------------------------- /py2app_tests/app_with_data/main.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/py2app_tests/app_with_data/main.icns -------------------------------------------------------------------------------- /stubs/ctypes/macholib/dyld.pyi: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | DEFAULT_FRAMEWORK_FALLBACK: list[str] 4 | DEFAULT_LIBRARY_FALLBACK: list[str] 5 | -------------------------------------------------------------------------------- /py2app_tests/app_with_scripts/subdir/helper2.py: -------------------------------------------------------------------------------- 1 | import code # noqa: F401 2 | 3 | import foo 4 | 5 | print(f"Helper 2: {foo.sq_2}") 6 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main.py"], 6 | ) 7 | -------------------------------------------------------------------------------- /examples/pygame/English.lproj/aliens.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/English.lproj/aliens.icns -------------------------------------------------------------------------------- /py2app_tests/app_with_scripts/src/libfoo.c: -------------------------------------------------------------------------------- 1 | #include "libfoo.h" 2 | 3 | 4 | int square(int value) 5 | { 6 | return value * value; 7 | } 8 | -------------------------------------------------------------------------------- /py2app_tests/basic_app2/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main-script"], 6 | ) 7 | -------------------------------------------------------------------------------- /examples/PyQt/view_app/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | name = "Viewer" 3 | script = "main.py" 4 | resources = [ "view.qml" ] 5 | -------------------------------------------------------------------------------- /examples/PyObjC/ICSharingWatcher/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | script = "ICSSharingWatcher.py" 3 | resources = [ "MainMenu.nib" ] 4 | -------------------------------------------------------------------------------- /py2app_tests/app_with_email/setup-all.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main-all.py"], 6 | ) 7 | -------------------------------------------------------------------------------- /py2app_tests/app_with_email/setup-plain.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main-plain.py"], 6 | ) 7 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main.py"], 6 | ) 7 | -------------------------------------------------------------------------------- /src/py2app/recipes/matplotlib_prescript.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["MATPLOTLIBDATA"] = os.path.join(os.environ["RESOURCEPATH"], "mpl-data") 4 | -------------------------------------------------------------------------------- /py2app_tests/app_with_email/setup-compat.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main-compat.py"], 6 | ) 7 | -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/MainMenu.nib/objects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/PyObjC/TinyTinyEdit/MainMenu.nib/objects.nib -------------------------------------------------------------------------------- /examples/Tkinter/hello_tk/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | name = "Hello" 3 | script = "hello.py" 4 | resources = [ 5 | "data.txt", 6 | ] 7 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/package2/sub/data.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/py2app_tests/basic_app_with_encoding/package2/sub/data.dat -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/MainMenu.nib/classes.nib: -------------------------------------------------------------------------------- 1 | { 2 | IBClasses = ({CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }); 3 | IBVersion = 1; 4 | } 5 | -------------------------------------------------------------------------------- /examples/pygame/English.lproj/MainMenu.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/pygame/English.lproj/MainMenu.nib/keyedobjects.nib -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/TinyTinyDocument.nib/objects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldoussoren/py2app/HEAD/examples/PyObjC/TinyTinyEdit/TinyTinyDocument.nib/objects.nib -------------------------------------------------------------------------------- /examples/multiprocessing/main.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | from proc import main 4 | 5 | if __name__ == "__main__": 6 | multiprocessing.freeze_support() 7 | main() 8 | -------------------------------------------------------------------------------- /py2app_tests/resource_compile_app/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="Resources", 5 | app=["main.py"], 6 | data_files=["MainMenu.xib"], 7 | ) 8 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.11" 6 | 7 | python: 8 | install: 9 | - requirements: docs/requirements.txt 10 | -------------------------------------------------------------------------------- /stubs/modulegraph/util.pyi: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | import typing 4 | 5 | def imp_find_module( 6 | name: str, path: typing.Sequence[str] | str | None = None 7 | ) -> typing.Tuple[typing.IO | None, str, str]: ... 8 | -------------------------------------------------------------------------------- /examples/multiprocessing/proc.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pool 2 | 3 | 4 | def f(x): 5 | return x * x 6 | 7 | 8 | def main(): 9 | with Pool(5) as p: 10 | print(p.map(f, [1, 2, 3])) 11 | -------------------------------------------------------------------------------- /py2app_tests/app_with_sharedlib/src/sharedlib.h: -------------------------------------------------------------------------------- 1 | #ifndef SHARED_LIB_H 2 | #define SHARED_LIB_H 3 | 4 | extern int squared(int); 5 | extern int doubled(int); 6 | extern int half(int); 7 | 8 | #endif /* SHARED_LIB_H */ 9 | -------------------------------------------------------------------------------- /py2app_tests/app_with_scripts/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main.py"], 6 | options={"py2app": {"extra_scripts": ["helper1.py", "subdir/helper2.py"]}}, 7 | ) 8 | -------------------------------------------------------------------------------- /py2app_tests/app_with_shared_ctypes/src/sharedlib.h: -------------------------------------------------------------------------------- 1 | #ifndef SHARED_LIB_H 2 | #define SHARED_LIB_H 3 | 4 | extern int squared(int); 5 | extern int doubled(int); 6 | extern int half(int); 7 | 8 | #endif /* SHARED_LIB_H */ 9 | -------------------------------------------------------------------------------- /src/py2app/_recipedefs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | py2app recipes 3 | 4 | All submodules from this package must be 5 | imported in __init__ 6 | """ 7 | 8 | from . import opencv, platformdirs, sphinx, stdlib, truststore # noqa: F401 9 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | ; PyObjCTest added temporarily 3 | skip = *.fwinfo,*.egg-info,*js,_build,PyObjCTest,*.rtf,_metadata.py,*.mht,workenv*,test_*.py,build,dist,.git 4 | ignore-words-list=inout,doubleClick,doubleclick,assertIn 5 | -------------------------------------------------------------------------------- /py2app_tests/basic_plugin/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import Foundation 4 | 5 | 6 | class BasicPlugin(Foundation.NSObject): 7 | def performCommand_(self, cmd): 8 | print(f"+ {cmd}") 9 | sys.stdout.flush() 10 | -------------------------------------------------------------------------------- /py2app_tests/plugin_with_scripts/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import Foundation 4 | 5 | 6 | class BasicPlugin(Foundation.NSObject): 7 | def performCommand_(self, cmd): 8 | print(f"+ {cmd}") 9 | sys.stdout.flush() 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | echo "stubs: Create sub executables" 3 | echo "clean: Remove stubs" 4 | 5 | stubs: 6 | python -m py2app._apptemplate 7 | 8 | clean: 9 | rm src/py2app/_apptemplate/launcher-*-* 10 | 11 | 12 | .PHONY: stubs clean 13 | -------------------------------------------------------------------------------- /py2app_tests/argv_app/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main.py"], 6 | options={ 7 | "py2app": { 8 | "argv_emulation": True, 9 | } 10 | }, 11 | ) 12 | -------------------------------------------------------------------------------- /py2app_tests/app_with_shared_ctypes/src/sharedlib.c: -------------------------------------------------------------------------------- 1 | #include "sharedlib.h" 2 | 3 | int squared(int x) 4 | { 5 | return x*x; 6 | } 7 | 8 | int doubled(int x) 9 | { 10 | return x+x; 11 | } 12 | 13 | int half(int x) 14 | { 15 | return x/2; 16 | } 17 | -------------------------------------------------------------------------------- /py2app_tests/app_with_sharedlib/src/sharedlib.c: -------------------------------------------------------------------------------- 1 | #include "sharedlib.h" 2 | 3 | int squared(int x) 4 | { 5 | return x*x; 6 | } 7 | 8 | int doubled(int x) 9 | { 10 | return x+x; 11 | } 12 | 13 | int half(int x) 14 | { 15 | return x/2; 16 | } 17 | -------------------------------------------------------------------------------- /examples/PIL/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | setup( 11 | app=["hello.py"], 12 | setup_requires=["py2app", "Pillow"], 13 | ) 14 | -------------------------------------------------------------------------------- /py2app_tests/shell_app/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main.py"], 6 | options={ 7 | "py2app": { 8 | "emulate_shell_environment": True, 9 | } 10 | }, 11 | ) 12 | -------------------------------------------------------------------------------- /examples/Tkinter/hello_tk/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | setup( 11 | app=["hello.py"], 12 | setup_requires=["py2app"], 13 | ) 14 | -------------------------------------------------------------------------------- /.github/workflows/extract-notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import re 4 | 5 | with open("doc/changelog.rst") as stream: 6 | body = stream.read() 7 | 8 | value = re.split("^-+$", body, flags=re.MULTILINE)[1] 9 | print(value.rsplit("\n", 2)[0].strip()) 10 | -------------------------------------------------------------------------------- /examples/PyQt/hello_app/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from distutils.core import setup 9 | 10 | setup( 11 | setup_requires=["py2app", "PyQt5"], 12 | app=["hello.py"], 13 | ) 14 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-sitemap 2 | sphinxcontrib.blockdiag 3 | sphinxcontrib.spelling 4 | sphinx>=7.3 5 | shibuya 6 | sphinx-copybutton 7 | sphinx-design 8 | sphinx-tabs 9 | sphinx-togglebutton 10 | sphinx-docsearch 11 | sphinxcontrib-mermaid 12 | sphinx-reredirects 13 | -------------------------------------------------------------------------------- /examples/PySide/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | OPTIONS = {} 11 | 12 | setup( 13 | app=["hello.py"], 14 | options={"py2app": OPTIONS}, 15 | ) 16 | -------------------------------------------------------------------------------- /examples/PySide6/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | OPTIONS = {} 11 | 12 | setup( 13 | app=["hello.py"], 14 | options={"py2app": OPTIONS}, 15 | ) 16 | -------------------------------------------------------------------------------- /stubs/macholib/framework.pyi: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | class _FrameworkInfo(typing.TypedDict): 4 | location: str 5 | name: str 6 | shortname: str 7 | version: str 8 | suffix: str 9 | 10 | def framework_info(filename: str) -> typing.Optional[_FrameworkInfo]: ... 11 | -------------------------------------------------------------------------------- /examples/multiprocessing/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | setup( 11 | name="MPExample", 12 | app=["main.py"], 13 | setup_requires=["py2app"], 14 | ) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/_build 2 | workenv 3 | build 4 | dist 5 | dist2 6 | py2app.egg-info 7 | .DS_Store 8 | __pycache__ 9 | .tox 10 | .coverage 11 | htmlcov 12 | TODO.txt 13 | 14 | syntax: glob 15 | *.dSYM 16 | *.pyc 17 | *.pyo 18 | *.so 19 | *.sv 20 | *.swp 21 | .coverage* 22 | launcher-*-* 23 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/argv_inject.py: -------------------------------------------------------------------------------- 1 | def _argv_inject(argv: "list[str]") -> None: 2 | import sys 3 | 4 | # only use if started by LaunchServices 5 | if len(sys.argv) > 1 and sys.argv[1].startswith("-psn"): 6 | sys.argv[1:2] = argv 7 | else: 8 | sys.argv[1:1] = argv 9 | -------------------------------------------------------------------------------- /py2app_tests/pkg_script_app/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | APP = ["quot.py"] 4 | DATA_FILES = [] 5 | OPTIONS = {"argv_emulation": True} 6 | 7 | setup( 8 | app=APP, 9 | data_files=DATA_FILES, 10 | options={"py2app": OPTIONS}, 11 | setup_requires=["py2app"], 12 | ) 13 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | select = C,E,F,W,B,B950,T,Q,M,B950,A,I 4 | # Temporarily ignore T000 messages 5 | ignore = E501,W503,E203,T000,A005 6 | inline-quotes = double 7 | multiline-quotes = double 8 | docstring-quotes = double 9 | banned-modules = pkg_resources = Use importlib 10 | -------------------------------------------------------------------------------- /examples/PyQt/view_app/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | APP = ["main.py"] 4 | DATA_FILES = ["view.qml"] 5 | OPTIONS = {"argv_emulation": False} 6 | 7 | setup( 8 | app=APP, 9 | data_files=DATA_FILES, 10 | options={"py2app": OPTIONS}, 11 | setup_requires=["py2app", "PyQt5"], 12 | ) 13 | -------------------------------------------------------------------------------- /py2app_tests/shell_app/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys # noqa: F401 3 | 4 | root = os.path.dirname( 5 | os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | ) 7 | 8 | fp = open(os.path.join(root, "env.txt"), "w") 9 | fp.write(repr(dict(os.environ))) 10 | fp.write("\n") 11 | fp.close() 12 | -------------------------------------------------------------------------------- /src/py2app/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Builds macOS application and plugin bundles from Python scripts 3 | 4 | There are no public API points in this package, functionality 5 | is invoked either using ``python -m py2app`` (preferred) or 6 | ``python setup.py py2app`` (legacy, deprecated). 7 | """ 8 | 9 | __version__ = "2.0a0" 10 | -------------------------------------------------------------------------------- /examples/pygame/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | name = "Aliens" 3 | script = "demo.py" 4 | resources = ["data", "English.lproj"] 5 | 6 | [tool.py2app.bundle.main.plist] 7 | CFBundleShortVersionString = "0.1" 8 | CFBundleGetInfoString = "aliens 0.1" 9 | CFBundleIdentifier = "org.pygame.examples.aliens" 10 | -------------------------------------------------------------------------------- /py2app_tests/basic_plugin/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | plist = {"NSPrincipleClass": "BasicPlugin"} 4 | 5 | setup( 6 | name="BasicPlugin", 7 | plugin=["main.py"], 8 | options={ 9 | "py2app": { 10 | "extension": ".bundle", 11 | "plist": plist, 12 | } 13 | }, 14 | ) 15 | -------------------------------------------------------------------------------- /stubs/README.txt: -------------------------------------------------------------------------------- 1 | Minimal typing stubs for libraries used by pyapp. 2 | 3 | These stubs are far from complete, just the minimal 4 | needed to get good enough typing checking for py2app 5 | itself. 6 | 7 | Note: Only add stubs here for libraries that 8 | don't have type info available, prefer installing 9 | those libraries in tox.ini. 10 | -------------------------------------------------------------------------------- /examples/PyObjC/ICSharingWatcher/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | setup( 11 | data_files=["MainMenu.nib"], 12 | app=["ICSharingWatcher.py"], 13 | install_requires=["pyobjc"], 14 | setup_requires=["py2app"], 15 | ) 16 | -------------------------------------------------------------------------------- /py2app_tests/argv_app/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | root = os.path.dirname( 5 | os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | ) 7 | 8 | sys.argv[0] = os.path.realpath(sys.argv[0]) 9 | fp = open(os.path.join(root, "argv.txt"), "w") 10 | fp.write(repr(sys.argv)) 11 | fp.write("\n") 12 | fp.close() 13 | -------------------------------------------------------------------------------- /examples/pygame/English.lproj/MainMenu.nib/JavaCompiling.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JavaSourceSubpath 6 | _MainMenu_EOArchive_English.java 7 | 8 | 9 | -------------------------------------------------------------------------------- /py2app_tests/app_with_environment/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | root = os.path.dirname( 5 | os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | ) 7 | 8 | sys.argv[0] = os.path.realpath(sys.argv[0]) 9 | fp = open(os.path.join(root, "env.txt"), "w") 10 | fp.write(repr(dict(os.environ))) 11 | fp.write("\n") 12 | fp.close() 13 | -------------------------------------------------------------------------------- /examples/PyObjC/ICSharingWatcher/MainMenu.nib/classes.nib: -------------------------------------------------------------------------------- 1 | { 2 | IBClasses = ( 3 | {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, 4 | { 5 | CLASS = TableModelAppDelegate; 6 | LANGUAGE = ObjC; 7 | OUTLETS = {mainWindow = id; }; 8 | SUPERCLASS = NSObject; 9 | } 10 | ); 11 | IBVersion = 1; 12 | } 13 | -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/TinyTinyDocument.nib/classes.nib: -------------------------------------------------------------------------------- 1 | { 2 | IBClasses = ( 3 | {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, 4 | { 5 | CLASS = TinyTinyDocument; 6 | LANGUAGE = ObjC; 7 | OUTLETS = {textView = id; }; 8 | SUPERCLASS = NSDocument; 9 | } 10 | ); 11 | IBVersion = 1; 12 | } 13 | -------------------------------------------------------------------------------- /stubs/modulegraph/zipio.pyi: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | def open(path: str, mode: str = "r") -> typing.IO: ... 4 | def listdir(path: str) -> typing.Sequence[str]: ... 5 | def isfile(path: str) -> bool: ... 6 | def isdir(path: str) -> bool: ... 7 | def islink(path: str) -> bool: ... 8 | def readlink(path: str) -> str: ... 9 | def getmode(path: str) -> int: ... 10 | def getmtime(path: str) -> float: ... 11 | -------------------------------------------------------------------------------- /examples/pygame/English.lproj/MainMenu.nib/classes.nib: -------------------------------------------------------------------------------- 1 | { 2 | IBClasses = ( 3 | {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, 4 | { 5 | ACTIONS = {}; 6 | CLASS = PygameAppDelegate; 7 | LANGUAGE = ObjC; 8 | OUTLETS = {}; 9 | SUPERCLASS = NSObject; 10 | } 11 | ); 12 | IBVersion = 1; 13 | } 14 | -------------------------------------------------------------------------------- /py2app_tests/plugin_with_scripts/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | plist = {"NSPrincipleClass": "BasicPlugin"} 4 | 5 | setup( 6 | name="BasicPlugin", 7 | plugin=["main.py"], 8 | options={ 9 | "py2app": { 10 | "extra_scripts": ["helper1.py", "helper2.py"], 11 | "extension": ".bundle", 12 | "plist": plist, 13 | } 14 | }, 15 | ) 16 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/setup_ctypes.py: -------------------------------------------------------------------------------- 1 | def _setup_ctypes() -> None: 2 | import os 3 | import sys 4 | from ctypes.macholib import dyld 5 | 6 | frameworks = os.path.join(sys.py2app_bundle_resources, "..", "Frameworks") # type: ignore[attr-defined] 7 | dyld.DEFAULT_FRAMEWORK_FALLBACK.insert(0, frameworks) 8 | dyld.DEFAULT_LIBRARY_FALLBACK.insert(0, frameworks) 9 | 10 | 11 | _setup_ctypes() 12 | -------------------------------------------------------------------------------- /examples/PyQt/view_app/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 1.0 2 | 3 | Rectangle { 4 | width: 240; height: 320; 5 | 6 | resources: [ 7 | Component { 8 | id: contactDelegate 9 | Text { 10 | text: modelData.firstName + " " + modelData.lastName 11 | } 12 | } 13 | ] 14 | 15 | ListView { 16 | anchors.fill: parent 17 | model: myModel 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.py2app.bundle.main] 2 | script = "TinyTinyEdit.py" 3 | resources = [ "MainMenu.nib", "TinyTinyDocument.nib" ] 4 | 5 | [tool.py2app.bundle.main.plist] 6 | [[tool.py2app.bundle.main.plist.CFBundleDocumentTypes]] 7 | "CFBundleTypeExtensions" = ["txt", "text", "*"] 8 | "CFBundleTypeName" = "Text File" 9 | "CFBundleTypeRole" = "Editor" 10 | "NSDocumentClass" = "TinyTinyDocument" 11 | -------------------------------------------------------------------------------- /examples/PyObjC/ICSharingWatcher/ICSharingWatcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | Display the useful contents of /var/db/dhcpd_leases 3 | 4 | This lets you see what IP addresses are leased out when using 5 | Internet Connection Sharing 6 | """ 7 | 8 | # import classes required to start application 9 | import TableModelAppDelegate # noqa: F401 10 | from PyObjCTools import AppHelper 11 | 12 | # start the event loop 13 | AppHelper.runEventLoop(argv=[], installInterrupt=False) 14 | -------------------------------------------------------------------------------- /src/py2app/recipes/pandas.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from modulegraph.modulegraph import ModuleGraph 4 | 5 | from .. import build_app 6 | from ._types import RecipeInfo 7 | 8 | 9 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 10 | m = mf.findNode("pandas") 11 | if m is None or m.filename is None: 12 | return None 13 | 14 | includes = ["pandas._libs.tslibs.base"] 15 | 16 | return {"includes": includes} 17 | -------------------------------------------------------------------------------- /stubs/macholib/dyld.pyi: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | __all__ = ("framework_info", "dyld_find", "framework_find") 4 | 5 | from macholib.framework import framework_info 6 | 7 | def dyld_find( 8 | name: str, 9 | executable_path: str | None = None, 10 | env: dict[str, str] | None = None, 11 | loader_path: str | None = None, 12 | ) -> str: ... 13 | def framework_find( 14 | fn: str, executable_path: str | None = None, env: dict[str, str] | None = None 15 | ) -> str: ... 16 | -------------------------------------------------------------------------------- /src/py2app/apptemplate/lib/__error__.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is the default apptemplate error script 4 | # 5 | 6 | echo "Launch error" 7 | if [ -n "$2" ]; then 8 | echo "An unexpected error has occurred during execution of the main script" 9 | echo "" 10 | echo "$2: $3" 11 | echo "" 12 | fi 13 | 14 | echo "See the py2app website for debugging launch issues" 15 | echo "" 16 | echo "ERRORURL: https://py2app.readthedocs.io/en/latest/debugging.html" 17 | exit 18 | -------------------------------------------------------------------------------- /stubs/macholib/MachO.pyi: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | import typing 4 | 5 | def lc_str_value(offset: int, cmd_info: tuple) -> bytes: ... 6 | 7 | class MachO: 8 | headers: typing.List[typing.Any] # XXX 9 | filename: str 10 | loader_path: str 11 | 12 | def __init__(self, filename: str, allow_unknown_load_commands: bool = False): ... 13 | def write(self, fileobj: typing.IO[bytes]) -> None: ... 14 | def rewriteLoadCommands(self, changefunc: typing.Callable[[str], str]) -> bool: ... 15 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | py2app is a tool that will create standalone application 2 | and plugin bundles on macOS from Python scripts. 3 | 4 | **NOTE:** The master branch is work in progress, current releases 5 | are created from branch ``v0.28-branch``. 6 | 7 | 8 | Project links 9 | ------------- 10 | 11 | * `Documentation `_ 12 | 13 | * `Issue Tracker `_ 14 | 15 | * `Repository `_ 16 | -------------------------------------------------------------------------------- /py2app_tests/app_with_environment/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main.py"], 6 | options={ 7 | "py2app": { 8 | "plist": { 9 | "LSEnvironment": { 10 | "LANG": "nl_NL.latin1", 11 | "LC_CTYPE": "nl_NL.UTF-8", 12 | "EXTRA_VAR": "hello world", 13 | "KNIGHT": "ni!", 14 | } 15 | } 16 | } 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /py2app_tests/test_env_health.py: -------------------------------------------------------------------------------- 1 | # Temporary tests to detect common issues caused 2 | # by switching between the 2.0 and 0.28 branches 3 | # during development. 4 | import pathlib 5 | from unittest import TestCase 6 | 7 | 8 | class TestEnvironmentHealth(TestCase): 9 | def test_health(self): 10 | if pathlib.Path("py2app.egg-info").exists(): 11 | self.fail("Old 'py2app.egg-info' found") 12 | 13 | if pathlib.Path("py2app").exists(): 14 | self.fail("Old 'py2app' source tree found") 15 | -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/TinyTinyDocument.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBDocumentLocation 6 | 44 306 356 240 0 0 1152 746 7 | IBFramework Version 8 | 291.0 9 | IBOpenObjects 10 | 11 | 5 12 | 13 | IBSystem Version 14 | 6L60 15 | 16 | 17 | -------------------------------------------------------------------------------- /stubs/macholib/mach_o.py: -------------------------------------------------------------------------------- 1 | CPU_TYPE_NAMES: dict[int, str] 2 | LC_NAMES: dict[int, str] 3 | 4 | LC_VERSION_MIN_MACOSX = 0x24 5 | LC_ID_DYLIB = 0x00 6 | LC_RPATH = 0x00 7 | 8 | 9 | class build_version_command: 10 | platform: int 11 | minos: int 12 | sdk: int 13 | ntools: int 14 | 15 | 16 | class version_min_command: 17 | version: int 18 | sdk: int 19 | 20 | 21 | class dylib_command: 22 | name: int 23 | timestamp: int 24 | current_version: int 25 | compatibility_version: int 26 | 27 | 28 | class rpath_command: 29 | path: int 30 | -------------------------------------------------------------------------------- /stubs/macholib/util.pyi: -------------------------------------------------------------------------------- 1 | """ Minimal stubs """ 2 | 3 | import typing 4 | 5 | NOT_SYSTEM_FILES: typing.List[str] 6 | 7 | def in_system_path(filename: str) -> bool: ... 8 | def is_platform_file(filename: str) -> bool: ... 9 | def copy2(src: str, dst: str) -> None: ... 10 | def mergecopy(src: str, dest: str) -> None: ... 11 | def mergetree( 12 | src: str, 13 | dst: str, 14 | condition: typing.Callable[[str], bool] | None = None, 15 | copyfn: typing.Callable[[str, str], None] = mergecopy, 16 | srcbase: str | None = None, 17 | ) -> None: ... 18 | def move(src: str, dst: str) -> None: ... 19 | -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/MainMenu.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBEditorPositions 6 | 7 | 29 8 | 87 457 318 44 0 0 1152 746 9 | 10 | IBFramework Version 11 | 291.0 12 | IBOpenObjects 13 | 14 | 29 15 | 16 | IBSystem Version 17 | 6L60 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/py2app/recipes/zmq.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | 4 | from modulegraph.modulegraph import ModuleGraph 5 | 6 | from .. import build_app 7 | from ._types import RecipeInfo 8 | 9 | 10 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 11 | m = mf.findNode("zmq") 12 | if m is None or m.filename is None: 13 | return None 14 | if m.packagepath is None: 15 | return None 16 | 17 | dylibs = os.scandir(os.path.join(m.packagepath[0], ".dylibs")) 18 | frameworks = [lib.path for lib in dylibs] 19 | 20 | return {"frameworks": frameworks} 21 | -------------------------------------------------------------------------------- /doc/supported-platforms.rst: -------------------------------------------------------------------------------- 1 | Supported platforms 2 | =================== 3 | 4 | Py2app is a project that targets macOS and does not work on other platforms, 5 | such as iOS, Linux or Windows. The project targets macOS 10.9 or later, and 6 | is tested regularly on the current release of macOS using the Python installers 7 | on `www.python.org `_. 8 | 9 | Py2app supports the Python versions that are supported by the CPython core 10 | developers and will add new versions of Python when they come available. 11 | 12 | Currently py2app supports Python 3.8 upto and including 3.13. 13 | -------------------------------------------------------------------------------- /examples/PyQt/hello_app/hello.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PyQt5 import Qt 4 | 5 | # We instantiate a QApplication passing the arguments of the script to it: 6 | a = Qt.QApplication(sys.argv) 7 | 8 | # Add a basic widget to this application: 9 | # The first argument is the text we want this QWidget to show, the second 10 | # one is the parent widget. Since Our "hello" is the only thing we use (the 11 | # so-called "MainWidget", it does not have a parent. 12 | hello = Qt.QLabel("

Hello, World

") 13 | 14 | # ... and that it should be shown. 15 | hello.show() 16 | 17 | # Now we can start it. 18 | a.exec_() 19 | -------------------------------------------------------------------------------- /examples/PySide/hello.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide import QtGui 4 | 5 | # We instantiate a QApplication passing the arguments of the script to it: 6 | a = QtGui.QApplication(sys.argv) 7 | 8 | # Add a basic widget to this application: 9 | # The first argument is the text we want this QWidget to show, the second 10 | # one is the parent widget. Since Our "hello" is the only thing we use (the 11 | # so-called "MainWidget", it does not have a parent. 12 | hello = QtGui.QLabel("

Hello, World

") 13 | 14 | # ... and that it should be shown. 15 | hello.show() 16 | 17 | # Now we can start it. 18 | a.exec_() 19 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/boot_aliasapp.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | SCRIPT_MAP: "dict[str, str]" 4 | DEFAULT_SCRIPT: str 5 | 6 | 7 | def _run() -> None: 8 | global __file__ 9 | import site # noqa: F401 10 | 11 | sys.frozen = "macosx_app" # type: ignore 12 | 13 | argv0 = sys.py2app_argv0.rsplit("/", 1)[-1] # type: ignore[attr-defined] 14 | script = SCRIPT_MAP.get(argv0, DEFAULT_SCRIPT) # noqa: F821 15 | 16 | sys.argv[0] = __file__ = script 17 | with open(script, "rb") as fp: 18 | source = fp.read() + b"\n" 19 | 20 | exec(compile(source, script, "exec"), globals(), globals()) 21 | -------------------------------------------------------------------------------- /src/py2app/recipes/gcloud.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from modulegraph.modulegraph import ModuleGraph 4 | 5 | from .. import build_app 6 | from ._types import RecipeInfo 7 | 8 | 9 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 10 | m = mf.findNode("gcloud") 11 | if m is None or m.filename is None: 12 | return None 13 | 14 | # Dependency in package metadata, but 15 | # no runtime dependency. Explicitly include 16 | # to ensure that the package metadata for 17 | # googleapis_common_protos is included. 18 | return {"includes": ["google.api"]} 19 | -------------------------------------------------------------------------------- /src/py2app/recipes/shiboken2.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from modulegraph.modulegraph import ModuleGraph 4 | 5 | from .. import build_app 6 | from ._types import RecipeInfo 7 | 8 | 9 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 10 | name = "shiboken2" 11 | m = mf.findNode(name) 12 | if m is None or m.filename is None: 13 | return None 14 | 15 | mf.import_hook("shiboken2.support", m, ["*"]) 16 | mf.import_hook("shiboken2.support.signature", m, ["*"]) 17 | mf.import_hook("shiboken2.support.signature.lib", m, ["*"]) 18 | 19 | return None 20 | -------------------------------------------------------------------------------- /examples/PySide6/hello.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtWidgets import QApplication, QLabel 4 | 5 | # We instantiate a QApplication passing the arguments of the script to it: 6 | a = QApplication(sys.argv) 7 | 8 | # Add a basic widget to this application: 9 | # The first argument is the text we want this QWidget to show, the second 10 | # one is the parent widget. Since Our "hello" is the only thing we use (the 11 | # so-called "MainWidget", it does not have a parent. 12 | hello = QLabel("

Hello, World

") 13 | 14 | # ... and that it should be shown. 15 | hello.show() 16 | 17 | # Now we can start it. 18 | a.exec() 19 | -------------------------------------------------------------------------------- /stubs/macholib/MachOStandalone.pyi: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | import typing 4 | import collections 5 | from macholib.MachOGraph import MachOGraph 6 | 7 | class MachOStandalone: 8 | dest: str 9 | pending: collections.deque 10 | excludes: typing.List[str] 11 | mm: MachOGraph 12 | 13 | def __init__( 14 | self, 15 | base: str, 16 | dest: typing.Optional[str] = None, 17 | graph: typing.Optional[str] = None, 18 | env: typing.Optional[typing.Dict[str, str]] = None, 19 | executable_path: typing.Optional[str] = None, 20 | ): ... 21 | def run(self) -> typing.Set[str]: ... 22 | -------------------------------------------------------------------------------- /py2app_tests/argv_app/setup-with-urlscheme.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | # A custom plist for letting it associate with a URL protocol. 4 | URLTYPES = [{"CFBundleURLName": "MyUrl", "CFBundleURLSchemes": ["myurl"]}] 5 | 6 | plist = { 7 | "NSAppleScriptEnabled": "YES", 8 | "CFBundleIdentifier": "com.myurl", 9 | "LSMinimumSystemVersion": "10.4", 10 | "CFBundleURLTypes": URLTYPES, 11 | } 12 | 13 | 14 | setup( 15 | name="BasicApp", 16 | app=["main.py"], 17 | options={ 18 | "py2app": { 19 | "argv_emulation": True, 20 | "plist": plist, 21 | } 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /src/py2app/recipes/shiboken6.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from modulegraph.modulegraph import ModuleGraph 4 | 5 | from .. import build_app 6 | from ._types import RecipeInfo 7 | 8 | 9 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 10 | name = "shiboken6" 11 | m = mf.findNode(name) 12 | if m is None or m.filename is None: 13 | return None 14 | 15 | mf.import_hook("shiboken6.support", m, ["*"]) 16 | mf.import_hook("shiboken6.support.signature", m, ["*"]) 17 | mf.import_hook("shiboken6.support.signature.lib", m, ["*"]) 18 | 19 | return {"packages": ["shiboken6"]} 20 | -------------------------------------------------------------------------------- /py2app_tests/app_with_data/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="BasicApp", 5 | app=["main.py"], 6 | options={ 7 | "py2app": { 8 | "plist": { 9 | "CFBundleName": "SimpleApp", 10 | "CFBundleShortVersionString": "1.0", 11 | "CFBudleGetInfoString": "SimpleApp 1.0", 12 | }, 13 | "iconfile": "main.icns", 14 | "resources": "data3/source.c", 15 | } 16 | }, 17 | data_files=[ 18 | ("sub1", ["data1/file1.txt", "data1/file2.txt", "data1/file3.sh"]), 19 | "data2", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /py2app_tests/app_with_email/main-plain.py: -------------------------------------------------------------------------------- 1 | import email # noqa: F401 2 | import sys 3 | 4 | 5 | def import_module(name): 6 | try: 7 | exec(f"import {name}") 8 | m = eval(name) 9 | except ImportError: 10 | print("* import failed") 11 | 12 | else: 13 | print(m.__name__) 14 | 15 | 16 | while True: 17 | line = sys.stdin.readline() 18 | if not line: 19 | break 20 | 21 | try: 22 | exec(line) 23 | except SystemExit: 24 | raise 25 | 26 | except Exception: 27 | print("* Exception " + str(sys.exc_info()[1])) 28 | 29 | sys.stdout.flush() 30 | sys.stderr.flush() 31 | -------------------------------------------------------------------------------- /examples/PyQt/view_app/main.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtCore import QUrl 2 | from PyQt4.QtDeclarative import QDeclarativeView 3 | from PyQt4.QtGui import QApplication 4 | 5 | # This example uses a QML file to show a scrolling list containing 6 | # all the items listed into dataList. 7 | 8 | dataList = ["Item 1", "Item 2", "Item 3", "Item 4"] 9 | 10 | app = QApplication([]) 11 | view = QDeclarativeView() 12 | 13 | ctxt = view.rootContext() 14 | ctxt.setContextProperty("myModel", dataList) 15 | 16 | url = QUrl( 17 | "view.qml" 18 | ) # <-- Problem seems to be here, the file gets copied correctly to Resources folder 19 | view.setSource(url) 20 | view.show() 21 | app.exec_() 22 | -------------------------------------------------------------------------------- /py2app_tests/app_with_email/main-all.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from email import * # noqa: F401, F403 3 | 4 | 5 | def import_module(name): 6 | try: 7 | exec(f"import {name}") 8 | m = eval(name) 9 | except ImportError: 10 | print("* import failed") 11 | 12 | else: 13 | print(m.__name__) 14 | 15 | 16 | while True: 17 | line = sys.stdin.readline() 18 | if not line: 19 | break 20 | 21 | try: 22 | exec(line) 23 | except SystemExit: 24 | raise 25 | 26 | except Exception: 27 | print("* Exception " + str(sys.exc_info()[1])) 28 | 29 | sys.stdout.flush() 30 | sys.stderr.flush() 31 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @echo "Usage:" 3 | @echo " make html # create html output" 4 | @echo " make view # show html output" 5 | @echo " make linkcheck # check links" 6 | 7 | html: _build/venv 8 | _build/venv/bin/sphinx-build -b html -d _build/doctrees . _build/html 9 | 10 | linkcheck: _build/venv 11 | _build/venv/bin/sphinx-build -b linkcheck -d _build/doctrees . _build/linkcheck 12 | 13 | view: 14 | open _build/html/index.html 15 | 16 | _build/venv: requirements.txt 17 | mkdir -p _build 18 | rm -rf _build/venv 19 | python3.11 -m venv _build/venv 20 | _build/venv/bin/python -m pip install -r requirements.txt 21 | 22 | .PHONY: html view linkcheck 23 | -------------------------------------------------------------------------------- /src/py2app/recipes/sphinx.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from modulegraph.modulegraph import ModuleGraph 4 | 5 | from .. import build_app 6 | from ._types import RecipeInfo 7 | 8 | 9 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 10 | m = mf.findNode("sphinx") 11 | if m is None or m.filename is None: 12 | return None 13 | 14 | includes = [ 15 | "sphinxcontrib.applehelp", 16 | "sphinxcontrib.devhelp", 17 | "sphinxcontrib.htmlhelp", 18 | "sphinxcontrib.jsmath", 19 | "sphinxcontrib.qthelp", 20 | "sphinxcontrib.serializinghtml", 21 | ] 22 | 23 | return {"includes": includes} 24 | -------------------------------------------------------------------------------- /examples/pygame/English.lproj/MainMenu.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBDocumentLocation 6 | 269 494 356 240 0 0 1600 1002 7 | IBEditorPositions 8 | 9 | 29 10 | 125 344 278 44 0 0 1600 1002 11 | 12 | IBFramework Version 13 | 349.0 14 | IBOpenObjects 15 | 16 | 29 17 | 18 | IBSystem Version 19 | 7D24 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/py2app/recipes/pygame.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | 4 | from modulegraph.modulegraph import ModuleGraph 5 | 6 | from .. import build_app 7 | from ._types import RecipeInfo 8 | 9 | 10 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 11 | m = mf.findNode("pygame") 12 | if m is None or m.filename is None: 13 | return None 14 | 15 | def addpath(f: str) -> str: 16 | assert m is not None 17 | assert m.filename is not None 18 | return os.path.join(os.path.dirname(m.filename), f) 19 | 20 | RESOURCES = ["freesansbold.ttf", "pygame_icon.icns"] 21 | return {"loader_files": [("pygame", list(map(addpath, RESOURCES)))]} 22 | -------------------------------------------------------------------------------- /src/py2app/_recipedefs/opencv.py: -------------------------------------------------------------------------------- 1 | from modulegraph2 import BaseNode 2 | 3 | from .._config import RecipeOptions 4 | from .._modulegraph import ModuleGraph 5 | from .._recipes import recipe 6 | 7 | 8 | @recipe("opencv-python", distribution="opencv-python", modules=["cv2"]) 9 | def opencv(graph: ModuleGraph, options: RecipeOptions) -> None: 10 | """ 11 | Recipe for `opencv-python `_ 12 | """ 13 | 14 | # The 'cv2.cv2' extension module imports 'numpy', update the 15 | # graph for this. 16 | m = graph.find_node("cv2.cv2") 17 | if not isinstance(m, BaseNode) or m.filename is None: 18 | return None 19 | 20 | graph.import_module(m, "numpy") 21 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.12' 16 | - name: set PY 17 | run: echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV 18 | - uses: actions/cache@v1 19 | with: 20 | path: ~/.cache/pre-commit 21 | key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} 22 | - uses: pre-commit/action@v1.0.1 23 | -------------------------------------------------------------------------------- /src/py2app/recipes/pyopengl.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | 4 | from modulegraph.modulegraph import ModuleGraph 5 | 6 | from .. import build_app 7 | from ._types import RecipeInfo 8 | 9 | 10 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 11 | m = mf.findNode("OpenGL") 12 | if m is None or m.filename is None: 13 | return None 14 | p = os.path.splitext(m.filename)[0] + ".py" 15 | # check to see if it's a patched version that doesn't suck 16 | if os.path.exists(p): 17 | for line in open(p): 18 | if line.startswith("__version__ = "): 19 | return {} 20 | # otherwise include the whole damned thing 21 | return {"packages": ["OpenGL"]} 22 | -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | plist = { 11 | "CFBundleDocumentTypes": [ 12 | { 13 | "CFBundleTypeExtensions": ["txt", "text", "*"], 14 | "CFBundleTypeName": "Text File", 15 | "CFBundleTypeRole": "Editor", 16 | "NSDocumentClass": "TinyTinyDocument", 17 | }, 18 | ], 19 | } 20 | 21 | setup( 22 | data_files=["MainMenu.nib", "TinyTinyDocument.nib"], 23 | app=[ 24 | {"script": "TinyTinyEdit.py", "plist": plist}, 25 | ], 26 | install_requires=["pyobjc-framework-Cocoa"], 27 | setup_requires=["py2app"], 28 | ) 29 | -------------------------------------------------------------------------------- /src/py2app/recipes/_types.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | class RecipeInfo(typing.TypedDict, total=False): 5 | expected_missing_imports: typing.Set[str] 6 | packages: typing.Sequence[str] 7 | flatpackages: typing.Sequence[typing.Union[str, typing.Tuple[str, str]]] 8 | loader_files: typing.Sequence[ 9 | typing.Union[str, typing.Tuple[str, typing.Sequence[str]]] 10 | ] 11 | prescripts: typing.Sequence[typing.Union[str, typing.IO[str]]] 12 | includes: typing.Sequence[str] 13 | resources: typing.Sequence[ 14 | typing.Union[ 15 | str, typing.Tuple[str, typing.Sequence[typing.Union[str, typing.IO[str]]]] 16 | ] 17 | ] 18 | frameworks: typing.Sequence[str] 19 | use_old_sdk: bool 20 | -------------------------------------------------------------------------------- /examples/PyObjC/ICSharingWatcher/MainMenu.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBDocumentLocation 6 | 199 472 585 307 0 0 1600 1002 7 | IBEditorPositions 8 | 9 | 29 10 | 93 283 318 44 0 0 1280 832 11 | 12 | IBFramework Version 13 | 387.0 14 | IBOpenObjects 15 | 16 | 21 17 | 18 | IBSystem Version 19 | 8A171 20 | IBUsesTextArchiving 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/boot_plugin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | SCRIPT_MAP: "dict[str|None, str]" 4 | DEFAULT_SCRIPT: str 5 | 6 | 7 | def _run() -> None: 8 | global __file__ 9 | import os 10 | import site # noqa: F401 11 | 12 | sys.frozen = "macosx_plugin" # type: ignore 13 | base = os.environ["RESOURCEPATH"] 14 | 15 | if "ARGVZERO" in os.environ: 16 | argv0 = os.path.basename(os.environ["ARGVZERO"]) 17 | else: 18 | argv0 = None 19 | 20 | script = SCRIPT_MAP.get(argv0, DEFAULT_SCRIPT) # noqa: F821 21 | 22 | __file__ = path = os.path.join(base, script) 23 | with open(path, "rb") as fp: 24 | source = fp.read() + b"\n" 25 | 26 | exec(compile(source, path, "exec"), globals(), globals()) 27 | -------------------------------------------------------------------------------- /examples/PIL/hello.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import PIL.Image 5 | 6 | PATH = "/System/Library//Automator/Crop Images.action/Contents/Resources/shark-tall-no-scale.jpg" 7 | 8 | print("globals() is %r" % id(globals())) 9 | 10 | 11 | def somefunc(): 12 | print("globals() is %r" % id(globals())) 13 | print("Hello from py2app") 14 | 15 | print("frozen", repr(getattr(sys, "frozen", None))) 16 | 17 | print("sys.path", sys.path) 18 | print("sys.executable", sys.executable) 19 | print("sys.prefix", sys.prefix) 20 | print("sys.argv", sys.argv) 21 | print("os.getcwd()", os.getcwd()) 22 | 23 | 24 | if __name__ == "__main__": 25 | somefunc() 26 | img = PIL.Image.open(PATH) 27 | print(type(img)) 28 | print(img.size) 29 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/boot_aliasplugin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | SCRIPT_MAP: "dict[str|None, str]" 4 | DEFAULT_SCRIPT: str 5 | 6 | 7 | def _run() -> None: 8 | global __file__ 9 | import os 10 | import site # noqa: F401 11 | 12 | sys.frozen = "macosx_plugin" # type: ignore 13 | base = os.environ["RESOURCEPATH"] 14 | 15 | if "ARGVZERO" in os.environ: 16 | argv0 = os.path.basename(os.environ["ARGVZERO"]) 17 | else: 18 | argv0 = None 19 | script = SCRIPT_MAP.get(argv0, DEFAULT_SCRIPT) # noqa: F821 20 | 21 | sys.argv[0] = __file__ = path = os.path.join(base, script) 22 | with open(path, "rb") as fp: 23 | source = fp.read() + b"\n" 24 | 25 | exec(compile(source, script, "exec"), globals(), globals()) 26 | -------------------------------------------------------------------------------- /py2app_tests/app_with_email/main-compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def function(): 5 | import email.Encoders as enc # noqa: F401 6 | from email import MIMEText # noqa: F401 7 | 8 | 9 | function() 10 | 11 | 12 | def import_module(name): 13 | try: 14 | exec(f"import {name}") 15 | m = eval(name) 16 | except ImportError: 17 | print("* import failed") 18 | 19 | else: 20 | print(m.__name__) 21 | 22 | 23 | while True: 24 | line = sys.stdin.readline() 25 | if not line: 26 | break 27 | 28 | try: 29 | exec(line) 30 | except SystemExit: 31 | raise 32 | 33 | except Exception: 34 | print("* Exception " + str(sys.exc_info()[1])) 35 | 36 | sys.stdout.flush() 37 | sys.stderr.flush() 38 | -------------------------------------------------------------------------------- /src/py2app/_recipedefs/sphinx.py: -------------------------------------------------------------------------------- 1 | from modulegraph2 import BaseNode 2 | 3 | from .._config import RecipeOptions 4 | from .._modulegraph import ModuleGraph 5 | from .._recipes import recipe 6 | 7 | 8 | @recipe("sphinx", distribution="sphinx", modules=["sphinx"]) 9 | def sphinx(graph: ModuleGraph, options: RecipeOptions) -> None: 10 | m = graph.find_node("sphinx") 11 | if not isinstance(m, BaseNode) or m.filename is None: 12 | return None 13 | 14 | for name in [ 15 | # XXX: Why this list? 16 | "sphinxcontrib.applehelp", 17 | "sphinxcontrib.devhelp", 18 | "sphinxcontrib.htmlhelp", 19 | "sphinxcontrib.jsmath", 20 | "sphinxcontrib.qthelp", 21 | "sphinxcontrib.serializinghtml", 22 | ]: 23 | graph.import_module(m, name) 24 | -------------------------------------------------------------------------------- /src/py2app/recipes/pylsp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | 4 | from modulegraph.modulegraph import ModuleGraph 5 | 6 | from .. import build_app 7 | from ._types import RecipeInfo 8 | 9 | 10 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 11 | m = mf.findNode("pylsp") 12 | if m is None or m.filename is None: 13 | return None 14 | if m.packagepath is None: 15 | return None 16 | 17 | includes = ["pylsp.__main__", "pylsp.python_lsp"] 18 | 19 | root_dir = m.packagepath[0] 20 | files = os.scandir(os.path.join(root_dir, "plugins")) 21 | for file in files: 22 | if file.name.endswith(".py"): 23 | includes.append(".".join(["pylsp", "plugins", file.name[:-3]])) 24 | 25 | return {"includes": includes} 26 | -------------------------------------------------------------------------------- /py2app_tests/basic_app2/main-script: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def function(): 5 | import decimal 6 | 7 | def import_module(name): 8 | try: 9 | exec ("import %s"%(name,)) 10 | m = eval(name) 11 | except ImportError: 12 | print ("* import failed") 13 | 14 | else: 15 | #for k in name.split('.')[1:]: 16 | # m = getattr(m, k) 17 | print (m.__name__) 18 | 19 | def print_path(): 20 | print(sys.path) 21 | 22 | while True: 23 | line = sys.stdin.readline() 24 | if not line: 25 | break 26 | 27 | try: 28 | exec (line) 29 | except SystemExit: 30 | raise 31 | 32 | except Exception: 33 | print ("* Exception " + str(sys.exc_info()[1])) 34 | 35 | sys.stdout.flush() 36 | sys.stderr.flush() 37 | -------------------------------------------------------------------------------- /examples/pygame/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script for building the example. 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | NAME = "aliens" 11 | VERSION = "0.1" 12 | 13 | plist = { 14 | "CFBundleIconFile": NAME, 15 | "CFBundleName": NAME, 16 | "CFBundleShortVersionString": VERSION, 17 | "CFBundleGetInfoString": " ".join([NAME, VERSION]), 18 | "CFBundleExecutable": NAME, 19 | "CFBundleIdentifier": "org.pygame.examples.aliens", 20 | } 21 | 22 | setup( 23 | # data_files=['English.lproj', 'data'], 24 | # app=[ 25 | # dict(script="aliens.py", plist=plist), 26 | # ], 27 | app=["demo.py"], 28 | setup_requires=["py2app"], 29 | options={ 30 | "py2app": { 31 | "arch": "i386", 32 | } 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /py2app_tests/app_with_data/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def function(): 5 | import decimal # noqa: F401 6 | 7 | 8 | def import_module(name): 9 | try: 10 | exec(f"import {name}") 11 | m = eval(name) 12 | except ImportError: 13 | print("* import failed") 14 | 15 | else: 16 | for k in name.split(".")[1:]: 17 | m = getattr(m, k) 18 | print(m.__name__) 19 | 20 | 21 | def print_path(): 22 | print(sys.path) 23 | 24 | 25 | while True: 26 | line = sys.stdin.readline() 27 | if not line: 28 | break 29 | 30 | try: 31 | exec(line) 32 | except SystemExit: 33 | raise 34 | 35 | except Exception: 36 | print("* Exception " + str(sys.exc_info()[1])) 37 | 38 | sys.stdout.flush() 39 | sys.stderr.flush() 40 | -------------------------------------------------------------------------------- /py2app_tests/app_with_scripts/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def function(): 5 | import decimal # noqa: F401 6 | 7 | 8 | def import_module(name): 9 | try: 10 | exec(f"import {name}") 11 | m = eval(name) 12 | except ImportError: 13 | print("* import failed") 14 | 15 | else: 16 | # for k in name.split('.')[1:]: 17 | # m = getattr(m, k) 18 | print(m.__name__) 19 | 20 | 21 | def print_path(): 22 | print(sys.path) 23 | 24 | 25 | while True: 26 | line = sys.stdin.readline() 27 | if not line: 28 | break 29 | 30 | try: 31 | exec(line) 32 | except SystemExit: 33 | raise 34 | 35 | except Exception: 36 | print("* Exception " + str(sys.exc_info()[1])) 37 | 38 | sys.stdout.flush() 39 | sys.stderr.flush() 40 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_plugin/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def function(): 5 | import decimal # noqa: F401 6 | 7 | 8 | def import_module(name): 9 | try: 10 | exec(f"import {name}") 11 | m = eval(name) 12 | except ImportError: 13 | print("* import failed") 14 | 15 | else: 16 | # for k in name.split('.')[1:]: 17 | # m = getattr(m, k) 18 | print(m.__name__) 19 | 20 | 21 | def print_path(): 22 | print(sys.path) 23 | 24 | 25 | while True: 26 | line = sys.stdin.readline() 27 | if not line: 28 | break 29 | 30 | try: 31 | exec(line) 32 | except SystemExit: 33 | raise 34 | 35 | except Exception: 36 | print("* Exception " + str(sys.exc_info()[1])) 37 | 38 | sys.stdout.flush() 39 | sys.stderr.flush() 40 | -------------------------------------------------------------------------------- /py2app_tests/resource_compile_app/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def function(): 5 | import decimal # noqa: F401 6 | 7 | 8 | def import_module(name): 9 | try: 10 | exec(f"import {name}") 11 | m = eval(name) 12 | except ImportError: 13 | print("* import failed") 14 | 15 | else: 16 | for k in name.split(".")[1:]: 17 | m = getattr(m, k) 18 | print(m.__name__) 19 | 20 | 21 | def print_path(): 22 | print(sys.path) 23 | 24 | 25 | while True: 26 | line = sys.stdin.readline() 27 | if not line: 28 | break 29 | 30 | try: 31 | exec(line) 32 | except SystemExit: 33 | raise 34 | 35 | except Exception: 36 | print("* Exception " + str(sys.exc_info()[1])) 37 | 38 | sys.stdout.flush() 39 | sys.stderr.flush() 40 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/boot_app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | SCRIPT_MAP: "dict[str, str]" 4 | DEFAULT_SCRIPT: str 5 | 6 | 7 | def _run() -> None: 8 | global __file__ 9 | import marshal 10 | import site # noqa: F401 11 | import zipfile 12 | 13 | sys.frozen = "macosx_app" # type: ignore 14 | base = sys.py2app_bundle_resources # type: ignore[attr-defined] 15 | 16 | argv0 = sys.py2app_argv0.rsplit("/", 1)[-1] # type: ignore[attr-defined] 17 | script = SCRIPT_MAP.get(argv0, DEFAULT_SCRIPT) # noqa: F821 18 | 19 | path = f"{base}/python-libraries.zip/bundle-scripts/{script}" 20 | sys.argv[0] = __file__ = path 21 | 22 | zf = zipfile.ZipFile(f"{base}/python-libraries.zip", "r") 23 | source = zf.read(f"bundle-scripts/{script}") 24 | 25 | exec(marshal.loads(source[16:]), globals(), globals()) 26 | -------------------------------------------------------------------------------- /src/py2app/_recipedefs/platformdirs.py: -------------------------------------------------------------------------------- 1 | from modulegraph2 import BaseNode 2 | 3 | from .._config import RecipeOptions 4 | from .._modulegraph import ModuleGraph 5 | from .._recipes import recipe 6 | 7 | 8 | @recipe("sphinx", distribution="platformdirs", modules=["platformdirs"]) 9 | def platformdirs(graph: ModuleGraph, options: RecipeOptions) -> None: 10 | """ 11 | Recipe for `platformdirs `_ 12 | """ 13 | m = graph.find_node("platformdirs") 14 | if not isinstance(m, BaseNode) or m.filename is None: 15 | return None 16 | 17 | # The package init dynamically determines which platform 18 | # specific submodule to import. Py2app only runs on 19 | # macOS, so we can hardcode the specific platform module 20 | # to use. 21 | graph.import_module(m, "platformdirs.macos") 22 | -------------------------------------------------------------------------------- /src/py2app/recipes/wx.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from modulegraph.modulegraph import ModuleGraph 4 | 5 | from .. import build_app 6 | from ._types import RecipeInfo 7 | 8 | 9 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 10 | # wx.lib.pubsub tries to be too smart w.r.t. 11 | # the __path__ it uses, include all of it when 12 | # found. 13 | m = mf.findNode("wx.lib.pubsub") 14 | if m is None or m.filename is None: 15 | return None 16 | 17 | include_packages = [ 18 | "wx.lib.pubsub.*", 19 | "wx.lib.pubsub.core.*", 20 | "wx.lib.pubsub.core.arg1.*", 21 | "wx.lib.pubsub.core.kwargs.*", 22 | "wx.lib.pubsub.pubsub1.*", 23 | "wx.lib.pubsub.pubsub2.*", 24 | "wx.lib.pubsub.utils.*", 25 | ] 26 | return {"includes": include_packages} 27 | -------------------------------------------------------------------------------- /py2app_tests/pkg_script_app/quot.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def function(): 5 | import quot # noqa: F401 6 | import quot.queue # noqa: F401 7 | 8 | 9 | def import_module(name): 10 | try: 11 | exec(f"import {name}") 12 | m = eval(name) 13 | except ImportError as exc: 14 | print(f"* import failed: {exc} path: {sys.path}") 15 | 16 | else: 17 | print(m.__name__) 18 | 19 | 20 | def print_path(): 21 | print(sys.path) 22 | 23 | 24 | if __name__ == "__main__": 25 | while True: 26 | line = sys.stdin.readline() 27 | if not line: 28 | break 29 | 30 | try: 31 | exec(line) 32 | except SystemExit: 33 | raise 34 | 35 | except Exception: 36 | print("* Exception " + str(sys.exc_info()[1])) 37 | 38 | sys.stdout.flush() 39 | sys.stderr.flush() 40 | -------------------------------------------------------------------------------- /stubs/modulegraph/find_modules.pyi: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | import typing 4 | from .modulegraph import Node, Extension, ModuleGraph 5 | 6 | PY_SUFFIXES: list[str] 7 | C_SUFFIXES: list[str] 8 | 9 | find_modules, find_needed_modules, parse_mf_results 10 | 11 | def get_implies() -> dict[str, list[str]]: ... 12 | def parse_mf_results(mf: ModuleGraph) -> tuple[list[Node], list[Extension]]: ... 13 | def find_needed_modules( 14 | mf: ModuleGraph | None = None, 15 | scripts: typing.Iterable[str] = (), 16 | includes: typing.Iterable[str] = (), 17 | packages: typing.Iterable[str] = (), 18 | ) -> ModuleGraph: ... 19 | def find_modules( 20 | scripts: typing.Iterable[str] = (), 21 | includes: typing.Iterable[str] = (), 22 | packages: typing.Iterable[str] = (), 23 | excludes: typing.Iterable[str] = (), 24 | path: typing.Sequence[str] | None = None, 25 | debug: int = 0, 26 | ) -> ModuleGraph: ... 27 | -------------------------------------------------------------------------------- /examples/Tkinter/hello_tk/hello.py: -------------------------------------------------------------------------------- 1 | import tkinter 2 | 3 | __version__ = "2.5b3" 4 | 5 | import objc 6 | 7 | print(objc.__version__) 8 | 9 | 10 | class Application(tkinter.Frame): 11 | def say_hi(self): 12 | print("hi there, everyone!") 13 | 14 | def createWidgets(self): 15 | self.QUIT = tkinter.Button(self) 16 | self.QUIT["text"] = "QUIT" 17 | self.QUIT["fg"] = "red" 18 | self.QUIT["command"] = self.quit 19 | 20 | self.QUIT.pack({"side": "left"}) 21 | 22 | self.hi_there = tkinter.Button(self) 23 | self.hi_there["text"] = ("Hello",) 24 | self.hi_there["command"] = self.say_hi 25 | 26 | self.hi_there.pack({"side": "left"}) 27 | 28 | def __init__(self, master=None): 29 | tkinter.Frame.__init__(self, master) 30 | self.pack() 31 | self.createWidgets() 32 | 33 | 34 | app = Application() 35 | app.mainloop() 36 | -------------------------------------------------------------------------------- /examples/PyObjC/ICSharingWatcher/leases.py: -------------------------------------------------------------------------------- 1 | """ 2 | parse a dhcpd leases file 3 | """ 4 | 5 | import itertools 6 | 7 | 8 | def leases(lines): 9 | lines = itertools.imap(lambda s: s.strip(), lines) 10 | for line in lines: 11 | if line == "{": 12 | d = {} 13 | for line in lines: 14 | if line == "}": 15 | yield d 16 | break 17 | if not line: 18 | continue 19 | k, v = line.split("=", 1) 20 | d[k] = v 21 | 22 | 23 | EXAMPLE = """ 24 | { 25 | name=EXAMPLEDATA 26 | ip_address=192.168.2.2 27 | hw_address=1,0:40:63:d6:61:2b 28 | identifier=1,0:40:63:d6:61:2b 29 | lease=0x416213ae 30 | } 31 | """ 32 | 33 | if __name__ == "__main__": 34 | import pprint 35 | 36 | for lease in leases(EXAMPLE.splitlines()): 37 | pprint.pprint(lease) 38 | -------------------------------------------------------------------------------- /src/py2app/_recipedefs/truststore.py: -------------------------------------------------------------------------------- 1 | from modulegraph2 import BaseNode 2 | 3 | from .._config import RecipeOptions 4 | from .._modulegraph import ModuleGraph 5 | from .._recipes import recipe 6 | 7 | 8 | @recipe("truststore", distribution="truststore", modules=["truststore"]) 9 | def truststore(graph: ModuleGraph, options: RecipeOptions) -> None: 10 | """ 11 | Recipe for `platformdirs `_ 12 | """ 13 | m = graph.find_node("truststore._api") 14 | if not isinstance(m, BaseNode) or m.filename is None: 15 | return 16 | 17 | graph.remove_all_edges(m, "truststore._windows") 18 | graph.remove_all_edges(m, "truststore._openssl") 19 | 20 | # 'inject_into_ssl' can integrate with 3th a number 21 | # of libraries, those links can be cut to ensure 22 | # these libraries are only included when the app 23 | # actually uses them. 24 | graph.remove_all_edges(m, "urllib3.util.ssl_") 25 | -------------------------------------------------------------------------------- /py2app_tests/app_with_sharedlib/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from double import double # noqa: F401 4 | from half import half # noqa: F401 5 | from square import square # noqa: F401 6 | 7 | 8 | def function(): 9 | import decimal # noqa: F401 10 | 11 | 12 | def import_module(name): 13 | try: 14 | exec(f"import {name}") 15 | m = eval(name) 16 | except ImportError: 17 | print("* import failed") 18 | 19 | else: 20 | # for k in name.split('.')[1:]: 21 | # m = getattr(m, k) 22 | print(m.__name__) 23 | 24 | 25 | def print_path(): 26 | print(sys.path) 27 | 28 | 29 | while True: 30 | line = sys.stdin.readline() 31 | if not line: 32 | break 33 | 34 | try: 35 | exec(line) 36 | except SystemExit: 37 | raise 38 | 39 | except Exception: 40 | print("* Exception " + str(sys.exc_info()[1])) 41 | 42 | sys.stdout.flush() 43 | sys.stderr.flush() 44 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_encoding/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | - coding: utf-8 - 3 | This is a script with a non-ASCII encoding 4 | 5 | German for Lion is Löwe. 6 | """ 7 | 8 | import sys 9 | 10 | 11 | def function(): 12 | import decimal # noqa: F401 13 | 14 | 15 | def import_module(name): 16 | try: 17 | exec(f"import {name}") 18 | m = eval(name) 19 | except ImportError: 20 | print("* import failed") 21 | 22 | else: 23 | # for k in name.split('.')[1:]: 24 | # m = getattr(m, k) 25 | print(m.__name__) 26 | 27 | 28 | def print_path(): 29 | print(sys.path) 30 | 31 | 32 | while True: 33 | line = sys.stdin.readline() 34 | if not line: 35 | break 36 | 37 | try: 38 | exec(line) 39 | except SystemExit: 40 | raise 41 | 42 | except Exception: 43 | print("* Exception " + str(sys.exc_info()[1])) 44 | 45 | sys.stdout.flush() 46 | sys.stderr.flush() 47 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/_setup_importlib.py: -------------------------------------------------------------------------------- 1 | # Set up loader for extension modules in possibly 2 | # zipped packages. 3 | import importlib 4 | import os 5 | import sys 6 | from importlib.abc import MetaPathFinder 7 | 8 | 9 | class Py2AppExtensionLoader(MetaPathFinder): 10 | def __init__(self, libdir: str) -> None: 11 | self._libdir = libdir 12 | 13 | # XXX: type annations would require importing typing 14 | def find_spec(self, fullname, path, target=None): # type: ignore 15 | ext_path = f"{self._libdir}/{fullname}.so" 16 | if not os.path.exists(ext_path): 17 | return None 18 | 19 | loader = importlib.machinery.ExtensionFileLoader(fullname, ext_path) 20 | return importlib.machinery.ModuleSpec( 21 | name=fullname, loader=loader, origin=ext_path 22 | ) 23 | 24 | 25 | for p in sys.path: 26 | if p.endswith("/lib-dynload"): 27 | sys.meta_path.insert(0, Py2AppExtensionLoader(p)) 28 | -------------------------------------------------------------------------------- /py2app_tests/basic_app/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def function(): 5 | import decimal # noqa: F401 6 | 7 | 8 | def import_module(name): 9 | try: 10 | exec(f"import {name}") 11 | m = eval(name) 12 | except ImportError: 13 | print("* import failed") 14 | 15 | else: 16 | # for k in name.split('.')[1:]: 17 | # m = getattr(m, k) 18 | print(m.__name__) 19 | 20 | 21 | def print_path(): 22 | print(sys.path) 23 | 24 | 25 | def run_python(): 26 | import subprocess 27 | 28 | p = subprocess.Popen([sys.executable, "-c", 'print("ok")']) 29 | p.wait() 30 | 31 | 32 | while True: 33 | line = sys.stdin.readline() 34 | if not line: 35 | break 36 | 37 | try: 38 | exec(line) 39 | except SystemExit: 40 | raise 41 | 42 | except Exception: 43 | print("* Exception " + str(sys.exc_info()[1])) 44 | 45 | sys.stdout.flush() 46 | sys.stderr.flush() 47 | -------------------------------------------------------------------------------- /src/py2app/converters/coredata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Automatic compilation of CoreData model files 3 | """ 4 | 5 | import os 6 | import typing 7 | 8 | from py2app.util import mapc, momc 9 | 10 | 11 | def convert_datamodel( 12 | source: typing.Union[os.PathLike[str], str], 13 | destination: typing.Union[os.PathLike[str], str], 14 | dry_run: bool = False, 15 | ) -> None: 16 | """ 17 | Convert an .xcdatamodel to a .mom 18 | """ 19 | destination = os.path.splitext(destination)[0] + ".mom" 20 | 21 | if dry_run: 22 | return 23 | 24 | momc(source, destination) 25 | 26 | 27 | def convert_mappingmodel( 28 | source: typing.Union[os.PathLike[str], str], 29 | destination: typing.Union[os.PathLike[str], str], 30 | dry_run: bool = False, 31 | ) -> None: 32 | """ 33 | Convert an .xcmappingmodel to a .cdm 34 | """ 35 | destination = os.path.splitext(destination)[0] + ".cdm" 36 | 37 | if dry_run: 38 | return 39 | 40 | mapc(source, destination) 41 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/setup_included_subpackages.py: -------------------------------------------------------------------------------- 1 | # mypy: ignore-errors 2 | 3 | import imp 4 | import os 5 | import sys 6 | import types 7 | 8 | _path_hooks: "list[str]" 9 | 10 | 11 | # XXX: Is this function used at all? 12 | def _included_subpackages(packages: "list[str]") -> None: 13 | for _pkg in packages: 14 | pass 15 | 16 | 17 | class Loader: 18 | def load_module(self, fullname: str) -> types.ModuleType: 19 | pkg_dir = os.path.join( 20 | os.environ["RESOURCEPATH"], "lib", "python%d.%d" % (sys.version_info[:2]) 21 | ) 22 | return imp.load_module( 23 | fullname, None, os.path.join(pkg_dir, fullname), ("", "", imp.PKG_DIRECTORY) 24 | ) 25 | 26 | 27 | class Finder: 28 | def find_module( 29 | self, fullname: str, path: "list[str]|None" = None 30 | ) -> "Loader|None": 31 | if fullname in _path_hooks: # noqa: F821 32 | return Loader() 33 | return None 34 | 35 | 36 | sys.meta_path.insert(0, Finder()) # type: ignore 37 | -------------------------------------------------------------------------------- /src/py2app/recipes/__init__.py: -------------------------------------------------------------------------------- 1 | from . import PIL # noqa: F401 2 | from . import black # noqa: F401 3 | from . import gcloud # noqa: F401 4 | from . import lxml # noqa: F401 5 | from . import matplotlib # noqa: F401 6 | from . import multiprocessing # noqa: F401 7 | from . import pandas # noqa: F401 8 | from . import pydantic # noqa: F401 9 | from . import pyenchant # noqa: F401 10 | from . import pygame # noqa: F401 11 | from . import pylsp # noqa: F401 12 | from . import pyopengl # noqa: F401 13 | from . import pyside # noqa: F401 14 | from . import pyside2 # noqa: F401 15 | from . import pyside6 # noqa: F401 16 | from . import qt5 # noqa: F401 17 | from . import qt6 # noqa: F401 18 | from . import setuptools # noqa: F401 19 | from . import shiboken2 # noqa: F401 20 | from . import shiboken6 # noqa: F401 21 | from . import sip # noqa: F401 22 | from . import sphinx # noqa: F401 23 | from . import sqlalchemy # noqa: F401 24 | from . import virtualenv # noqa: F401 25 | from . import wx # noqa: F401 26 | from . import zmq # noqa: F401 27 | -------------------------------------------------------------------------------- /stubs/macholib/MachOGraph.pyi: -------------------------------------------------------------------------------- 1 | import typing 2 | from .MachO import MachO 3 | 4 | class MachOGraph: 5 | env: typing.Optional[typing.Dict[str, str]] 6 | executable_path: typing.Optional[str] 7 | 8 | def __init__( 9 | self, 10 | debug: int = 0, 11 | graph: None = None, 12 | env: typing.Optional[typing.Dict[str, str]] = None, 13 | executable_path: typing.Optional[str] = None, 14 | ) -> None: ... 15 | def locate( 16 | self, filename: str, loader: typing.Optional[str] = None 17 | ) -> typing.Optional[str]: ... 18 | def findNode( 19 | self, name: str, loader: typing.Optional[str] = None 20 | ) -> typing.Optional[MachO]: ... 21 | def run_file(self, pathname: str, caller: typing.Optional[str] = None) -> MachO: ... 22 | def load_file( 23 | self, name: str, loader: typing.Optional[str] = None 24 | ) -> typing.Optional[MachO]: ... 25 | def scan_node(self, node: MachO) -> None: ... 26 | def graphreport(self, fileobj: typing.Optional[typing.IO[str]] = None) -> None: ... 27 | -------------------------------------------------------------------------------- /src/py2app/_stubs/__main__.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | import sysconfig 4 | 5 | from .._config import BuildArch 6 | from . import LauncherType, copy_launcher 7 | 8 | 9 | def build_executable_cache() -> None: 10 | """ 11 | Build the cached executables for the current python release 12 | """ 13 | 14 | for program_type in LauncherType: 15 | deployment_target = sysconfig.get_config_var("MACOSX_DEPLOYMENT_TARGET") 16 | assert deployment_target is not None 17 | 18 | for arch in BuildArch: 19 | fn = ( 20 | pathlib.Path(__file__).parent 21 | / f"launcher-{arch.value}-{sys.abiflags}-{program_type.value}" 22 | ) 23 | fn.unlink(missing_ok=True) 24 | print(f"Generate {fn.name}") 25 | copy_launcher( 26 | fn, 27 | arch=arch, 28 | deployment_target=deployment_target, 29 | program_type=program_type, 30 | ) 31 | 32 | 33 | if __name__ == "__main__": 34 | build_executable_cache() 35 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Installing with pip 5 | ------------------- 6 | 7 | To install py2app using `pip`_, or to upgrade to the latest released version 8 | of py2app: 9 | 10 | .. code-block:: console 11 | 12 | $ pip3 install -U py2app 13 | 14 | Setuptools support in py2app is optional, to force the installation 15 | of `setuptools`_ install the setuptools extra: 16 | 17 | .. code-block:: console 18 | 19 | $ pip install -U 'py2app[setuptools]' 20 | 21 | Note that `setuptools`_ is installed by default in most Python 22 | installations and virtual environments, which means that 23 | the default installation command will likely work even when 24 | using the legacy setuptools interface of py2app. 25 | 26 | Installing from source 27 | ---------------------- 28 | 29 | The preferred way to install py2app from source is to 30 | invoke pip in the root of the py2app source directory: 31 | 32 | .. code-block:: console 33 | 34 | $ pip install . 35 | 36 | .. _`setuptools`: http://pypi.python.org/pypi/setuptools/ 37 | .. _`pip`: http://www.pip-installer.org/en/latest/ 38 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/_disable_linecache.py: -------------------------------------------------------------------------------- 1 | def _disable_linecache() -> None: 2 | import linecache 3 | 4 | def fake_getline( 5 | filename: str, lineno: int, module_globals: "dict|None" = None 6 | ) -> str: 7 | return "" 8 | 9 | def fake_getlines(filename: str, module_globals: "dict|None" = None) -> "list[str]": 10 | return [] 11 | 12 | def fake_checkcache(filename: "str|None" = None) -> None: 13 | return 14 | 15 | def fake_updatecache( 16 | filename: str, module_globals: "dict|None" = None 17 | ) -> "list[str]": 18 | return [] 19 | 20 | linecache.orig_getline = linecache.getline # type: ignore 21 | linecache.getline = fake_getline 22 | 23 | linecache.orig_getlines = linecache.getlines # type: ignore 24 | linecache.getlines = fake_getlines 25 | 26 | linecache.orig_checkcache = linecache.checkcache # type: ignore 27 | linecache.checkcache = fake_checkcache 28 | 29 | linecache.orig_updatecache = linecache.updatecache # type: ignore 30 | linecache.updatecache = fake_updatecache 31 | 32 | 33 | _disable_linecache() 34 | -------------------------------------------------------------------------------- /src/py2app/recipes/pydantic.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from modulegraph.modulegraph import ModuleGraph 4 | 5 | from .. import build_app 6 | from ._types import RecipeInfo 7 | 8 | PYDANTIC_IMPORTS = [ 9 | "abc", 10 | "collections", 11 | "collections.abc", 12 | "colorsys", 13 | "configparser", 14 | "contextlib", 15 | "copy", 16 | "dataclasses", 17 | "datetime", 18 | "decimal", 19 | "enum", 20 | "fractions", 21 | "functools", 22 | "ipaddress", 23 | "itertools", 24 | "json", 25 | "math", 26 | "os", 27 | "pathlib", 28 | "pickle", 29 | "re", 30 | "sys", 31 | "types", 32 | "typing", 33 | "typing_extensions", 34 | "uuid", 35 | "warnings", 36 | "weakref", 37 | ] 38 | 39 | 40 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 41 | m = mf.findNode("pydantic") 42 | if m is None or m.filename is None: 43 | return None 44 | 45 | # Pydantic Cython and therefore hides imports from the 46 | # modulegraph machinery 47 | return {"packages": ["pydantic"], "includes": PYDANTIC_IMPORTS} 48 | -------------------------------------------------------------------------------- /py2app_tests/app_with_scripts/src/modfoo.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | #include "libfoo.h" 3 | 4 | 5 | static PyMethodDef mod_methods[] = { 6 | { 0, 0, 0, 0} 7 | }; 8 | 9 | #if PY_VERSION_HEX >= 0x03000000 10 | 11 | static struct PyModuleDef mod_module = { 12 | PyModuleDef_HEAD_INIT, 13 | "foo", 14 | NULL, 15 | 0, 16 | mod_methods, 17 | NULL, 18 | NULL, 19 | NULL, 20 | NULL 21 | }; 22 | 23 | #define INITERROR() return NULL 24 | #define INITDONE() return m 25 | 26 | PyObject* PyInit_foo(void); 27 | 28 | PyObject* 29 | PyInit_foo(void) 30 | 31 | #else 32 | 33 | #define INITERROR() return 34 | #define INITDONE() return 35 | 36 | void initfoo(void); 37 | 38 | void 39 | initfoo(void) 40 | #endif 41 | { 42 | PyObject* m; 43 | 44 | #if PY_VERSION_HEX >= 0x03000000 45 | m = PyModule_Create(&mod_module); 46 | #else 47 | m = Py_InitModule4("foo", mod_methods, 48 | NULL, NULL, PYTHON_API_VERSION); 49 | #endif 50 | if (!m) { 51 | INITERROR(); 52 | } 53 | 54 | if (PyModule_AddIntConstant(m, "sq_1", square(1))) { 55 | INITERROR(); 56 | } 57 | 58 | if (PyModule_AddIntConstant(m, "sq_2", square(2))) { 59 | INITERROR(); 60 | } 61 | 62 | INITDONE(); 63 | } 64 | -------------------------------------------------------------------------------- /src/py2app/converters/nibfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Automatic compilation of XIB files 3 | """ 4 | 5 | import os 6 | import subprocess 7 | import typing 8 | 9 | from py2app.util import get_tool, reset_blocking_status 10 | 11 | gTool = None 12 | 13 | 14 | def convert_xib( 15 | source: typing.Union[os.PathLike[str], str], 16 | destination: typing.Union[os.PathLike[str], str], 17 | dry_run: bool = False, 18 | ) -> None: 19 | destination = os.fspath(destination)[:-4] + ".nib" 20 | 21 | print(f"compile {source} -> {destination}") 22 | if dry_run: 23 | return 24 | 25 | with reset_blocking_status(): 26 | subprocess.check_call([get_tool("ibtool"), "--compile", destination, source]) 27 | 28 | 29 | def convert_nib( 30 | source: typing.Union[os.PathLike[str], str], 31 | destination: typing.Union[os.PathLike[str], str], 32 | dry_run: bool = False, 33 | ) -> None: 34 | destination = os.fspath(destination)[:-4] + ".nib" 35 | print(f"compile {source} -> {destination}") 36 | 37 | if dry_run: 38 | return 39 | 40 | with reset_blocking_status(): 41 | subprocess.check_call([get_tool("ibtool"), "--compile", destination, source]) 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is the MIT license. This software may also be distributed under the same terms as Python (the PSF license). 2 | 3 | Copyright (c) 2004 Bob Ippolito. 4 | 5 | Copyright (c) 2010-2024 Ronald Oussoren 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /py2app_tests/test_recipe_imports.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import unittest 4 | 5 | 6 | class TestRecipeImports(unittest.TestCase): 7 | def test_imports(self): 8 | import py2app.recipes as m 9 | 10 | dirname = os.path.dirname(m.__file__) 11 | 12 | all_imported = set() 13 | for fn in os.listdir(dirname): 14 | if fn.startswith("__"): 15 | continue 16 | if fn.endswith(".py"): 17 | with open(os.path.join(dirname, fn)) as fp: 18 | for ln in fp: 19 | m = re.search(r"^\s*import (.*)", ln) 20 | if m is not None: 21 | for nm in m.group(1).split(","): 22 | all_imported.add(nm.strip()) 23 | 24 | for fn in os.listdir(dirname): 25 | if fn.startswith("__"): 26 | continue 27 | 28 | mod = os.path.splitext(fn)[0] 29 | if mod not in all_imported: 30 | continue 31 | 32 | try: 33 | m = __import__(mod) 34 | except ImportError: 35 | pass 36 | 37 | else: 38 | 39 | self.fail(f"Can import {mod!r}") 40 | -------------------------------------------------------------------------------- /py2app_tests/app_with_shared_ctypes/main.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import ctypes.util 3 | import sys 4 | 5 | 6 | def find_library(name): 7 | print(ctypes.util.find_library(name)) 8 | 9 | 10 | mod = None 11 | 12 | 13 | def _load(): 14 | global mod 15 | if mod is None: 16 | mod = ctypes.CDLL(ctypes.util.find_library("libshared.dylib")) 17 | return mod 18 | 19 | 20 | def half(v): 21 | return _load().half(v) 22 | 23 | 24 | def double(v): 25 | return _load().doubled(v) 26 | 27 | 28 | def square(v): 29 | return _load().squared(v) 30 | 31 | 32 | def import_module(name): 33 | try: 34 | exec(f"import {name}") 35 | m = eval(name) 36 | except ImportError: 37 | print("* import failed") 38 | 39 | else: 40 | # for k in name.split('.')[1:]: 41 | # m = getattr(m, k) 42 | print(m.__name__) 43 | 44 | 45 | def print_path(): 46 | print(sys.path) 47 | 48 | 49 | while True: 50 | line = sys.stdin.readline() 51 | if not line: 52 | break 53 | 54 | try: 55 | exec(line) 56 | except SystemExit: 57 | raise 58 | 59 | except Exception: 60 | print("* Exception " + str(sys.exc_info()[1])) 61 | 62 | sys.stdout.flush() 63 | sys.stderr.flush() 64 | -------------------------------------------------------------------------------- /src/py2app/progress.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic progress reporting and logging 3 | 4 | The interface is a work in progress, and might 5 | be dropped later in favour of direct usage of 6 | rich.progress 7 | """ 8 | 9 | import rich.progress 10 | 11 | 12 | class Progress: 13 | def __init__(self, level: int = 2) -> None: 14 | # XXX: Reduce the default level after finding 15 | # a nicer way to report progress on 16 | # copying files. 17 | self._progress = rich.progress.Progress() 18 | self._progress.start() 19 | self._level = level 20 | 21 | def stop(self) -> None: 22 | self._progress.stop() 23 | 24 | def add_task(self, name: str, count: int) -> rich.progress.TaskID: 25 | return self._progress.add_task(name, total=count) 26 | 27 | def step_task(self, task_id: rich.progress.TaskID) -> None: 28 | self._progress.advance(task_id) 29 | 30 | def info(self, message: str) -> None: 31 | if self._level >= 1: 32 | self._progress.print(message) 33 | 34 | def trace(self, message: str) -> None: 35 | if self._level >= 2: 36 | self._progress.print(message) 37 | 38 | def warning(self, message: str) -> None: 39 | self._progress.print(f"[red]{message}[/red]") 40 | -------------------------------------------------------------------------------- /py2app_tests/tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import subprocess 4 | 5 | 6 | def kill_child_processes(): 7 | parent_pid = os.getpid() 8 | sig = signal.SIGKILL 9 | ps_command = subprocess.Popen( 10 | "ps -o pid,ppid -ax", shell=True, stdout=subprocess.PIPE 11 | ) 12 | ps_output = ps_command.stdout.read().decode("utf-8") 13 | ps_command.wait() 14 | ps_command.stdout.close() 15 | for line in ps_output.splitlines(): 16 | pid, ppid = line.split() 17 | if ppid != parent_pid: 18 | continue 19 | try: 20 | os.kill(int(pid), sig) 21 | except OSError: 22 | pass 23 | 24 | ps_command = subprocess.Popen("ps -ax", shell=True, stdout=subprocess.PIPE) 25 | ps_output = ps_command.stdout.read() 26 | ps_command.wait() 27 | ps_command.stdout.close() 28 | ps_output = ps_output.decode("utf-8") 29 | 30 | my_dir = os.path.dirname(__file__) + "/" 31 | for line in ps_output.splitlines(): 32 | if my_dir in line: 33 | pid, _ = line.split(None, 1) 34 | try: 35 | os.kill(int(pid), sig) 36 | except OSError: 37 | pass 38 | 39 | try: 40 | os.waitpid(0, 0) 41 | except OSError: 42 | pass 43 | -------------------------------------------------------------------------------- /doc/environment.rst: -------------------------------------------------------------------------------- 1 | Environment in launched applications 2 | ==================================== 3 | 4 | 5 | Environment variables added by py2app 6 | ------------------------------------- 7 | 8 | * ``RESOURCEPATH`` 9 | 10 | Filesystem path for the "Resources" folder inside the application bundle 11 | 12 | 13 | System environment 14 | ------------------ 15 | 16 | When the application is launched normally (double clicking in the Finder, 17 | using the ``open(1)`` command) the application will be launched with a minimal 18 | shell environment, which does not pick up changes to the environment in the 19 | user's shell profile. 20 | 21 | The "emulate_shell_environment" option will run a login shell in the background 22 | to fetch exported environment variables and inject them into your application. 23 | 24 | It is also possible to inject extra variables into the environment by using 25 | the ``LSEnvironment`` key in the Info.plist file, for example like so: 26 | 27 | .. sourcecode:: toml 28 | :caption: pyproject configuration for changing the app environment 29 | 30 | [tool.py2app.bundle.main] 31 | name = "BasicApp" 32 | script = "main.py" 33 | 34 | [tool.py2app.bundle.main.plist.LSEnvironment] 35 | LANG = "nl_NL.latin1" 36 | LC_CTYPE = "nl_NL.UTF-8" 37 | EXTRA_VAR = "hello world" 38 | KNIGHT = "ni!" 39 | -------------------------------------------------------------------------------- /src/py2app/recipes/pyenchant.py: -------------------------------------------------------------------------------- 1 | """ 2 | Recipe for pyEnchant 3 | 4 | PyEnchant is a python library that wraps a C library 5 | using ctypes, hence the usual way to find the library 6 | won't work. 7 | """ 8 | 9 | import os 10 | import typing 11 | 12 | from modulegraph.modulegraph import ModuleGraph 13 | 14 | from .. import build_app 15 | from ._types import RecipeInfo 16 | 17 | 18 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 19 | m = mf.findNode("enchant") 20 | if m is None or m.filename is None: 21 | return None 22 | 23 | if "PYENCHANT_LIBRARY_PATH" in os.environ: 24 | # libpath = os.environ['PYENCHANT_LIBRARY_PATH'] 25 | print("WARNING: using pyEnchant without embedding") 26 | print("WARNING: this is not supported at the moment") 27 | 28 | else: 29 | path = os.path.dirname(m.filename) 30 | if not os.path.exists(os.path.join(path, "lib", "libenchant.1.dylib")): 31 | print("WARNING: using pyEnchant without embedding") 32 | print("WARNING: this is not supported at the moment") 33 | 34 | # Include the entire package outside of site-packages.zip, 35 | # mostly to avoid trying to extract the C code from the package 36 | return {"packages": ["enchant"]} 37 | -------------------------------------------------------------------------------- /src/py2app/recipes/lxml.py: -------------------------------------------------------------------------------- 1 | # 2 | # LXML uses imports from C code (or actually Cython code) 3 | # and those cannot be detected by modulegraph. 4 | # The check function adds the hidden imports to the graph 5 | # 6 | # The dependency list was extracted from lxml 3.0.2 7 | import sys 8 | import typing 9 | 10 | from modulegraph.modulegraph import ModuleGraph 11 | 12 | from .. import build_app 13 | from ._types import RecipeInfo 14 | 15 | 16 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 17 | m = mf.findNode("lxml.etree") 18 | if m is not None and m.filename is not None: 19 | mf.import_hook("lxml._elementpath", m) 20 | mf.import_hook("os.path", m) 21 | mf.import_hook("re", m) 22 | mf.import_hook("gzip", m) 23 | mf.import_hook("io", m) 24 | 25 | m = mf.findNode("lxml.objectify") 26 | if m is not None and m.filename is not None: 27 | if sys.version_info[0] == 2: 28 | mf.import_hook("copy_reg", m) 29 | else: 30 | mf.import_hook("copyreg", m) 31 | 32 | m = mf.findNode("lxml.isoschematron") 33 | if m is not None and m.filename is not None: 34 | # Not zip-safe (see issue 118) 35 | return {"packages": ["lxml"]} 36 | 37 | if mf.findNode("lxml") is None: 38 | return None 39 | 40 | return {} 41 | -------------------------------------------------------------------------------- /src/py2app/recipes/PIL/prescript.py: -------------------------------------------------------------------------------- 1 | # Recipe assumes users migrated to 2 | # "Pillow" which always installs as 3 | # "PIL.Image" 4 | 5 | 6 | def _recipes_pil_prescript(plugins: "list[str]") -> None: 7 | import sys 8 | 9 | from PIL import Image 10 | 11 | def init() -> None: 12 | if Image._initialized >= 2: # type: ignore 13 | return 14 | 15 | try: 16 | import PIL.JpegPresets 17 | 18 | sys.modules["JpegPresets"] = PIL.JpegPresets 19 | except ImportError: 20 | pass 21 | 22 | for plugin in plugins: 23 | try: 24 | try: 25 | # First try absolute import through PIL (for 26 | # Pillow support) only then try relative imports 27 | m = __import__("PIL." + plugin, globals(), locals(), []) 28 | m = getattr(m, plugin) 29 | sys.modules[plugin] = m 30 | continue 31 | except ImportError: 32 | pass 33 | 34 | __import__(plugin, globals(), locals(), []) 35 | except ImportError: 36 | print("Image: failed to import") 37 | 38 | if Image.OPEN or Image.SAVE: 39 | Image._initialized = 2 # type: ignore 40 | return 41 | return 42 | 43 | Image.init = init 44 | -------------------------------------------------------------------------------- /py2app_tests/app_with_sharedlib/mod.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | #include "sharedlib.h" 3 | 4 | #define _STR(x) #x 5 | #define STR(x) _STR(x) 6 | 7 | static PyObject* 8 | mod_function(PyObject* mod __attribute__((__unused__)), PyObject* arg) 9 | { 10 | int value = PyLong_AsLong(arg); 11 | if (PyErr_Occurred()) { 12 | return NULL; 13 | } 14 | value = FUNC_NAME(value); 15 | return PyLong_FromLong(value); 16 | } 17 | 18 | static PyMethodDef mod_methods[] = { 19 | { 20 | STR(NAME), 21 | (PyCFunction)mod_function, 22 | METH_O, 23 | 0 24 | }, 25 | { 0, 0, 0, 0 } 26 | }; 27 | 28 | #if PY_VERSION_HEX >= 0x03000000 29 | 30 | static struct PyModuleDef mod_module = { 31 | PyModuleDef_HEAD_INIT, 32 | STR(NAME), 33 | NULL, 34 | 0, 35 | mod_methods, 36 | NULL, 37 | NULL, 38 | NULL, 39 | NULL 40 | }; 41 | 42 | #define INITERROR() return NULL 43 | #define INITDONE() return m 44 | 45 | PyObject* INITFUNC(void); 46 | 47 | PyObject* 48 | INITFUNC(void) 49 | 50 | #else 51 | 52 | #define INITERROR() return 53 | #define INITDONE() return 54 | 55 | 56 | void INITFUNC(void); 57 | 58 | void 59 | INITFUNC(void) 60 | #endif 61 | 62 | { 63 | PyObject* m; 64 | 65 | 66 | #if PY_VERSION_HEX >= 0x03000000 67 | m = PyModule_Create(&mod_module); 68 | #else 69 | m = Py_InitModule4(STR(NAME), mod_methods, 70 | NULL, NULL, PYTHON_API_VERSION); 71 | #endif 72 | if (!m) { 73 | INITERROR(); 74 | } 75 | 76 | INITDONE(); 77 | } 78 | -------------------------------------------------------------------------------- /examples/PyObjC/ICSharingWatcher/TableModelAppDelegate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import leases 4 | import objc 5 | from Cocoa import NSApp, NSObject, NSTimer 6 | 7 | FILENAME = "/var/db/dhcpd_leases" 8 | 9 | 10 | def getLeases(fn): 11 | if os.path.exists(fn): 12 | lines = open(fn) 13 | else: 14 | lines = leases.EXAMPLE.splitlines() 15 | return list(leases.leases(lines)) 16 | 17 | 18 | class TableModelAppDelegate(NSObject): 19 | mainWindow = objc.IBOutlet() 20 | 21 | def awakeFromNib(self): 22 | self.timer = ( 23 | NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( 24 | 1.0, self, "pollLeases:", {}, True 25 | ) 26 | ) 27 | 28 | def pollLeases_(self, timer): 29 | if not os.path.exists(FILENAME): 30 | return 31 | d = timer.userInfo() 32 | newtime = os.stat(FILENAME).st_mtime 33 | oldtime = d.get("st_mtime", 0) 34 | if newtime > oldtime: 35 | d["st_mtime"] = newtime 36 | self.setLeases_(getLeases(FILENAME)) 37 | 38 | def leases(self): 39 | if not hasattr(self, "_cachedleases"): 40 | self._cachedleases = getLeases(FILENAME) 41 | return self._cachedleases 42 | 43 | def setLeases_(self, leases): 44 | self._cachedleases = leases 45 | 46 | def windowWillClose_(self, sender): 47 | if sender is self.mainWindow: 48 | NSApp().terminate() 49 | -------------------------------------------------------------------------------- /examples/pygame/aliens_bootstrap.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSApp, NSRunAlertPanel, NSTerminateNow 2 | from Foundation import NSObject 3 | from PyObjCTools import AppHelper 4 | 5 | 6 | def exception_handler(): 7 | import os 8 | import sys 9 | import traceback 10 | 11 | typ, info, trace = sys.exc_info() 12 | if typ in (KeyboardInterrupt, SystemExit): 13 | return 14 | tracetop = traceback.extract_tb(trace)[-1] 15 | tracetext = "File %s, Line %d" % tracetop[:2] 16 | if tracetop[2] != "?": 17 | tracetext += ", Function %s" % tracetop[2] 18 | exception_message = '%s:\n%s\n\n%s\n"%s"' 19 | message = exception_message % (str(type), str(info), tracetext, tracetop[3]) 20 | title = os.path.splitext(os.path.basename(sys.argv[0]))[0] 21 | title = title.capitalize() + " Error" 22 | NSRunAlertPanel(title, message, None, None, None) 23 | 24 | 25 | class PygameAppDelegate(NSObject): 26 | def applicationDidFinishLaunching_(self, aNotification): 27 | try: 28 | import aliens 29 | 30 | aliens.main() 31 | except: # noqa: E722, B001 32 | exception_handler() 33 | NSApp().terminate_(self) 34 | 35 | def applicationShouldTerminate_(self, app): 36 | import pygame 37 | import pygame.event 38 | 39 | pygame.event.post(pygame.event.Event(pygame.QUIT)) 40 | return NSTerminateNow 41 | 42 | 43 | if __name__ == "__main__": 44 | AppHelper.runEventLoop() 45 | -------------------------------------------------------------------------------- /py2app_tests/test_filters.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from py2app import filters 5 | 6 | 7 | class Node: 8 | def __init__(self, filename): 9 | self.filename = filename 10 | 11 | 12 | def return_true(value): 13 | return True 14 | 15 | 16 | def return_false(value): 17 | return False 18 | 19 | 20 | class FilterTest(unittest.TestCase): 21 | def test_not_stdlib_filter(self): 22 | prefix = sys.prefix 23 | 24 | # Outside the tree: 25 | self.assertTrue(filters.not_stdlib_filter(Node("/foo/bar"))) 26 | self.assertTrue(filters.not_stdlib_filter(Node(prefix + "rest"))) 27 | 28 | # Site-specific directories within sys.prefix: 29 | self.assertTrue( 30 | filters.not_stdlib_filter(Node(prefix + "/lib/site-packages/foo.py")) 31 | ) 32 | self.assertTrue( 33 | filters.not_stdlib_filter(Node(prefix + "/lib/site-python/foo.py")) 34 | ) 35 | 36 | # Inside the tree: 37 | self.assertFalse(filters.not_stdlib_filter(Node(prefix + "/foo.py"))) 38 | 39 | def test_not_system_filter(self): 40 | cur_func = filters.in_system_path 41 | try: 42 | filters.in_system_path = return_true 43 | self.assertFalse(filters.not_system_filter(Node("/tmp/foo"))) 44 | 45 | filters.in_system_path = return_false 46 | self.assertTrue(filters.not_system_filter(Node("/tmp/foo"))) 47 | finally: 48 | filters.in_system_path = cur_func 49 | -------------------------------------------------------------------------------- /src/py2app/recipes/multiprocessing.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | import typing 3 | from io import StringIO 4 | 5 | from modulegraph.modulegraph import ModuleGraph 6 | 7 | from .. import build_app 8 | from ._types import RecipeInfo 9 | 10 | 11 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 12 | m = mf.findNode("multiprocessing") 13 | if m is None: 14 | return None 15 | 16 | # This is a fairly crude hack to get multiprocessing to do the 17 | # right thing without user visible changes in py2app. 18 | # In particular: multiprocessing assumes that a special command-line 19 | # should be used when "sys.frozen" is set. That is not necessary 20 | # with py2app even though it does set "sys.frozen". 21 | prescript = textwrap.dedent( 22 | """\ 23 | def _boot_multiprocessing(): 24 | import sys 25 | import multiprocessing.spawn 26 | 27 | orig_get_command_line = multiprocessing.spawn.get_command_line 28 | def wrapped_get_command_line(**kwargs): 29 | orig_frozen = sys.frozen 30 | del sys.frozen 31 | try: 32 | return orig_get_command_line(**kwargs) 33 | finally: 34 | sys.frozen = orig_frozen 35 | multiprocessing.spawn.get_command_line = wrapped_get_command_line 36 | 37 | _boot_multiprocessing() 38 | """ 39 | ) 40 | 41 | return {"prescripts": [StringIO(prescript)]} 42 | -------------------------------------------------------------------------------- /src/py2app/recipes/sqlalchemy.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from modulegraph.modulegraph import ModuleGraph 4 | 5 | from .. import build_app 6 | from ._types import RecipeInfo 7 | 8 | # __import__ in sqlalchemy.dialects 9 | ENGINE_DEPS = { 10 | "asyncpg": ("asyncpg",), 11 | "psycopg2cffi": ("psycopg2cffi",), 12 | "pg8000": ("pg8000",), 13 | "firebird": ("sqlalchemy_firebird",), 14 | "sybase": "sqlalchemy_sybase", 15 | "aiosqlite": ("aiosqlite", "sqlite3"), 16 | "oursql": ("oursql",), 17 | "aiomysql": ("oursql", "pymysql"), 18 | "mariadb": ("mariadb",), 19 | "mysqldb": ("MySQLdb",), 20 | "cymysql": ("cymysql",), 21 | "pymssql": ("pymssql",), 22 | "fdb": ("fdb",), 23 | "kinterbasdb": ("kinterbasdb",), 24 | } 25 | 26 | # __import__ in sqlalchemy.connectors 27 | CONNECTOR_DEPS = { 28 | "pyodbc": ("pyodbc",), 29 | } 30 | 31 | 32 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 33 | m = mf.findNode("sqlalchemy") 34 | if m is None or m.filename is None: 35 | return None 36 | 37 | for deps in ENGINE_DEPS.values(): 38 | for mod in deps: 39 | try: 40 | mf.import_hook(mod, m) 41 | except ImportError: 42 | pass 43 | 44 | for deps in CONNECTOR_DEPS.values(): 45 | for mod in deps: 46 | try: 47 | mf.import_hook(mod, m) 48 | except ImportError: 49 | pass 50 | 51 | return {"packages": ["sqlalchemy"]} 52 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/virtualenv.py: -------------------------------------------------------------------------------- 1 | def _fixup_virtualenv(real_prefix: str) -> None: 2 | import os 3 | import sys 4 | 5 | sys.real_prefix = real_prefix # type: ignore 6 | 7 | # NOTE: The adjustment code is based from logic in the site.py 8 | # installed by virtualenv 1.8.2 (but simplified by removing support 9 | # for platforms that aren't supported by py2app) 10 | 11 | paths = [os.path.join(real_prefix, "lib", "python%d.%d" % (sys.version_info[:2]))] 12 | hardcoded_relative_dirs = paths[:] 13 | plat_path = os.path.join( 14 | real_prefix, 15 | "lib", 16 | "python%d.%d" % (sys.version_info[:2]), 17 | "plat-%s" % sys.platform, 18 | ) 19 | if os.path.exists(plat_path): 20 | paths.append(plat_path) 21 | 22 | # This is hardcoded in the Python executable, but 23 | # relative to sys.prefix, so we have to fix up: 24 | for path in list(paths): 25 | tk_dir = os.path.join(path, "lib-tk") 26 | if os.path.exists(tk_dir): 27 | paths.append(tk_dir) 28 | 29 | # These are hardcoded in the Apple's Python executable, 30 | # but relative to sys.prefix, so we have to fix them up: 31 | hardcoded_paths = [ 32 | os.path.join(relative_dir, module) 33 | for relative_dir in hardcoded_relative_dirs 34 | for module in ("plat-darwin", "plat-mac", "plat-mac/lib-scriptpackages") 35 | ] 36 | 37 | for path in hardcoded_paths: 38 | if os.path.exists(path): 39 | paths.append(path) 40 | 41 | sys.path.extend(paths) 42 | -------------------------------------------------------------------------------- /py2app_tests/test_setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for typechecking of input arguments 3 | """ 4 | 5 | import os 6 | import unittest 7 | 8 | from setuptools import Distribution 9 | 10 | from py2app.build_app import py2app as py2app_cmd 11 | 12 | 13 | class TestSetupArguments(unittest.TestCase): 14 | def create_cmd(self, **kwds): 15 | dist = Distribution(kwds) 16 | cmd = py2app_cmd(dist) 17 | cmd.dist_dir = "dist" 18 | cmd.fixup_distribution() 19 | cmd.finalize_options() 20 | 21 | return cmd 22 | 23 | def test_version(self): 24 | # Explicit version 25 | cmd = self.create_cmd( 26 | name="py2app_test", 27 | version="1.0", 28 | app=["main.py"], 29 | ) 30 | cmd.progress._progress.stop() 31 | pl = cmd.get_default_plist() 32 | self.assertEqual(pl["CFBundleVersion"], "1.0") 33 | 34 | # No version specified, none in script as well. 35 | cmd = self.create_cmd( 36 | name="py2app_test", 37 | app=[os.path.join(os.path.dirname(__file__), "shell_app/main.py")], 38 | ) 39 | pl = cmd.get_default_plist() 40 | self.assertEqual(pl["CFBundleVersion"], "0.0.0") 41 | cmd.progress._progress.stop() 42 | 43 | # A bit annoyinly distutils will automatically convert 44 | # integers to strings: 45 | cmd = self.create_cmd(name="py2app_test", app=["main.py"], version=1) 46 | pl = cmd.get_default_plist() 47 | self.assertEqual(pl["CFBundleVersion"], "1") 48 | self.assertEqual(cmd.distribution.get_version(), "1") 49 | cmd.progress._progress.stop() 50 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | ;envlist = py37,py38,py39,py310 3 | envlist = py27,py39,py310,py311 4 | isolated_build = True 5 | 6 | [testenv] 7 | ;commands = {envbindir}/python -mcoverage run --parallel -m unittest -v py2app_tests/ 8 | commands = {envbindir}/python -m unittest discover -v 9 | deps = 10 | altgraph 11 | macholib 12 | modulegraph 13 | coverage 14 | pyobjc 15 | importlib_metadata >= 4.7 ; python_version < '3.10' 16 | importlib_resources >= 4.7 ; python_version < '3.10' 17 | 18 | [testenv:coverage-report] 19 | deps = coverage 20 | skip_install = true 21 | commands = 22 | coverage combine 23 | coverage html 24 | coverage report 25 | 26 | [testenv:mypy] 27 | basepython = python3.10 28 | deps = 29 | mypy 30 | rich 31 | types-setuptools 32 | types-Pillow 33 | types-attrs 34 | packaging 35 | skip_install = true 36 | setenv = MYPYPATH = {toxinidir}/stubs 37 | commands = 38 | {envbindir}/python -m mypy --namespace-packages --check-untyped-defs --install-types src --disallow-untyped-defs 39 | ; {envbindir}/python -m mypy --explicit-package-bases --namespace-packages --check-untyped-defs src 40 | 41 | 42 | [coverage:run] 43 | branch = True 44 | source = py2app 45 | 46 | [coverage:report] 47 | sort = Cover 48 | 49 | [coverage:paths] 50 | source = 51 | py2app 52 | .tox/*/lib/python*/site-packages/py2app 53 | 54 | [flake8] 55 | max-line-length = 80 56 | select = C,E,F,W,B,B950,T,Q,M,R 57 | ignore = E501,W503 58 | inline-quotes = double 59 | multiline-quotes = double 60 | docstring-quotes = double 61 | 62 | [isort] 63 | multi_line_output=3 64 | include_trailing_comma=True 65 | force_grid_wrap=0 66 | use_parentheses=True 67 | line_length=88 68 | -------------------------------------------------------------------------------- /.github/workflows/release_to_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI / GitHub 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | run-tests: 10 | # For now only run pre-commit, later on this 11 | # should perform a unittest run. But that 12 | # requires having tests that run quickly enough 13 | # in CI. 14 | uses: ./.github/workflows/pre-commit.yml 15 | 16 | build-n-publish: 17 | name: Build and publish to PyPI 18 | needs: [run-tests] 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout source 23 | uses: actions/checkout@v2 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: "3.x" 29 | 30 | - name: Build source and wheel distributions 31 | run: | 32 | python -m pip install --upgrade build twine 33 | python -m build 34 | twine check --strict dist/* 35 | 36 | - name: create release notes 37 | run: | 38 | python .github/workflows/extract-notes.py > release-notes.rst 39 | 40 | - name: Publish distribution to PyPI 41 | uses: pypa/gh-action-pypi-publish@master 42 | with: 43 | user: __token__ 44 | password: ${{ secrets.PYPI_API_TOKEN }} 45 | 46 | - name: Create GitHub Release 47 | id: create_release 48 | uses: actions/create-release@v1 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | with: 52 | tag_name: ${{ github.ref }} 53 | release_name: ${{ github.ref }} 54 | draft: false 55 | prerelease: false 56 | body_path: ./release-notes.txt 57 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/virtualenv_site_packages.py: -------------------------------------------------------------------------------- 1 | def _site_packages(prefix: str, real_prefix: str, global_site_packages: bool) -> None: 2 | import os 3 | import site 4 | import sys 5 | 6 | paths = [] 7 | 8 | paths.append( 9 | os.path.join( 10 | prefix, "lib", "python%d.%d" % (sys.version_info[:2]), "site-packages" 11 | ) 12 | ) 13 | if os.path.join(".framework", "") in os.path.join(prefix, ""): 14 | home = os.environ.get("HOME") 15 | if home: 16 | paths.append( 17 | os.path.join( 18 | home, 19 | "Library", 20 | "Python", 21 | "%d.%d" % (sys.version_info[:2]), 22 | "site-packages", 23 | ) 24 | ) 25 | 26 | # Work around for a misfeature in setuptools: easy_install.pth places 27 | # site-packages way to early on sys.path and that breaks py2app bundles. 28 | # NOTE: this is hacks into an undocumented feature of setuptools and 29 | # might stop to work without warning. 30 | sys.__egginsert = len(sys.path) # type: ignore 31 | 32 | for path in paths: 33 | site.addsitedir(path) 34 | 35 | # Ensure that the global site packages get placed on sys.path after 36 | # the site packages from the virtual environment (this functionality 37 | # is also in virtualenv) 38 | sys.__egginsert = len(sys.path) # type: ignore 39 | 40 | if global_site_packages: 41 | site.addsitedir( 42 | os.path.join( 43 | real_prefix, 44 | "lib", 45 | "python%d.%d" % (sys.version_info[:2]), 46 | "site-packages", 47 | ) 48 | ) 49 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/site_packages.py: -------------------------------------------------------------------------------- 1 | def _site_packages() -> None: 2 | import os 3 | import site 4 | import sys 5 | 6 | paths = [] 7 | prefixes = [sys.prefix] 8 | if sys.exec_prefix != sys.prefix: 9 | prefixes.append(sys.exec_prefix) 10 | for prefix in prefixes: 11 | paths.append( 12 | os.path.join( 13 | prefix, "lib", "python%d.%d" % (sys.version_info[:2]), "site-packages" 14 | ) 15 | ) 16 | 17 | if os.path.join(".framework", "") in os.path.join(sys.prefix, ""): 18 | home = os.environ.get("HOME") 19 | if home: 20 | # Sierra and later 21 | paths.append( 22 | os.path.join( 23 | home, 24 | "Library", 25 | "Python", 26 | "%d.%d" % (sys.version_info[:2]), 27 | "lib", 28 | "python", 29 | "site-packages", 30 | ) 31 | ) 32 | 33 | # Before Sierra 34 | paths.append( 35 | os.path.join( 36 | home, 37 | "Library", 38 | "Python", 39 | "%d.%d" % (sys.version_info[:2]), 40 | "site-packages", 41 | ) 42 | ) 43 | 44 | # Work around for a misfeature in setuptools: easy_install.pth places 45 | # site-packages way to early on sys.path and that breaks py2app bundles. 46 | # NOTE: this is hacks into an undocumented feature of setuptools and 47 | # might stop to work without warning. 48 | sys.__egginsert = len(sys.path) # type: ignore 49 | 50 | for path in paths: 51 | site.addsitedir(path) 52 | 53 | 54 | _site_packages() 55 | -------------------------------------------------------------------------------- /doc/debugging.rst: -------------------------------------------------------------------------------- 1 | Debugging application building 2 | ============================== 3 | 4 | The py2app builder won't always generate a working application out of the box for 5 | various reasons. An incomplete build generally results in an application 6 | that won't launch, most of the time with a generic error dialog from 7 | py2app. 8 | 9 | The easiest way to debug build problems is to start the application 10 | directly in the Terminal. 11 | 12 | Given an application "MyApp.app" you can launch the application as 13 | follows: 14 | 15 | .. sourcecode:: shell 16 | 17 | $ dist/MyApp.app/Contents/MacOS/MyApp 18 | 19 | This will start the application as a normal shell command, with 20 | output from the application (both stdout and stderr) shown in 21 | the Terminal window. 22 | 23 | Some common problems are: 24 | 25 | * An import statement fails due to a missing module or package 26 | 27 | This generally happens when the dependency cannot be found 28 | by the source code analyzer, either due to dynamic imports 29 | (using ``__import__()`` or ``importlib`` to load a module), 30 | or due to imports in a C extension. 31 | 32 | In both cases use ``--includes`` or ``--packages`` to add 33 | the missing module to the application. 34 | 35 | If this is needed for a project on PyPI: Please file a bug 36 | on GitHub, that way we can teach py2app to do the right thing. 37 | 38 | * C library cannot find resources 39 | 40 | This might happen when a C library looks for resources in 41 | a fixed location instead to looking relative to the library 42 | itself. There are often APIs to tell the library which location 43 | it should use for resources. 44 | 45 | If this needed for a project on PyPI: Please file a bug 46 | on GitHub, including the workaround, that way we can teach 47 | py2app to the the right thing. 48 | -------------------------------------------------------------------------------- /py2app_tests/test_py2applet.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import sys 5 | import unittest 6 | 7 | import py2app 8 | from py2app import script_py2applet 9 | 10 | from .tools import kill_child_processes 11 | 12 | 13 | class TestPy2Applet(unittest.TestCase): 14 | def setUp(self): 15 | self.testdir = "test.dir" 16 | os.mkdir(self.testdir) 17 | 18 | def tearDown(self): 19 | shutil.rmtree(self.testdir) 20 | kill_child_processes() 21 | 22 | def run_py2applet(self, *args): 23 | env = os.environ.copy() 24 | pp = os.path.dirname(os.path.dirname(py2app.__file__)) 25 | if "PYTHONPATH" in env: 26 | env["PYTHONPATH"] = pp + ":" + env["PYTHONPATH"] 27 | else: 28 | env["PYTHONPATH"] = pp 29 | 30 | scriptfn = script_py2applet.__file__ 31 | if scriptfn.endswith(".pyc"): 32 | scriptfn = scriptfn[:-1] 33 | 34 | p = subprocess.Popen( 35 | [sys.executable, scriptfn] + list(args), 36 | cwd=self.testdir, 37 | stdout=subprocess.PIPE, 38 | stderr=subprocess.STDOUT, 39 | stdin=subprocess.PIPE, 40 | env=env, 41 | ) 42 | p.stdin.write(b"y\n") 43 | data = p.communicate()[0] 44 | 45 | xit = p.wait() 46 | if xit != 0: 47 | sys.stdout.write(data.decode("latin1")) 48 | self.fail("Running py2applet {} failed".format(" ".join(args))) 49 | 50 | def test_generate_setup(self): 51 | self.run_py2applet("--make-setup", "foo.py") 52 | 53 | setupfn = os.path.join(self.testdir, "setup.py") 54 | self.assertTrue(os.path.exists(setupfn)) 55 | fp = open(setupfn) 56 | contents = fp.read() 57 | fp.close() 58 | 59 | self.assertTrue("APP = ['foo.py']" in contents) 60 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | :layout: landing 2 | :description: py2app creates application and plugin bundles for Python scripts on macOS 3 | 4 | .. rst-class:: lead 5 | 6 | Py2app packages Python GUI applications as standalone macOS application or 7 | plugin bundles. 8 | 9 | This allows distributing such application to others that don't have 10 | Python installed on their system. 11 | 12 | .. container:: buttons 13 | 14 | `GitHub `_ `License `_ 15 | 16 | .. grid:: 1 1 2 3 17 | :gutter: 2 18 | :padding: 0 19 | :class-row: surface 20 | 21 | .. grid-item-card:: Release Info 22 | :link: changelog 23 | :link-type: doc 24 | 25 | py2app 2.0 was released on 2024-XX-XX. See the :doc:`changelog ` for more information. 26 | 27 | 28 | .. grid-item-card:: Supported Platforms 29 | :link: supported-platforms 30 | :link-type: doc 31 | 32 | - macOS 10.9 and later 33 | - Python 3.7 and later 34 | - x86_64 and arm64 35 | 36 | .. grid-item-card:: Installing py2app 37 | :link: install 38 | :link-type: doc 39 | 40 | .. sourcecode:: sh 41 | 42 | $ python3 -mpip \ 43 | install -U py2app 44 | 45 | .. toctree:: 46 | :hidden: 47 | :maxdepth: 1 48 | 49 | install 50 | changelog 51 | supported-platforms 52 | license 53 | 54 | .. toctree:: 55 | :maxdepth: 1 56 | :caption: Introduction 57 | 58 | tutorial 59 | examples 60 | 61 | .. toctree:: 62 | :caption: Usage 63 | :maxdepth: 1 64 | 65 | command-line 66 | pyproject 67 | setuptools 68 | 69 | .. toctree:: 70 | :caption: Finetuning 71 | :maxdepth: 1 72 | 73 | debugging 74 | environment 75 | tweaking 76 | faq 77 | 78 | .. toctree:: 79 | :caption: Internals 80 | :maxdepth: 1 81 | 82 | bundle-structure 83 | recipes 84 | -------------------------------------------------------------------------------- /examples/PyObjC/TinyTinyEdit/TinyTinyEdit.py: -------------------------------------------------------------------------------- 1 | """TinyTinyEdit -- A minimal Document-based Cocoa application.""" 2 | 3 | import sys 4 | 5 | import objc 6 | from Cocoa import NSDocument 7 | from PyObjCTools import AppHelper 8 | 9 | try: 10 | unicode 11 | except NameError: 12 | unicode = str 13 | 14 | 15 | class TinyTinyDocument(NSDocument): 16 | textView = objc.IBOutlet() 17 | 18 | path = None 19 | 20 | def windowNibName(self): 21 | return "TinyTinyDocument" 22 | 23 | def readFromFile_ofType_(self, path, tp): 24 | if self.textView is None: 25 | # we're not yet fully loaded 26 | self.path = path 27 | else: 28 | # "revert" 29 | self.readFromUTF8_(path) 30 | return True 31 | 32 | def writeToFile_ofType_(self, path, tp): 33 | f = open(path, "w") 34 | text = self.textView.string() 35 | f.write(text.encode("utf8")) 36 | f.close() 37 | return True 38 | 39 | def windowControllerDidLoadNib_(self, controller): 40 | if self.path: 41 | self.readFromUTF8_(self.path) 42 | else: 43 | if hasattr(sys, "maxint"): 44 | maxint = sys.maxint 45 | maxint_label = "maxint" 46 | else: 47 | maxint = sys.maxsize 48 | maxint_label = "maxsize" 49 | 50 | self.textView.setString_( 51 | "Welcome to TinyTinyEdit in Python\nVersion: %s\nsys.%s: %d\nbyteorder: %s\nflags: %s" 52 | % (sys.version, maxint_label, maxint, sys.byteorder, sys.flags) 53 | ) 54 | 55 | def readFromUTF8_(self, path): 56 | f = open(path) 57 | text = unicode(f.read(), "utf8") 58 | f.close() 59 | self.textView.setString_(text) 60 | 61 | 62 | if __name__ == "__main__": 63 | AppHelper.runEventLoop() 64 | -------------------------------------------------------------------------------- /doc/command-line.rst: -------------------------------------------------------------------------------- 1 | Invoking py2app 2 | ================= 3 | 4 | Command-line interface 5 | ---------------------- 6 | 7 | The preferred interface for py2app is using it as a command-line tool: 8 | 9 | .. sourcecode:: sh 10 | 11 | $ python3 -m py2app 12 | 13 | This command reads configuration from a ``pyproject.toml`` file and 14 | performs the build. The command has a number of arguments that affect 15 | the build: 16 | 17 | * ``--pyproject-tom FILE``, ``-c FILE`` 18 | 19 | Specify a different configuration file in the same format as 20 | ``pyproject.toml``. All paths in this file will be resolved 21 | relative to the directory containing the file. 22 | 23 | This option defaults to ``pyproject.toml`` in the current directory. 24 | 25 | * ``--semi-standalone`` 26 | 27 | Perform a semi-standalone build. This creates a bundle that 28 | contains all code and resources except for the Python interpreter. 29 | 30 | * ``--alias``, ``-A`` 31 | 32 | Perform an alias build. This creates a bundle that contains 33 | symbolic links to code and is primarily useful during development 34 | because it allows for a quicker edit&test cycle. 35 | 36 | * ``--verbose``, ``-v`` 37 | 38 | Print more information while building. 39 | 40 | * ``--help``, ``-h`` 41 | 42 | Show help about the command-line interface. 43 | 44 | * ``--version`` 45 | 46 | Show program version. 47 | 48 | See :doc:`pyproject` for information on the structure of the pyproject 49 | configuration file. 50 | 51 | Legacy setuptools interface 52 | --------------------------- 53 | 54 | Py2app before version 2.0 was an extension command for setuptools 55 | and this interface is still supported, but is deprecated. 56 | 57 | This interface requires using a ``setup.py`` file that contains 58 | configuration, use ``python3 setup.py py2app`` to invoke the 59 | setuptools command. 60 | 61 | See :doc:`setuptools` for more information on 62 | how to configure py2app. 63 | -------------------------------------------------------------------------------- /src/py2app/filters.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | 4 | from macholib.util import in_system_path 5 | from modulegraph import modulegraph 6 | 7 | 8 | def has_filename_filter(module: modulegraph.Node, /) -> bool: 9 | if isinstance(module, modulegraph.MissingModule): 10 | return True 11 | if hasattr(modulegraph, "InvalidRelativeImport") and isinstance( 12 | module, modulegraph.InvalidRelativeImport 13 | ): 14 | return True 15 | return getattr(module, "filename", None) is not None 16 | 17 | 18 | def _is_site_path(relpath: pathlib.Path) -> bool: 19 | return bool(any(x in relpath.parts for x in {"site-python", "site-packages"})) 20 | 21 | 22 | def not_stdlib_filter(module: modulegraph.Node) -> bool: 23 | """ 24 | Return False if the module is located in the standard library 25 | """ 26 | 27 | if module.filename is None: 28 | return True 29 | 30 | prefix = pathlib.Path(sys.prefix).resolve() 31 | rp = pathlib.Path(module.filename).resolve() 32 | 33 | if rp.is_relative_to(prefix): 34 | return _is_site_path(rp.relative_to(prefix)) 35 | 36 | if (prefix / ".Python").exists(): 37 | # Virtualenv 38 | v = sys.version_info 39 | fn = prefix / "lib" / f"python{v[0]}.{v[1]}" / "orig-prefix.txt" 40 | 41 | if fn.exists(): 42 | prefix = pathlib.Path(fn.read_text().strip()) 43 | if rp.is_relative_to(prefix): 44 | return _is_site_path(rp.relative_to(prefix)) 45 | 46 | if hasattr(sys, "base_prefix"): 47 | # Venv 48 | prefix = pathlib.Path(sys.base_prefix).resolve() 49 | if rp.is_relative_to(prefix): 50 | return _is_site_path(rp.relative_to(prefix)) 51 | 52 | return True 53 | 54 | 55 | def not_system_filter(module: modulegraph.Node) -> bool: 56 | """ 57 | Return False if the module is located in a system directory 58 | """ 59 | if module.filename is None: 60 | return False 61 | return not in_system_path(module.filename) 62 | -------------------------------------------------------------------------------- /src/py2app/_bundlepaths.py: -------------------------------------------------------------------------------- 1 | """ 2 | *BundlePaths* defines the paths to interesting locations 3 | in a bundle. 4 | """ 5 | 6 | import dataclasses 7 | import pathlib 8 | import typing 9 | 10 | __all__ = ("BundlePaths", "bundle_paths") 11 | 12 | 13 | @dataclasses.dataclass(frozen=True) 14 | class BundlePaths: 15 | """ 16 | Record the paths to interesting bits of a bundle: 17 | 18 | - ``root``: 'Contents' folder; 19 | - ``resources``: Root of the resource folder; 20 | - ``main``: Folder containing the main bundle binary; 21 | - ``pylib``: Location of Python libraries used that aren't zip safe; 22 | - ``pylib_zipped``: Location of the zip file containing the Python libraries used; 23 | - ``extlib``: Location of C extensions with special handling; 24 | - ``framework``: Location for included native libraries and frameworks. 25 | """ 26 | 27 | root: pathlib.Path 28 | resources: pathlib.Path 29 | main: pathlib.Path 30 | pylib: pathlib.Path 31 | pylib_zipped: pathlib.Path 32 | extlib: pathlib.Path 33 | framework: pathlib.Path 34 | 35 | def all_directories(self) -> typing.List[pathlib.Path]: 36 | """ 37 | Return all directories for the bundle paths 38 | """ 39 | return [ 40 | self.root, 41 | self.resources, 42 | self.main, 43 | self.pylib, 44 | self.extlib, 45 | self.framework, 46 | ] 47 | 48 | 49 | def bundle_paths(root: pathlib.Path) -> BundlePaths: 50 | """ 51 | Return a ``BundlePaths`` value for a bundle located at *root*. 52 | """ 53 | # See doc/bundle-structure.rst, section "Python Locations" 54 | return BundlePaths( 55 | root=root / "Contents", 56 | resources=root / "Contents/Resources", 57 | main=root / "Contents/MacOS", 58 | pylib_zipped=root / "Contents/Resources/python-libraries.zip", 59 | pylib=root / "Contents/Resources/python-libraries", 60 | extlib=root / "Contents/Resources/lib-dynload", 61 | framework=root / "Contents/Frameworks", 62 | ) 63 | -------------------------------------------------------------------------------- /doc/examples.rst: -------------------------------------------------------------------------------- 1 | Example setup.py templates 2 | ========================== 3 | 4 | Basic 5 | ----- 6 | 7 | The simplest possible ``setup.py`` script to build a py2app application 8 | looks like the following:: 9 | 10 | """ 11 | py2app build script for MyApplication 12 | 13 | Usage: 14 | python setup.py py2app 15 | """ 16 | from setuptools import setup 17 | setup( 18 | app=["MyApplication.py"], 19 | setup_requires=["py2app"], 20 | ) 21 | 22 | Cross-platform 23 | -------------- 24 | 25 | Cross-platform applications can share a ``setup.py`` script for both 26 | `py2exe`_ and py2app. Here is an example 27 | ``setup.py`` that will build an application on Windows or Mac OS X:: 28 | 29 | """ 30 | py2app/py2exe build script for MyApplication. 31 | 32 | Will automatically ensure that all build prerequisites are available 33 | via ez_setup 34 | 35 | Usage (Mac OS X): 36 | python setup.py py2app 37 | 38 | Usage (Windows): 39 | python setup.py py2exe 40 | """ 41 | import sys 42 | from setuptools import setup 43 | 44 | mainscript = 'MyApplication.py' 45 | 46 | if sys.platform == 'darwin': 47 | extra_options = dict( 48 | setup_requires=['py2app'], 49 | app=[mainscript], 50 | # Cross-platform applications generally expect sys.argv to 51 | # be used for opening files. 52 | # Don't use this with GUI toolkits, the argv 53 | # emulator causes problems and toolkits generally have 54 | # hooks for responding to file-open events. 55 | options=dict(py2app=dict(argv_emulation=True)), 56 | ) 57 | elif sys.platform == 'win32': 58 | extra_options = dict( 59 | setup_requires=['py2exe'], 60 | app=[mainscript], 61 | ) 62 | else: 63 | extra_options = dict( 64 | # Normally unix-like platforms will use "setup.py install" 65 | # and install the main script as such 66 | scripts=[mainscript], 67 | ) 68 | 69 | setup( 70 | name="MyApplication", 71 | **extra_options 72 | ) 73 | 74 | .. _`setuptools`: http://pypi.python.org/pypi/setuptools/ 75 | .. _`py2exe`: http://pypi.python.org/pypi/py2exe/ 76 | -------------------------------------------------------------------------------- /py2app_tests/basic_app_with_plugin/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shlex 4 | import shutil 5 | import subprocess 6 | from distutils.sysconfig import get_config_var 7 | 8 | from setuptools import Command, setup 9 | 10 | PLUGIN_NAMES = ["dummy1.qlgenerator", "dummy2.mdimporter"] 11 | 12 | 13 | class pluginexe(Command): 14 | description = "Generate dummy plugin executables" 15 | user_options = [] 16 | 17 | def initialize_options(self): 18 | pass 19 | 20 | def finalize_options(self): 21 | pass 22 | 23 | def run(self): 24 | cc = ["xcrun", "clang"] 25 | env = dict(os.environ) 26 | env["MACOSX_DEPLOYMENT_TARGET"] = get_config_var("MACOSX_DEPLOYMENT_TARGET") 27 | 28 | if not os.path.exists("lib"): 29 | os.mkdir("lib") 30 | cflags = get_config_var("CFLAGS") 31 | arch_flags = sum( 32 | (shlex.split(x) for x in re.findall(r"-arch\s+\S+", cflags)), [] 33 | ) 34 | root_flags = sum( 35 | (shlex.split(x) for x in re.findall(r"-isysroot\s+\S+", cflags)), [] 36 | ) 37 | 38 | for plugin_name in PLUGIN_NAMES: 39 | cmd = ( 40 | cc 41 | + arch_flags 42 | + root_flags 43 | + ["-dynamiclib", "-o", plugin_name, "plugin.c"] 44 | ) 45 | subprocess.check_call(cmd, env=env) 46 | 47 | 48 | class cleanup(Command): 49 | description = "cleanup build stuff" 50 | user_options = [] 51 | 52 | def initialize_options(self): 53 | pass 54 | 55 | def finalize_options(self): 56 | pass 57 | 58 | def run(self): 59 | for dn in ["build", "dist"]: 60 | if os.path.exists(dn): 61 | shutil.rmtree(dn) 62 | 63 | for fn in PLUGIN_NAMES: 64 | if os.path.exists(fn): 65 | os.unlink(fn) 66 | 67 | 68 | setup( 69 | name="BasicApp", 70 | app=["main.py"], 71 | options={ 72 | "py2app": { 73 | "include_plugins": ["dummy1.qlgenerator", "dummy2.mdimporter"], 74 | } 75 | }, 76 | cmdclass={ 77 | "pluginexe": pluginexe, 78 | "cleanup": cleanup, 79 | }, 80 | ) 81 | -------------------------------------------------------------------------------- /src/py2app/recipes/matplotlib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | 4 | # XXX: Only used for parsing version numbers 5 | import packaging.version 6 | from modulegraph.modulegraph import ModuleGraph 7 | 8 | from .. import build_app 9 | from ._types import RecipeInfo 10 | 11 | 12 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 13 | m = mf.findNode("matplotlib") 14 | if m is None or m.filename is None: 15 | return None 16 | 17 | # Don't try to import unless we've found the library, 18 | # otherwise we'll get an error when trying to use 19 | # py2app on a system with matplotlib 20 | VER: str 21 | from matplotlib import __version__ as VER # type: ignore 22 | 23 | if cmd.matplotlib_backends: 24 | use_package = False 25 | for backend in cmd.matplotlib_backends: 26 | if backend == "-": 27 | pass 28 | 29 | elif backend == "*": 30 | mf.import_hook("matplotlib.backends", m, ["*"]) 31 | 32 | else: 33 | mf.import_hook(f"matplotlib.backends.backend_{backend}", m) 34 | 35 | else: 36 | use_package = True 37 | 38 | # XXX: I don't particularly like this code pattern, repetition is needed 39 | # to avoid confusing mypy. 40 | if use_package: 41 | if packaging.version.parse(VER) < packaging.version.parse("3.1"): 42 | return { 43 | "resources": [os.path.join(os.path.dirname(m.filename), "mpl-data")], 44 | "prescripts": ["py2app.recipes.matplotlib_prescript"], 45 | "packages": ["matplotlib"], 46 | } 47 | else: 48 | return { 49 | "resources": [os.path.join(os.path.dirname(m.filename), "mpl-data")], 50 | "packages": ["matplotlib"], 51 | } 52 | else: 53 | if packaging.version.parse(VER) < packaging.version.parse("3.1"): 54 | return { 55 | "resources": [os.path.join(os.path.dirname(m.filename), "mpl-data")], 56 | "prescripts": ["py2app.recipes.matplotlib_prescript"], 57 | } 58 | else: 59 | return { 60 | "resources": [os.path.join(os.path.dirname(m.filename), "mpl-data")], 61 | } 62 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.12 3 | repos: 4 | - repo: https://github.com/pre-commit/mirrors-isort 5 | rev: v5.10.1 6 | hooks: 7 | - id: isort 8 | additional_dependencies: [toml] 9 | 10 | - repo: https://github.com/psf/black 11 | rev: 24.4.2 12 | hooks: 13 | - id: black 14 | # override until resolved: https://github.com/ambv/black/issues/402 15 | files: \.pyi?$ 16 | types: [] 17 | 18 | - repo: https://github.com/asottile/pyupgrade 19 | rev: v3.16.0 20 | hooks: 21 | - id: pyupgrade 22 | args: ['--py38-plus'] 23 | 24 | - repo: https://github.com/codespell-project/codespell 25 | rev: v2.3.0 26 | hooks: 27 | - id: codespell 28 | args: ["--config", ".codespellrc"] 29 | exclude: PyObjCTest|_metadata\.py$|\.fwinfo$|\.rtf$|\.mht$ 30 | 31 | - repo: https://github.com/pycqa/flake8 32 | rev: 7.1.0 33 | hooks: 34 | - id: flake8 35 | args: ["--config", ".flake8" ] 36 | additional_dependencies: 37 | - flake8-bugbear 38 | - flake8-deprecated 39 | - flake8-comprehensions 40 | - flake8-isort 41 | - flake8-quotes 42 | - flake8-mutable 43 | - flake8-todo 44 | - flake8-builtins 45 | - flake8-raise 46 | - flake8-tidy-imports 47 | 48 | # XXX: Enabling this requires a new release of modulegraph2, and 49 | # debugging why this command fails while the equivalent config 50 | # in tox.ini does work. 51 | # - repo: https://github.com/pre-commit/mirrors-mypy 52 | # rev: 'v1.10.1' 53 | # hooks: 54 | # - id: mypy 55 | # entry: env MYPYPATH=./stubs mypy 56 | # args: [--namespace-packages, --check-untyped-defs, --disallow-untyped-defs] 57 | # exclude: examples|py2app_tests 58 | # additional_dependencies: 59 | # - rich 60 | # - types-setuptools 61 | # - types-Pillow 62 | # - types-attrs 63 | # - packaging 64 | # - modulegraph2 65 | # - truststore 66 | # - pyobjc-core 67 | 68 | - repo: https://github.com/pre-commit/pre-commit-hooks 69 | rev: v4.6.0 70 | hooks: 71 | - id: trailing-whitespace 72 | - id: end-of-file-fixer 73 | # - id: debug-statements 74 | -------------------------------------------------------------------------------- /py2app_tests/app_with_scripts/presetup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import subprocess 5 | from distutils import sysconfig 6 | from distutils.command.build_ext import build_ext 7 | from distutils.core import Command, Extension, setup 8 | 9 | 10 | class my_build_ext(build_ext): 11 | def run(self): 12 | cmd = self.reinitialize_command("build_dylib") 13 | cmd.run() 14 | build_ext.run(self) 15 | 16 | 17 | class build_dylib(Command): 18 | user_options = [] 19 | 20 | def initialize_options(self): 21 | pass 22 | 23 | def finalize_options(self): 24 | pass 25 | 26 | def get_arch_flags(self): 27 | cflags = sysconfig.get_config_var("CFLAGS") 28 | result = [] 29 | for item in re.findall(r"(-arch\s+\S+)", cflags): 30 | result.extend(item.split()) 31 | return result 32 | 33 | def run(self): 34 | print("running build_dylib") 35 | bdir = "build/libdir" 36 | if os.path.exists(bdir): 37 | shutil.rmtree(bdir) 38 | 39 | os.makedirs(bdir) 40 | cflags = self.get_arch_flags() 41 | cc = ["xcrun", "clang"] 42 | env = dict(os.environ) 43 | env["MACOSX_DEPLOYMENT_TARGET"] = sysconfig.get_config_var( 44 | "MACOSX_DEPLOYMENT_TARGET" 45 | ) 46 | 47 | subprocess.check_call( 48 | cc + cflags + ["-c", "-o", os.path.join(bdir, "libfoo.o"), "src/libfoo.c"], 49 | env=env, 50 | ) 51 | 52 | subprocess.check_call( 53 | cc 54 | + [ 55 | "-dynamiclib", 56 | "-o", 57 | os.path.join(bdir, "libfoo.dylib"), 58 | "-install_name", 59 | os.path.abspath(os.path.join(bdir, "libfoo.dylib")), 60 | os.path.join(os.path.join(bdir, "libfoo.o")), 61 | ], 62 | env=env, 63 | ) 64 | 65 | 66 | setup( 67 | cmdclass={ 68 | "build_dylib": build_dylib, 69 | "build_ext": my_build_ext, 70 | }, 71 | ext_modules=[ 72 | Extension( 73 | "foo", 74 | ["src/modfoo.c"], 75 | extra_link_args=["-L{}".format(os.path.abspath("build/libdir")), "-lfoo"], 76 | ) 77 | ], 78 | options={ 79 | "build_ext": {"inplace": True}, 80 | }, 81 | ) 82 | -------------------------------------------------------------------------------- /src/py2app/recipes/qt6.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | 4 | from modulegraph.modulegraph import MissingModule, ModuleGraph 5 | 6 | from .. import build_app 7 | from ._types import RecipeInfo 8 | 9 | 10 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 11 | m = mf.findNode("PyQt6") 12 | if m and not isinstance(m, MissingModule): 13 | try: 14 | # PyQt6 with sipconfig module, handled 15 | # by sip recipe 16 | import sipconfig # type: ignore # noqa: F401 17 | 18 | return None 19 | 20 | except ImportError: 21 | pass 22 | 23 | try: 24 | import PyQt6 # type: ignore 25 | from PyQt6.QtCore import QLibraryInfo # type: ignore 26 | except ImportError: 27 | # Dependency in the graph, but PyQt6 isn't 28 | # installed. 29 | return None 30 | 31 | qtdir = QLibraryInfo.path(QLibraryInfo.LibraryPath.LibrariesPath) 32 | assert isinstance(qtdir, str) 33 | if os.path.relpath(qtdir, os.path.dirname(PyQt6.__file__)).startswith("../"): 34 | # Qt6's prefix is not the PyQt6 package, which means 35 | # the "packages" directive below won't include everything 36 | # needed, and in particular won't include the plugins 37 | # folder. 38 | print("System install of Qt6") 39 | 40 | # Ensure that the Qt plugins are copied into the "Contents/plugins" folder, 41 | # that's where the bundles Qt expects them to be 42 | pluginspath = QLibraryInfo.path(QLibraryInfo.LibraryPath.PluginsPath) 43 | assert isinstance(pluginspath, str) 44 | resources = [("..", [pluginspath])] 45 | 46 | else: 47 | resources = None 48 | 49 | # All imports are done from C code, hence not visible 50 | # for modulegraph 51 | # 1. Use of 'sip' 52 | # 2. Use of other modules, datafiles and C libraries 53 | # in the PyQt5 package. 54 | try: 55 | mf.import_hook("sip", m) 56 | except ImportError: 57 | mf.import_hook("sip", m, level=1) 58 | 59 | result: RecipeInfo = {"packages": ["PyQt6"]} 60 | if resources is not None: 61 | result["resources"] = resources 62 | return result 63 | 64 | return None 65 | -------------------------------------------------------------------------------- /src/py2app/recipes/qt5.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | 4 | from modulegraph.modulegraph import MissingModule, ModuleGraph 5 | 6 | from .. import build_app 7 | from ._types import RecipeInfo 8 | 9 | 10 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 11 | m = mf.findNode("PyQt5") 12 | if m and not isinstance(m, MissingModule): 13 | try: 14 | # PyQt5 with sipconfig module, handled 15 | # by sip recipe 16 | import sipconfig # type: ignore # noqa: F401 17 | 18 | return None 19 | 20 | except ImportError: 21 | pass 22 | 23 | try: 24 | import PyQt5 # type: ignore 25 | from PyQt5.QtCore import QLibraryInfo # type: ignore 26 | except ImportError: 27 | # PyQt5 in the graph, but not installed 28 | return None 29 | 30 | # All imports are done from C code, hence not visible 31 | # for modulegraph 32 | # 1. Use of 'sip' 33 | # 2. Use of other modules, datafiles and C libraries 34 | # in the PyQt5 package. 35 | try: 36 | mf.import_hook("PyQt5.sip", m) 37 | except ImportError: 38 | mf.import_hook("sip", m, level=1) 39 | 40 | qtdir = QLibraryInfo.location(QLibraryInfo.LibrariesPath) 41 | if os.path.relpath(qtdir, os.path.dirname(PyQt5.__file__)).startswith("../"): 42 | # Qt5's prefix is not the PyQt5 package, which means 43 | # the "packages" directive below won't include everything 44 | # needed, and in particular won't include the plugins 45 | # folder. 46 | print("System install of Qt5") 47 | 48 | # Ensure that the Qt plugins are copied into the "Contents/plugins" 49 | # folder, that's where the bundles Qt expects them to be 50 | pluginspath = QLibraryInfo.location(QLibraryInfo.PluginsPath) 51 | assert isinstance(pluginspath, str) 52 | resources = [("..", [pluginspath])] 53 | 54 | else: 55 | resources = None 56 | 57 | result: RecipeInfo = { 58 | "packages": ["PyQt5"], 59 | "expected_missing_imports": {"copy_reg", "cStringIO", "StringIO"}, 60 | } 61 | if resources is not None: 62 | result["resources"] = resources 63 | return result 64 | 65 | return None 66 | -------------------------------------------------------------------------------- /src/py2app/recipes/pyside.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import importlib.resources 3 | import io 4 | import os 5 | import typing 6 | 7 | from modulegraph.modulegraph import ModuleGraph 8 | 9 | from .. import build_app 10 | from ._types import RecipeInfo 11 | 12 | 13 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 14 | name = "PySide" 15 | m = mf.findNode(name) 16 | if m is None or m.filename is None: 17 | return None 18 | 19 | try: 20 | from PySide import QtCore # type: ignore 21 | except ImportError: 22 | print("WARNING: macholib found PySide, but cannot import") 23 | return {} 24 | 25 | plugin_dir = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.PluginsPath) 26 | 27 | resource_data = importlib.resources.read_text("py2app.recipes", "qt.conf") 28 | resource_fp = io.StringIO(resource_data) 29 | resource_fp.name = "qt.conf" 30 | 31 | resources: typing.Sequence[ 32 | typing.Union[ 33 | str, typing.Tuple[str, typing.Sequence[typing.Union[str, typing.IO[str]]]] 34 | ] 35 | ] 36 | resources = [("", [resource_fp])] 37 | for item in cmd.qt_plugins if cmd.qt_plugins is not None else (): 38 | if "/" not in item: 39 | item = item + "/*" 40 | 41 | if "*" in item: 42 | for path in glob.glob(os.path.join(plugin_dir, item)): 43 | assert isinstance(path, str) 44 | rel_path = path[len(plugin_dir) :] # noqa: E203 45 | resources.append((os.path.dirname("qt_plugins" + rel_path), [path])) 46 | else: 47 | resources.append( 48 | ( 49 | os.path.dirname(os.path.join("qt_plugins", item)), 50 | [os.path.join(plugin_dir, item)], 51 | ) 52 | ) 53 | 54 | # PySide dumps some of its shared files 55 | # into /usr/lib, which is a system location 56 | # and those files are therefore not included 57 | # into the app bundle by default. 58 | from macholib.util import NOT_SYSTEM_FILES 59 | 60 | for fn in os.listdir("/usr/lib"): 61 | add = False 62 | if fn.startswith("libpyside-python"): 63 | add = True 64 | elif fn.startswith("libshiboken-python"): 65 | add = True 66 | if add: 67 | NOT_SYSTEM_FILES.append(os.path.join("/usr/lib", fn)) 68 | 69 | return {"resources": resources} 70 | -------------------------------------------------------------------------------- /py2app_tests/bundle_loader.m: -------------------------------------------------------------------------------- 1 | #import 2 | #include 3 | 4 | static NSMutableDictionary* pluginMap; 5 | 6 | @interface PluginObject : NSObject 7 | {} 8 | -(void)performCommand:(NSString*)command; 9 | @end 10 | 11 | 12 | static int loadBundle(NSString* bundlePath) 13 | { 14 | if (pluginMap == nil) { 15 | pluginMap = [NSMutableDictionary dictionary]; 16 | if (pluginMap == nil) { 17 | printf("** Cannot allocate plugin map\n"); 18 | return -1; 19 | } 20 | [pluginMap retain]; 21 | } 22 | 23 | NSBundle* bundle = [NSBundle bundleWithPath: bundlePath]; 24 | if (bundle == NULL) { 25 | printf("** Cannot load bundle %s\n", [bundlePath UTF8String]); 26 | return -1; 27 | } 28 | 29 | Class pluginClass = [bundle principalClass]; 30 | if (pluginClass == Nil) { 31 | printf("** No principle class in %s\n", [bundlePath UTF8String]); 32 | return -1; 33 | } 34 | 35 | PluginObject* pluginObject = [[pluginClass alloc] init]; 36 | if (pluginObject == Nil) { 37 | printf("** No principle class in %s\n", [bundlePath UTF8String]); 38 | return -1; 39 | } 40 | 41 | [pluginMap setObject:pluginObject forKey:[bundlePath lastPathComponent]]; 42 | return 0; 43 | } 44 | 45 | 46 | static int 47 | perform_commands(void) 48 | { 49 | static char gBuf[1024]; 50 | char* ln; 51 | while ((ln = fgets(gBuf, 1024, stdin)) != NULL) { 52 | char* e = strchr(ln, '\n'); 53 | if (e) { *e = '\0'; } 54 | char* cmd = strchr(ln, ':'); 55 | if (cmd == NULL) { 56 | if (strcmp(ln, "quit") == 0) { 57 | return (0); 58 | } 59 | printf("* UNKNOWN COMMAND: %s\n", ln); 60 | } else { 61 | *cmd++ = '\0'; 62 | NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; 63 | 64 | PluginObject* pluginObject = [pluginMap objectForKey:[NSString stringWithUTF8String:ln]]; 65 | if (pluginObject == NULL) { 66 | printf("* NO OBJECT: %s\n", cmd); 67 | continue; 68 | } 69 | [pluginObject performCommand: [NSString stringWithUTF8String:cmd]]; 70 | [pool release]; 71 | } 72 | } 73 | return 0; 74 | } 75 | 76 | 77 | int main(int argc, char** argv) 78 | { 79 | int i, r; 80 | NSAutoreleasePool* pool; 81 | 82 | pool = [[NSAutoreleasePool alloc] init]; 83 | for (i = 1; i < argc; i++) { 84 | r = loadBundle([NSString stringWithUTF8String:argv[i]]); 85 | if (r != 0) { 86 | return 2; 87 | } 88 | } 89 | printf("+ loaded %d bundles\n", argc - 1); 90 | fflush(stdout); 91 | [pool release]; 92 | return perform_commands(); 93 | } 94 | -------------------------------------------------------------------------------- /src/py2app/apptemplate/plist_template.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import py2app 4 | 5 | __all__ = ["infoPlistDict"] 6 | 7 | 8 | def infoPlistDict( 9 | CFBundleExecutable: str, plist: dict = {} # noqa: B006, M511 10 | ) -> dict: 11 | CFBundleExecutable = CFBundleExecutable 12 | version = ".".join(map(str, sys.version_info[:2])) 13 | pdict = { 14 | "CFBundleDevelopmentRegion": "English", 15 | "CFBundleDisplayName": plist.get("CFBundleName", CFBundleExecutable), 16 | "CFBundleExecutable": CFBundleExecutable, 17 | "CFBundleIconFile": CFBundleExecutable, 18 | "CFBundleIdentifier": f"org.pythonmac.unspecified.{''.join(CFBundleExecutable.split())}", 19 | "CFBundleInfoDictionaryVersion": "6.0", 20 | "CFBundleName": CFBundleExecutable, 21 | "CFBundlePackageType": "APPL", 22 | "CFBundleShortVersionString": plist.get("CFBundleVersion", "0.0"), 23 | "CFBundleSignature": "????", 24 | "CFBundleVersion": "0.0", 25 | "LSHasLocalizedDisplayName": False, 26 | "NSAppleScriptEnabled": False, 27 | "NSHumanReadableCopyright": "Copyright not specified", 28 | "NSMainNibFile": "MainMenu", 29 | "NSPrincipalClass": "NSApplication", 30 | "PyMainFileNames": ["__boot__"], 31 | "PyResourcePackages": [], 32 | "PyRuntimeLocations": [ 33 | (s % version) 34 | for s in [ 35 | ( 36 | "@executable_path/../Frameworks/Python.framework" 37 | "/Versions/%s/Python" 38 | ), 39 | "~/Library/Frameworks/Python.framework/Versions/%s/Python", 40 | "/Library/Frameworks/Python.framework/Versions/%s/Python", 41 | "/Network/Library/Frameworks/Python.framework/Versions/%s/Python", 42 | "/System/Library/Frameworks/Python.framework/Versions/%s/Python", 43 | ] 44 | ], 45 | } 46 | pdict.update(plist) 47 | pythonInfo = pdict.setdefault("PythonInfoDict", {}) 48 | pythonInfo.update( 49 | { 50 | "PythonLongVersion": sys.version, 51 | "PythonShortVersion": ".".join(str(x) for x in sys.version_info[:2]), 52 | "PythonExecutable": sys.executable, 53 | } 54 | ) 55 | py2appInfo = pythonInfo.setdefault("py2app", {}) 56 | py2appInfo.update( 57 | { 58 | "version": py2app.__version__, 59 | "template": "app", 60 | } 61 | ) 62 | return pdict 63 | -------------------------------------------------------------------------------- /src/py2app/bootstrap/emulate_shell_environment.py: -------------------------------------------------------------------------------- 1 | def _emulate_shell_environ() -> None: 2 | import os 3 | import time 4 | 5 | env = os.environb 6 | 7 | split_char = b"=" 8 | 9 | # Start 'login -qf $LOGIN' in a pseudo-tty. The pseudo-tty 10 | # is required to get the right behavior from the shell, without 11 | # a tty the shell won't properly initialize the environment. 12 | # 13 | # NOTE: The code is very careful w.r.t. getting the login 14 | # name, the application shouldn't crash when the shell information 15 | # cannot be retrieved 16 | try: 17 | login: "str|None" = os.getlogin() 18 | if login == "root": 19 | # For some reason os.getlogin() returns 20 | # "root" for user sessions on Catalina. 21 | try: 22 | login = os.environ["LOGNAME"] 23 | except KeyError: 24 | login = None 25 | except AttributeError: 26 | try: 27 | login = os.environ["LOGNAME"] 28 | except KeyError: 29 | login = None 30 | 31 | if login is None: 32 | return 33 | 34 | master, slave = os.openpty() 35 | pid = os.fork() 36 | 37 | if pid == 0: 38 | # Child 39 | os.close(master) 40 | os.setsid() 41 | os.dup2(slave, 0) 42 | os.dup2(slave, 1) 43 | os.dup2(slave, 2) 44 | os.execv("/usr/bin/login", ["login", "-qf", login]) 45 | os._exit(42) 46 | 47 | else: 48 | # Parent 49 | os.close(slave) 50 | # Echo markers around the actual output of env, that makes it 51 | # easier to find the real data between other data printed 52 | # by the shell. 53 | os.write(master, b'echo "---------";env;echo "-----------"\r\n') 54 | os.write(master, b"exit\r\n") 55 | time.sleep(1) 56 | 57 | data_parts = [] 58 | b = os.read(master, 2048) 59 | while b: 60 | data_parts.append(b) 61 | b = os.read(master, 2048) 62 | data = b"".join(data_parts) 63 | del data_parts 64 | os.waitpid(pid, 0) 65 | 66 | in_data = False 67 | for ln in data.splitlines(): 68 | if not in_data: 69 | if ln.strip().startswith(b"--------"): 70 | in_data = True 71 | continue 72 | 73 | if ln.startswith(b"--------"): 74 | break 75 | 76 | try: 77 | key, value = ln.rstrip().split(split_char, 1) 78 | except Exception: 79 | pass 80 | 81 | else: 82 | env[key] = value 83 | 84 | 85 | _emulate_shell_environ() 86 | -------------------------------------------------------------------------------- /src/py2app/recipes/pyside2.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import importlib.resources 3 | import io 4 | import os 5 | import typing 6 | 7 | from modulegraph.modulegraph import ModuleGraph 8 | 9 | from .. import build_app 10 | from ._types import RecipeInfo 11 | 12 | 13 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 14 | name = "PySide2" 15 | m = mf.findNode(name) 16 | if m is None or m.filename is None: 17 | return None 18 | 19 | try: 20 | from PySide2 import QtCore # type: ignore 21 | except ImportError: 22 | print("WARNING: macholib found PySide2, but cannot import") 23 | return {} 24 | 25 | plugin_dir = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.PluginsPath) 26 | 27 | resource_data = importlib.resources.read_text("py2app.recipes", "qt.conf") 28 | resource_fp = io.StringIO(resource_data) 29 | resource_fp.name = "qt.conf" 30 | 31 | resources: typing.Sequence[ 32 | typing.Union[ 33 | str, typing.Tuple[str, typing.Sequence[typing.Union[str, typing.IO[str]]]] 34 | ] 35 | ] 36 | resources = [("", [resource_fp])] 37 | 38 | for item in cmd.qt_plugins if cmd.qt_plugins is not None else (): 39 | if "/" not in item: 40 | item = item + "/*" 41 | 42 | if "*" in item: 43 | for path in glob.glob(os.path.join(plugin_dir, item)): 44 | rel_path = path[len(plugin_dir) :] # noqa: E203 45 | resources.append((os.path.dirname("qt_plugins" + rel_path), [path])) 46 | else: 47 | resources.append( 48 | ( 49 | os.path.dirname(os.path.join("qt_plugins", item)), 50 | [os.path.join(plugin_dir, item)], 51 | ) 52 | ) 53 | 54 | # PySide dumps some of its shared files 55 | # into /usr/lib, which is a system location 56 | # and those files are therefore not included 57 | # into the app bundle by default. 58 | from macholib.util import NOT_SYSTEM_FILES 59 | 60 | for fn in os.listdir("/usr/lib"): 61 | add = False 62 | if fn.startswith("libpyside2-python"): 63 | add = True 64 | elif fn.startswith("libshiboken2-python"): 65 | add = True 66 | if add: 67 | NOT_SYSTEM_FILES.append(os.path.join("/usr/lib", fn)) 68 | 69 | mf.import_hook("PySide2.support", m, ["*"]) 70 | mf.import_hook("PySide2.support.signature", m, ["*"]) 71 | mf.import_hook("PySide2.support.signature.lib", m, ["*"]) 72 | mf.import_hook("PySide2.support.signature.typing", m, ["*"]) 73 | 74 | return {"resources": resources} 75 | -------------------------------------------------------------------------------- /src/py2app/recipes/pyside6.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import importlib.resources 3 | import io 4 | import os 5 | import typing 6 | 7 | from modulegraph.modulegraph import ModuleGraph 8 | 9 | from .. import build_app 10 | from ._types import RecipeInfo 11 | 12 | 13 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 14 | name = "PySide6" 15 | m = mf.findNode(name) 16 | if m is None or m.filename is None: 17 | return None 18 | 19 | try: 20 | from PySide6 import QtCore # type: ignore 21 | except ImportError: 22 | print("WARNING: macholib found PySide6, but cannot import") 23 | return {} 24 | 25 | plugin_dir = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.PluginsPath) 26 | 27 | resource_data = importlib.resources.read_text("py2app.recipes", "qt.conf") 28 | resource_fp = io.StringIO(resource_data) 29 | resource_fp.name = "qt.conf" 30 | 31 | resources: typing.Sequence[ 32 | typing.Union[ 33 | str, typing.Tuple[str, typing.Sequence[typing.Union[str, typing.IO[str]]]] 34 | ] 35 | ] 36 | resources = [("", [resource_fp])] 37 | for item in cmd.qt_plugins if cmd.qt_plugins is not None else (): 38 | if "/" not in item: 39 | item = item + "/*" 40 | 41 | if "*" in item: 42 | for path in glob.glob(os.path.join(plugin_dir, item)): 43 | rel_path = path[len(plugin_dir) :] # noqa: E203 44 | resources.append((os.path.dirname("qt_plugins" + rel_path), [path])) 45 | else: 46 | resources.append( 47 | ( 48 | os.path.dirname(os.path.join("qt_plugins", item)), 49 | [os.path.join(plugin_dir, item)], 50 | ) 51 | ) 52 | 53 | # PySide dumps some of its shared files 54 | # into /usr/lib, which is a system location 55 | # and those files are therefore not included 56 | # into the app bundle by default. 57 | from macholib.util import NOT_SYSTEM_FILES 58 | 59 | for fn in os.listdir("/usr/lib"): 60 | add = False 61 | if fn.startswith("libpyside6-python"): 62 | add = True 63 | elif fn.startswith("libshiboken6-python"): 64 | add = True 65 | if add: 66 | NOT_SYSTEM_FILES.append(os.path.join("/usr/lib", fn)) 67 | 68 | mf.import_hook("PySide6.support", m, ["*"]) 69 | mf.import_hook("PySide6.support.signature", m, ["*"]) 70 | mf.import_hook("PySide6.support.signature.lib", m, ["*"]) 71 | mf.import_hook("PySide6.support.signature.typing", m, ["*"]) 72 | 73 | return {"resources": resources, "packages": ["PySide6"]} 74 | -------------------------------------------------------------------------------- /src/py2app/_progress.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import rich.progress 4 | 5 | T = typing.TypeVar("T") 6 | 7 | 8 | class Progress: 9 | def __init__(self, level: int = 1) -> None: 10 | self._progress = rich.progress.Progress( 11 | *rich.progress.Progress.get_default_columns()[:-1], 12 | rich.progress.TimeElapsedColumn(), 13 | rich.progress.TextColumn("{task.fields[current]}"), 14 | transient=True, 15 | ) 16 | self._progress.start() 17 | self._level = level 18 | self.have_error = False 19 | 20 | def stop(self) -> None: 21 | self._progress.stop() 22 | 23 | def add_task(self, name: str, count: int | None) -> rich.progress.TaskID: 24 | return self._progress.add_task(name, total=count, current="", start=True) 25 | 26 | def step_task(self, task_id: rich.progress.TaskID) -> None: 27 | self._progress.advance(task_id) 28 | 29 | def iter_task( 30 | self, items: typing.Sequence[T], label: str, current: typing.Callable[[T], str] 31 | ) -> typing.Iterator[T]: 32 | task_id = self.add_task(label, count=len(items)) 33 | 34 | for value in items: 35 | self.update(task_id, current=current(value)) 36 | yield value 37 | self.step_task(task_id) 38 | 39 | self.update(task_id, current="") 40 | 41 | def update(self, task_id: rich.progress.TaskID, **kwds: typing.Any) -> None: 42 | self._progress.update(task_id, **kwds) 43 | 44 | def task_done(self, task_id: rich.progress.TaskID) -> None: 45 | task = self._progress.tasks[task_id] 46 | if task.total is None: 47 | self._progress.update(task_id, total=task.completed, current="") 48 | 49 | def print( # noqa: A003 50 | self, message: str, *, highlight: typing.Optional[bool] = None 51 | ) -> None: 52 | if highlight is not None: 53 | self._progress.print(message, highlight=highlight) 54 | else: 55 | self._progress.print(message) 56 | 57 | def info(self, message: str, *, highlight: typing.Optional[bool] = None) -> None: 58 | if self._level >= 1: 59 | self.print(message, highlight=highlight) 60 | 61 | def trace(self, message: str) -> None: 62 | if self._level >= 2: 63 | self._progress.print(message) 64 | 65 | def warning(self, message: str) -> None: 66 | # XXX: Color doesn't work? 67 | if message: 68 | self._progress.print(f":orange_circle: {message}") 69 | 70 | def error(self, message: str) -> None: 71 | if message: 72 | self._progress.print(f":red_circle: {message}") 73 | self.have_error = True 74 | -------------------------------------------------------------------------------- /src/py2app/recipes/black.py: -------------------------------------------------------------------------------- 1 | # mypy: ignore-errors 2 | import ast 3 | import os 4 | import sys 5 | 6 | from pkg_resources import get_distribution # noqa: I251 7 | 8 | 9 | def check(cmd, mf): 10 | m = mf.findNode("black") 11 | if m is None or m.filename is None: 12 | return None 13 | 14 | egg = get_distribution("black").egg_info 15 | top = os.path.join(egg, "RECORD") 16 | 17 | # These cannot be in zip 18 | packages = ["black", "blib2to3"] 19 | 20 | # black may include optimized platform specific C extension which has 21 | # unusual name, e.g. 610faff656c4cfcbb4a3__mypyc; best to determine it from 22 | # the egg-info/RECORD file. 23 | 24 | includes = set() 25 | with open(top) as f: 26 | for line in f: 27 | fname = line.split(",", 1)[0] 28 | toplevel = fname.split("/", 1)[0] 29 | if all(ch not in toplevel for ch in (".", "-")): 30 | includes.add(toplevel) 31 | 32 | if fname.endswith(".py"): 33 | toplevel_node = mf.findNode(toplevel) 34 | if toplevel_node is None: 35 | continue 36 | # Black is mypyc compiled, but generally ships with 37 | # the original source inside the wheel, that allows us 38 | # to extract dependencies from those sources. 39 | # 40 | # This is, to state is nicely, a crude hack that uses 41 | # internal details of modulegraph. 42 | fqname = fname[:-3].replace("/", ".") 43 | source_path = os.path.join(os.path.dirname(egg), fname) 44 | with open(source_path, "rb") as fp: 45 | update_dependencies_from_source( 46 | mf, fqname, fp, source_path, toplevel_node 47 | ) 48 | 49 | includes = list(includes.difference(packages)) 50 | 51 | return {"includes": includes, "packages": packages} 52 | 53 | 54 | def update_dependencies_from_source(mf, fqname, fp, pathname, toplevel): 55 | # This logic is from ModuleGraph._load_module, but only 56 | # the imp.PY_SOURCE case and without updating the graph 57 | m = mf.findNode(fqname) 58 | if m is None: 59 | m = mf.import_hook(fqname, toplevel)[0] 60 | 61 | contents = fp.read() 62 | if isinstance(contents, bytes): 63 | contents += b"\n" 64 | else: 65 | contents += "\n" 66 | 67 | try: 68 | co = compile(contents, pathname, "exec", ast.PyCF_ONLY_AST, True) 69 | if sys.version_info[:2] == (3, 5): 70 | # In Python 3.5 some syntax problems with async 71 | # functions are only reported when compiling to bytecode 72 | compile(co, "-", "exec", 0, True) 73 | except SyntaxError: 74 | return 75 | 76 | mf._scan_code(co, m) 77 | -------------------------------------------------------------------------------- /py2app_tests/test_compile_resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import sys 5 | import time 6 | import unittest 7 | 8 | import py2app 9 | 10 | from .tools import kill_child_processes 11 | 12 | DIR_NAME = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | 15 | class TestBasicApp(unittest.TestCase): 16 | py2app_args = [] 17 | app_dir = os.path.join(DIR_NAME, "resource_compile_app") 18 | 19 | # Basic setup code 20 | # 21 | # The code in this block needs to be moved to 22 | # a base-class. 23 | @classmethod 24 | def setUpClass(cls): 25 | kill_child_processes() 26 | 27 | env = os.environ.copy() 28 | pp = os.path.dirname(os.path.dirname(py2app.__file__)) 29 | if "PYTHONPATH" in env: 30 | env["PYTHONPATH"] = pp + ":" + env["PYTHONPATH"] 31 | else: 32 | env["PYTHONPATH"] = pp 33 | 34 | p = subprocess.Popen( 35 | [sys.executable, "setup.py", "py2app"] + cls.py2app_args, 36 | cwd=cls.app_dir, 37 | stdout=subprocess.PIPE, 38 | stderr=subprocess.STDOUT, 39 | close_fds=False, 40 | env=env, 41 | ) 42 | lines = p.communicate()[0] 43 | if p.wait() != 0: 44 | print(lines) 45 | raise AssertionError("Creating basic_app bundle failed") 46 | 47 | @classmethod 48 | def tearDownClass(cls): 49 | if os.path.exists(os.path.join(cls.app_dir, "build")): 50 | shutil.rmtree(os.path.join(cls.app_dir, "build")) 51 | 52 | if os.path.exists(os.path.join(cls.app_dir, "dist")): 53 | shutil.rmtree(os.path.join(cls.app_dir, "dist")) 54 | 55 | time.sleep(2) 56 | 57 | def tearDown(self): 58 | kill_child_processes() 59 | time.sleep(1) 60 | 61 | def assertContentsEqual(self, src_file, dst_file): 62 | fp = open(src_file, "rb") 63 | src_data = fp.read() 64 | fp.close() 65 | 66 | fp = open(dst_file, "rb") 67 | dst_data = fp.read() 68 | fp.close() 69 | 70 | self.assertEqual(src_data, dst_data) 71 | 72 | def test_resources(self): 73 | resource_dir = os.path.join( 74 | self.app_dir, "dist", "Resources.app", "Contents", "Resources" 75 | ) 76 | 77 | self.assertFalse(os.path.exists(os.path.join(resource_dir, "MainMenu.xib"))) 78 | self.assertTrue(os.path.exists(os.path.join(resource_dir, "MainMenu.nib"))) 79 | 80 | # XXX: Need to test for other resource types as well, this 81 | # will do for now to test that the basic functionality works. 82 | 83 | 84 | class TestBasicAliasApp(TestBasicApp): 85 | py2app_args = [ 86 | "--alias", 87 | ] 88 | 89 | 90 | class TestBasicSemiStandaloneApp(TestBasicApp): 91 | py2app_args = [ 92 | "--semi-standalone", 93 | ] 94 | -------------------------------------------------------------------------------- /py2app_tests/test_shell_environment.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testcase for checking emulate_shell_environment 3 | """ 4 | 5 | import os 6 | import shutil 7 | import subprocess 8 | import sys 9 | import time 10 | import unittest 11 | 12 | import py2app 13 | 14 | from .tools import kill_child_processes 15 | 16 | DIR_NAME = os.path.dirname(os.path.abspath(__file__)) 17 | 18 | 19 | class TestShellEnvironment(unittest.TestCase): 20 | py2app_args = [] 21 | setup_file = "setup.py" 22 | app_dir = os.path.join(DIR_NAME, "shell_app") 23 | 24 | # Basic setup code 25 | # 26 | # The code in this block needs to be moved to 27 | # a base-class. 28 | @classmethod 29 | def setUpClass(cls): 30 | cls.tearDownClass() 31 | 32 | kill_child_processes() 33 | 34 | env = os.environ.copy() 35 | pp = os.path.dirname(os.path.dirname(py2app.__file__)) 36 | if "PYTHONPATH" in env: 37 | env["PYTHONPATH"] = pp + ":" + env["PYTHONPATH"] 38 | else: 39 | env["PYTHONPATH"] = pp 40 | 41 | p = subprocess.Popen( 42 | [sys.executable, cls.setup_file, "py2app"] + cls.py2app_args, 43 | cwd=cls.app_dir, 44 | stdout=subprocess.PIPE, 45 | stderr=subprocess.STDOUT, 46 | close_fds=False, 47 | env=env, 48 | ) 49 | lines = p.communicate()[0] 50 | if p.wait() != 0: 51 | print(lines) 52 | raise AssertionError("Creating basic_app bundle failed") 53 | 54 | @classmethod 55 | def tearDownClass(cls): 56 | if os.path.exists(os.path.join(cls.app_dir, "build")): 57 | shutil.rmtree(os.path.join(cls.app_dir, "build")) 58 | 59 | if os.path.exists(os.path.join(cls.app_dir, "dist")): 60 | shutil.rmtree(os.path.join(cls.app_dir, "dist")) 61 | 62 | time.sleep(2) 63 | 64 | def tearDown(self): 65 | kill_child_processes() 66 | time.sleep(1) 67 | 68 | # 69 | # End of setup code 70 | # 71 | 72 | def test_shell_environment(self): 73 | self.maxDiff = None 74 | path = os.path.join(self.app_dir, "dist/BasicApp.app") 75 | 76 | p = subprocess.Popen(["/usr/bin/open", "-a", path]) 77 | status = p.wait() 78 | 79 | self.assertEqual(status, 0) 80 | 81 | path = os.path.join(self.app_dir, "dist/env.txt") 82 | for _ in range(25): 83 | time.sleep(1) 84 | if os.path.exists(path): 85 | break 86 | 87 | self.assertTrue(os.path.isfile(path), f"{path!r} is not a file") 88 | 89 | fp = open(path) 90 | data = fp.read().strip() 91 | fp.close() 92 | 93 | env = eval(data) 94 | path = env["PATH"] 95 | 96 | self.assertNotEqual(path, "/usr/bin:/bin") 97 | 98 | elems = path.split(":") 99 | self.assertIn("/usr/bin", elems) 100 | -------------------------------------------------------------------------------- /doc/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | * "Mach-O header may be too large to relocate" 5 | 6 | Py2app will fail with a relocation error when 7 | it cannot rewrite the load commands in shared 8 | libraries and binaries copied into the application 9 | or plugin bundle. 10 | 11 | This error can be avoided by rebuilding binaries 12 | with enough space in the Mach-O headers, either 13 | by using the linker flag "-headerpad_max_install_names" 14 | or by installing shared libraries in a deeply 15 | nested location (the path for the install root needs 16 | to be at least 30 characters long). 17 | 18 | * M1 Macs and libraries not available for arm64 19 | 20 | A lot of libraries are not yet available as arm64 or 21 | universal2 libraries. 22 | 23 | For applications using those libraries you can 24 | create an x86_64 (Intel) application instead: 25 | 26 | 1. Create a new virtual environment and activate this 27 | 28 | 2. Use ``arch -x86_64 python -mpip install ...`` to 29 | install libraries. 30 | 31 | The ``arch`` command is necessary here to ensure 32 | that pip selects variants that are compatible with 33 | the x86_64 architecture instead of arm64. 34 | 35 | 36 | 3. Use ``arch -x86_64 python setup.py py2app --arch x86_64`` 37 | to build 38 | 39 | This results in an application bundle where the 40 | launcher is an x86_64 only binary, and where included 41 | C extensions and libraries are compatible with that architecture 42 | as well. 43 | 44 | * Using Cython with py2app 45 | 46 | Cython generates C extensions. Because of that the dependency 47 | walker in py2app cannot find import statements in ".pyx" files". 48 | 49 | To create working applications you have to ensure that 50 | dependencies are made visible to py2app, either by adding 51 | import statements to a python file that is included in the 52 | application, or by using the "includes" option. 53 | 54 | See examples/PyQt/cython_app in the repository for an 55 | example of the latter. 56 | 57 | * Dark mode support 58 | 59 | .. note:: 60 | 61 | As of py2app 0.26 the stub executables are compiled with 62 | a modern SDK, with an automatic fallback to the older binaries 63 | for old builds of Tkinter. 64 | 65 | The stub executables from py2app were compiled on an 66 | old version of macOS and therefore the system assumes 67 | that applications build with py2app do not support Dark Mode 68 | unless you're building a "Universal 2" or "Apple Silicon" 69 | application. 70 | 71 | To enable Dark Mode support for other builds of Python you 72 | need to add a key to the Info.plist file. The easiest way 73 | to do this is using the following option in setup.py: 74 | 75 | .. sourcecode:: python 76 | 77 | setup( 78 | ... 79 | options=dict( 80 | py2app=dict( 81 | plist=dict( 82 | NSRequiresAquaSystemAppearance=False 83 | ) 84 | ) 85 | ) 86 | ) 87 | -------------------------------------------------------------------------------- /py2app_tests/app_with_shared_ctypes/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shlex 4 | import shutil 5 | import subprocess 6 | from distutils.sysconfig import get_config_var 7 | 8 | from setuptools import Command, setup 9 | 10 | 11 | class sharedlib(Command): 12 | description = "build a shared library" 13 | user_options = [] 14 | 15 | def initialize_options(self): 16 | pass 17 | 18 | def finalize_options(self): 19 | pass 20 | 21 | def run(self): 22 | cc = ["xcrun", "clang"] 23 | env = dict(os.environ) 24 | env["MACOSX_DEPLOYMENT_TARGET"] = get_config_var("MACOSX_DEPLOYMENT_TARGET") 25 | 26 | if not os.path.exists("lib"): 27 | os.mkdir("lib") 28 | cflags = get_config_var("CFLAGS") 29 | arch_flags = sum( 30 | (shlex.split(x) for x in re.findall(r"-arch\s+\S+", cflags)), [] 31 | ) 32 | root_flags = sum( 33 | (shlex.split(x) for x in re.findall(r"-isysroot\s+\S+", cflags)), [] 34 | ) 35 | 36 | cmd = ( 37 | cc 38 | + arch_flags 39 | + root_flags 40 | + [ 41 | "-dynamiclib", 42 | "-o", 43 | os.path.abspath("lib/libshared.1.dylib"), 44 | "src/sharedlib.c", 45 | ] 46 | ) 47 | subprocess.check_call(cmd, env=env) 48 | if os.path.exists("lib/libshared.dylib"): 49 | os.unlink("lib/libshared.dylib") 50 | os.symlink("libshared.1.dylib", "lib/libshared.dylib") 51 | 52 | if not os.path.exists("lib/stash"): 53 | os.makedirs("lib/stash") 54 | 55 | if os.path.exists("lib/libhalf.dylib"): 56 | os.unlink("lib/libhalf.dylib") 57 | 58 | cmd = ( 59 | cc 60 | + arch_flags 61 | + root_flags 62 | + [ 63 | "-dynamiclib", 64 | "-o", 65 | os.path.abspath("lib/libhalf.dylib"), 66 | "src/sharedlib.c", 67 | ] 68 | ) 69 | subprocess.check_call(cmd, env=env) 70 | 71 | os.rename("lib/libhalf.dylib", "lib/stash/libhalf.dylib") 72 | os.symlink("stash/libhalf.dylib", "lib/libhalf.dylib") 73 | 74 | 75 | class cleanup(Command): 76 | description = "cleanup build stuff" 77 | user_options = [] 78 | 79 | def initialize_options(self): 80 | pass 81 | 82 | def finalize_options(self): 83 | pass 84 | 85 | def run(self): 86 | for dn in ("lib", "build", "dist"): 87 | if os.path.exists(dn): 88 | shutil.rmtree(dn) 89 | 90 | for fn in os.listdir("."): 91 | if fn.endswith(".so"): 92 | os.unlink(fn) 93 | 94 | 95 | setup( 96 | name="BasicApp", 97 | app=["main.py"], 98 | cmdclass={ 99 | "sharedlib": sharedlib, 100 | "cleanup": cleanup, 101 | }, 102 | options={ 103 | "py2app": { 104 | "frameworks": ["lib/libshared.dylib"], 105 | } 106 | }, 107 | ) 108 | -------------------------------------------------------------------------------- /py2app_tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import textwrap 3 | import unittest 4 | 5 | from py2app import util 6 | 7 | 8 | class TestVersionExtraction(unittest.TestCase): 9 | def assert_version_equals(self, source, version): 10 | with tempfile.NamedTemporaryFile("w") as stream: 11 | stream.write(source) 12 | stream.flush() 13 | 14 | found = util.find_version(stream.name) 15 | self.assertEqual(found, version) 16 | 17 | def test_no_version(self): 18 | self.assert_version_equals("x = 1", None) 19 | 20 | def test_simple_version(self): 21 | self.assert_version_equals("__version__ = 'a'", "a") 22 | 23 | def test_multi_part(self): 24 | self.assert_version_equals("__version__ = 'a' 'b'", "ab") 25 | 26 | def test_multi_target(self): 27 | self.assert_version_equals("b = __version__ = 'c'", "c") 28 | 29 | def test_multi_target2(self): 30 | self.assert_version_equals("b[x] = __version__ = 'c'", "c") 31 | 32 | def test_not_string(self): 33 | self.assert_version_equals("__version__ = 42", None) 34 | 35 | def test_expression(self): 36 | self.assert_version_equals("b = __version__ = 'c'.upper()", None) 37 | 38 | def test_syntax_error(self): 39 | with self.assertRaises(SyntaxError): 40 | self.assert_version_equals("__version__ = 42k", None) 41 | 42 | def test_multiple_assigments(self): 43 | self.assert_version_equals( 44 | textwrap.dedent( 45 | """\ 46 | __version__ = 'a' 47 | def foo(self): 48 | a = 42 49 | __version__ = 'z' 50 | """ 51 | ), 52 | "z", 53 | ) 54 | 55 | def test_last_invalid(self): 56 | self.assert_version_equals( 57 | textwrap.dedent( 58 | """\ 59 | __version__ = 'a' 60 | def foo(self): 61 | a = 42 62 | __version__ = 42 63 | """ 64 | ), 65 | None, 66 | ) 67 | 68 | def test_in_ifstatement(self): 69 | self.assert_version_equals( 70 | textwrap.dedent( 71 | """\ 72 | if a == 42: 73 | __version__ = 'a' 74 | """ 75 | ), 76 | None, 77 | ) 78 | 79 | def test_in_forstatement(self): 80 | self.assert_version_equals( 81 | textwrap.dedent( 82 | """\ 83 | for a in 42: 84 | __version__ = 'a' 85 | """ 86 | ), 87 | None, 88 | ) 89 | 90 | def test_in_whilestatement(self): 91 | self.assert_version_equals( 92 | textwrap.dedent( 93 | """\ 94 | while a: 95 | __version__ = 'a' 96 | """ 97 | ), 98 | None, 99 | ) 100 | 101 | def test_in_function(self): 102 | self.assert_version_equals( 103 | textwrap.dedent( 104 | """\ 105 | __version__ = 'a' 106 | def f(): 107 | __version__ = 'b' 108 | """ 109 | ), 110 | "a", 111 | ) 112 | -------------------------------------------------------------------------------- /py2app_tests/test_lsenvironment.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testcase for checking argv_emulation 3 | """ 4 | 5 | import ast # noqa: F401 6 | import os 7 | import platform # noqa: F401 8 | import shutil 9 | import signal # noqa: F401 10 | import subprocess 11 | import sys 12 | import time 13 | import unittest 14 | 15 | import py2app # noqa: F401 16 | 17 | from .tools import kill_child_processes 18 | 19 | DIR_NAME = os.path.dirname(os.path.abspath(__file__)) 20 | 21 | 22 | class TestLSEnvironment(unittest.TestCase): 23 | py2app_args = [] 24 | setup_file = "setup.py" 25 | app_dir = os.path.join(DIR_NAME, "app_with_environment") 26 | 27 | # Basic setup code 28 | # 29 | # The code in this block needs to be moved to 30 | # a base-class. 31 | @classmethod 32 | def setUpClass(cls): 33 | kill_child_processes() 34 | 35 | env = os.environ.copy() 36 | pp = os.path.dirname(os.path.dirname(py2app.__file__)) 37 | if "PYTHONPATH" in env: 38 | env["PYTHONPATH"] = pp + ":" + env["PYTHONPATH"] 39 | else: 40 | env["PYTHONPATH"] = pp 41 | 42 | p = subprocess.Popen( 43 | [sys.executable, cls.setup_file, "py2app"] + cls.py2app_args, 44 | cwd=cls.app_dir, 45 | stdout=subprocess.PIPE, 46 | stderr=subprocess.STDOUT, 47 | close_fds=False, 48 | env=env, 49 | ) 50 | lines = p.communicate()[0] 51 | if p.wait() != 0: 52 | print(lines) 53 | raise AssertionError("Creating basic_app bundle failed") 54 | 55 | @classmethod 56 | def tearDownClass(cls): 57 | if os.path.exists(os.path.join(cls.app_dir, "build")): 58 | shutil.rmtree(os.path.join(cls.app_dir, "build")) 59 | 60 | if os.path.exists(os.path.join(cls.app_dir, "dist")): 61 | shutil.rmtree(os.path.join(cls.app_dir, "dist")) 62 | 63 | time.sleep(2) 64 | 65 | def tearDown(self): 66 | kill_child_processes() 67 | time.sleep(1) 68 | 69 | # 70 | # End of setup code 71 | # 72 | 73 | def test_basic_start(self): 74 | self.maxDiff = None 75 | 76 | path = os.path.join(self.app_dir, "dist/env.txt") 77 | if os.path.exists(path): 78 | os.unlink(path) 79 | 80 | path = os.path.join(self.app_dir, "dist/BasicApp.app") 81 | 82 | proc = subprocess.Popen(["/usr/bin/open", path]) 83 | status = proc.wait() 84 | 85 | if status == 1: 86 | print("/usr/bin/open failed, retry") 87 | time.sleep(5) 88 | proc = subprocess.Popen(["/usr/bin/open", path]) 89 | status = proc.wait() 90 | 91 | self.assertEqual(status, 0) 92 | 93 | path = os.path.join(self.app_dir, "dist/env.txt") 94 | for _ in range(70): # Argv emulation can take up-to 60 seconds 95 | time.sleep(0.1) 96 | if os.path.exists(path): 97 | break 98 | 99 | self.assertTrue(os.path.isfile(path)) 100 | 101 | fp = open(path) 102 | data = fp.read().strip() 103 | fp.close() 104 | 105 | env = ast.literal_eval(data) 106 | self.assertEqual(env["KNIGHT"], "ni!") 107 | self.assertEqual(env["EXTRA_VAR"], "hello world") 108 | self.assertEqual(env["LANG"], "nl_NL.latin1") 109 | self.assertEqual(env["LC_CTYPE"], "nl_NL.UTF-8") 110 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "py2app" 7 | authors = [ 8 | { name = "Ronald Oussoren", email = "ronaldoussoren@mac.com" } 9 | ] 10 | description = "Create standalone macOS applications with Python" 11 | dynamic = [ "version"] 12 | requires-python = ">=3.8,<4" 13 | readme = "README.rst" 14 | license = { text = "MIT" } 15 | keywords=[".app", "standalone"] 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Environment :: Console", 19 | "Environment :: MacOS X :: Cocoa", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Natural Language :: English", 23 | "Operating System :: MacOS :: MacOS X", 24 | "Programming Language :: Python", 25 | "Programming Language :: Python :: 3", 26 | "Programming Language :: Python :: 3 :: Only", 27 | "Programming Language :: Python :: 3.6", 28 | "Programming Language :: Python :: 3.7", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Programming Language :: Python :: 3.13", 35 | "Programming Language :: Objective C", 36 | "Topic :: Software Development :: Libraries :: Python Modules", 37 | "Topic :: Software Development :: User Interfaces", 38 | "Topic :: Software Development :: Build Tools", 39 | ] 40 | dependencies=[ 41 | "setuptools", 42 | "packaging", 43 | "rich >= 12.0", 44 | "altgraph>=0.17", 45 | "modulegraph>=0.19", 46 | "modulegraph2", 47 | "macholib>=1.16", 48 | "importlib_metadata>=4.7; python_version < '3.10'", 49 | "importlib_resources; python_version < '3.10'", 50 | "tomli; python_version < '3.11'", 51 | ] 52 | 53 | [project.optional-dependencies] 54 | setuptools = [ 55 | "setuptools >= 65", 56 | ] 57 | 58 | 59 | [project.urls] 60 | "Documentation" = "https://py2app.readthedocs.io/" 61 | "Source Code" = "https://github.com/ronaldoussoren/py2app/" 62 | "Issue Tracker" = "https://github.com/ronaldoussoren/py2app/issues" 63 | "Supporting" = "https://blog.ronaldoussoren.net/support/" 64 | 65 | #[project.scripts] 66 | #py2app = "py2app.__main__:main" 67 | 68 | [project.entry-points."setuptools.finalize_distribution_options"] 69 | py2app = "py2app._setuptools_stub:finalize_distribution_options" 70 | 71 | [project.entry-points."distutils.commands"] 72 | py2app = "py2app._setuptools_stub:py2app" 73 | 74 | [project.entry-points."distutils.setup_keywords"] 75 | app = "py2app._setuptools_stub:validate_target" 76 | plugin = "py2app._setuptools_stub:validate_target" 77 | 78 | [project.entry-points."py2app.converter"] 79 | xib = "py2app.converters.nibfile:convert_xib" 80 | nib = "py2app.converters.nibfile:convert_nib" 81 | xcdatamodel = "py2app.converters.coredata:convert_datamodel" 82 | xcmappingmodel = "py2app.converters.coredata:convert_mappingmodel" 83 | 84 | [tool.flit.sdist] 85 | exclude = [".github"] 86 | 87 | # This includes prebuild stub executables, even though those are 88 | # depedant on the python version. 89 | # XXX: Move back to setuptools to better control wheel creation 90 | # - do not include launcher stubs in sdist 91 | # - generate the launcher stubs when building a wheel 92 | # - wheel is python-version specific (due to launcher binaries) 93 | # XXX: All of these should be automated to enable using trusted 94 | # publishers on PyPI for releases. 95 | include = ["src/py2app/_apptemplate/launcher-*"] 96 | -------------------------------------------------------------------------------- /src/py2app/recipes/PIL/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import typing 4 | from io import StringIO 5 | 6 | from modulegraph.modulegraph import ModuleGraph 7 | from modulegraph.util import imp_find_module 8 | 9 | from ... import build_app 10 | from .._types import RecipeInfo 11 | 12 | 13 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 14 | m = mf.findNode("Image") or mf.findNode("PIL.Image") 15 | if m is None or m.filename is None: 16 | return None 17 | 18 | have_PIL = bool(mf.findNode("PIL.Image")) 19 | 20 | plugins = set() 21 | visited = set() 22 | 23 | # XXX: Most users should now use Pillow, which always uses 24 | # "PIL.Image", which can simply the code below. 25 | for folder in sys.path: 26 | if not isinstance(folder, str): 27 | continue 28 | 29 | for extra in ("", "PIL"): 30 | folder = os.path.realpath(os.path.join(folder, extra)) 31 | if (not os.path.isdir(folder)) or (folder in visited): 32 | continue 33 | for fn in os.listdir(folder): 34 | if not fn.endswith("ImagePlugin.py"): 35 | continue 36 | 37 | mod, ext = os.path.splitext(fn) 38 | try: 39 | sys.path.insert(0, folder) 40 | imp_find_module(mod) 41 | del sys.path[0] 42 | except ImportError: 43 | pass 44 | else: 45 | plugins.add(mod) 46 | visited.add(folder) 47 | s = StringIO("_recipes_pil_prescript(%r)\n" % list(plugins)) 48 | print(plugins) 49 | plugins = set() 50 | # sys.exit(1) 51 | for plugin in plugins: 52 | if have_PIL: 53 | mf.implyNodeReference(m, "PIL." + plugin) 54 | else: 55 | mf.implyNodeReference(m, plugin) 56 | 57 | mf.removeReference(m, "FixTk") 58 | # Since Imaging-1.1.5, SpiderImagePlugin imports ImageTk conditionally. 59 | # This is not ever used unless the user is explicitly using Tk elsewhere. 60 | sip = mf.findNode("SpiderImagePlugin") 61 | if sip is not None: 62 | mf.removeReference(sip, "ImageTk") 63 | 64 | # The ImageQt plugin should only be useful when using PyQt, which 65 | # would then be explicitly imported. 66 | # Note: this code doesn't have the right side-effect at the moment 67 | # due to the way the PyQt5 recipe is structured. 68 | sip = mf.findNode("PIL.ImageQt") 69 | if sip is not None: 70 | mf.removeReference(sip, "PyQt5") 71 | mf.removeReference(sip, "PyQt5.QtGui") 72 | mf.removeReference(sip, "PyQt5.QtCore") 73 | 74 | mf.removeReference(sip, "PyQt4") 75 | mf.removeReference(sip, "PyQt4.QtGui") 76 | mf.removeReference(sip, "PyQt4.QtCore") 77 | pass 78 | 79 | imagefilter = mf.findNode("PIL.ImageFilter") 80 | if imagefilter is not None: 81 | # Optional dependency on numpy to process 82 | # numpy data passed into the filter. Remove 83 | # this reference to ensure numpy is only copied 84 | # when it is actually used in the application. 85 | mf.removeReference(imagefilter, "numpy") 86 | 87 | image = mf.findNode("PIL.Image") 88 | if image is not None: 89 | # Optional dependency on numpy to convert 90 | # to a numpy array. 91 | mf.removeReference(image, "numpy") 92 | 93 | return { 94 | "prescripts": ["py2app.recipes.PIL.prescript", s], 95 | "includes": ["PIL.JpegPresets"], # import from PIL.JpegPlugin in Pillow 2.0 96 | "flatpackages": [os.path.dirname(m.filename)], 97 | } 98 | -------------------------------------------------------------------------------- /src/py2app/recipes/setuptools.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import textwrap 4 | import typing 5 | from io import StringIO 6 | 7 | from modulegraph.modulegraph import ModuleGraph 8 | 9 | from .. import build_app 10 | from ._types import RecipeInfo 11 | 12 | PRESCRIPT = textwrap.dedent( 13 | """\ 14 | import pkg_resources, zipimport, os 15 | 16 | def find_eggs_in_zip(importer, path_item, only=False): 17 | if importer.archive.endswith('.whl'): 18 | # wheels are not supported with this finder 19 | # they don't have PKG-INFO metadata, and won't ever contain eggs 20 | return 21 | 22 | metadata = pkg_resources.EggMetadata(importer) 23 | if metadata.has_metadata('PKG-INFO'): 24 | yield Distribution.from_filename(path_item, metadata=metadata) 25 | for subitem in metadata.resource_listdir(''): 26 | if not only and pkg_resources._is_egg_path(subitem): 27 | subpath = os.path.join(path_item, subitem) 28 | dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath) 29 | for dist in dists: 30 | yield dist 31 | elif subitem.lower().endswith(('.dist-info', '.egg-info')): 32 | subpath = os.path.join(path_item, subitem) 33 | submeta = pkg_resources.EggMetadata(zipimport.zipimporter(subpath)) 34 | submeta.egg_info = subpath 35 | yield pkg_resources.Distribution.from_location(path_item, subitem, submeta) 36 | 37 | def _fixup_pkg_resources(): 38 | pkg_resources.register_finder(zipimport.zipimporter, find_eggs_in_zip) 39 | pkg_resources.working_set.entries = [] 40 | list(map(pkg_resources.working_set.add_entry, sys.path)) 41 | 42 | _fixup_pkg_resources() 43 | """ 44 | ) 45 | 46 | 47 | def check(cmd: "build_app.py2app", mf: ModuleGraph) -> typing.Optional[RecipeInfo]: 48 | m = mf.findNode("pkg_resources") 49 | if m is None or m.filename is None: 50 | return None 51 | 52 | if m.filename.endswith("__init__.py"): 53 | vendor_dir = os.path.join(os.path.dirname(m.filename), "_vendor") 54 | else: 55 | vendor_dir = os.path.join(m.filename, "_vendor") 56 | 57 | expected_missing_imports = { 58 | "__main__.__requires__", 59 | } 60 | 61 | if os.path.exists(vendor_dir): 62 | for topdir, dirs, files in os.walk(vendor_dir): 63 | for fn in files: 64 | if fn in ("__pycache__", "__init__.py"): 65 | continue 66 | 67 | relnm = os.path.relpath(os.path.join(topdir, fn), vendor_dir) 68 | if relnm.endswith(".py"): 69 | relnm = relnm[:-3] 70 | relnm = relnm.replace("/", ".") 71 | 72 | if fn.endswith(".py"): 73 | mf.import_hook("pkg_resources._vendor." + relnm, m, ["*"]) 74 | expected_missing_imports.add("pkg_resources.extern." + relnm) 75 | for dn in dirs: 76 | if not os.path.exists(os.path.join(topdir, dn, "__init__.py")): 77 | continue 78 | relnm = os.path.relpath(os.path.join(topdir, dn), vendor_dir) 79 | relnm = relnm.replace("/", ".") 80 | 81 | mf.import_hook("pkg_resources._vendor." + relnm, m, ["*"]) 82 | expected_missing_imports.add("pkg_resources.extern." + relnm) 83 | 84 | mf.import_hook("pkg_resources._vendor", m) 85 | 86 | if sys.version[0] != 2: 87 | expected_missing_imports.add("__builtin__") 88 | 89 | return { 90 | "expected_missing_imports": expected_missing_imports, 91 | "prescripts": [StringIO(PRESCRIPT)], 92 | } 93 | -------------------------------------------------------------------------------- /doc/tweaking.rst: -------------------------------------------------------------------------------- 1 | Tweaking your Info.plist 2 | ======================== 3 | 4 | It's often useful to make some tweaks to your Info.plist file to change how 5 | your application behaves and interacts with Mac OS X. The most complete 6 | reference for the keys available to you is in Apple's 7 | `Runtime Configuration Guidelines`_. 8 | 9 | Commonly customized keys 10 | ------------------------ 11 | 12 | Here are some commonly customized property list keys relevant to py2app 13 | applications: 14 | 15 | ``CFBundleDocumentTypes``: 16 | An array of dictionaries describing document types supported by the bundle. 17 | Use this to associate your application with opening or editing document 18 | types, and/or to assign icons to document types. 19 | 20 | ``CFBundleGetInfoString``: 21 | The text shown by Finder's Get Info panel. 22 | 23 | ``CFBundleIdentifier``: 24 | The identifier string for your application (in reverse-domain syntax), 25 | e.g. ``"org.pythonmac.py2app"``. 26 | 27 | ``CFBundleURLTypes``: 28 | An array of dictionaries describing URL schemes supported by the bundle. 29 | 30 | ``LSBackgroundOnly``: 31 | If ``True``, the bundle will be a faceless background application. 32 | 33 | ``LSUIElement``: 34 | If ``True``, the bundle will be an agent application. It will not appear 35 | in the Dock or Force Quit window, but still can come to the foreground 36 | and present a UI. 37 | 38 | ``NSServices``: 39 | An array of dictionaries specifying the services provided by the 40 | application. 41 | 42 | 43 | Specifying customizations 44 | ------------------------- 45 | 46 | There are three ways to specify ``Info.plist`` customizations to py2app. 47 | 48 | You can specify an Info.plist XML file on the command-line with the 49 | ``--plist`` option, or as a string in your ``setup.py``:: 50 | 51 | setup( 52 | app=['MyApplication.py'], 53 | options=dict(py2app=dict( 54 | plist='Info.plist', 55 | )), 56 | ) 57 | 58 | You may also specify the plist as a Python dict in the ``setup.py``:: 59 | 60 | setup( 61 | app=['MyApplication.py'], 62 | options=dict(py2app=dict( 63 | plist=dict( 64 | LSPrefersPPC=True, 65 | ), 66 | )), 67 | ) 68 | 69 | Or you may use a hybrid approach using the standard library plistlib module:: 70 | 71 | from plistlib import Plist 72 | plist = Plist.fromFile('Info.plist') 73 | plist.update(dict( 74 | LSPrefersPPC=True, 75 | )) 76 | setup( 77 | app=['MyApplication.py'], 78 | options=dict(py2app=dict( 79 | plist=plist, 80 | )), 81 | ) 82 | 83 | 84 | Universal Binaries 85 | ------------------ 86 | 87 | .. note:: the documentation about universal binaries is outdated! 88 | 89 | py2app is currently fully compatible with Universal Binaries, however 90 | it does not try and detect which architectures your application will 91 | correctly run on. 92 | 93 | If you are building your application with a version of Python that is not 94 | universal, or have extensions that are not universal, then you must set 95 | the ``LSPrefersPPC`` Info.plist key to ``True``. This will force the 96 | application to run translated with Rosetta by default. This is necessary 97 | because the py2app bootstrap application is universal, so Finder 98 | will try and launch natively by default. 99 | 100 | Alternatively, the ``--prefer-ppc`` option can be used as a shortcut to 101 | ensure that this Info.plist key is set. 102 | 103 | .. _`Runtime Configuration Guidelines`: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPRuntimeConfig/000-Introduction/introduction.html#//apple_ref/doc/uid/10000170i 104 | -------------------------------------------------------------------------------- /py2app_tests/test_explicit_includes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import signal 4 | import subprocess 5 | import sys 6 | import time 7 | import unittest 8 | 9 | import py2app 10 | 11 | from .tools import kill_child_processes 12 | 13 | DIR_NAME = os.path.dirname(os.path.abspath(__file__)) 14 | 15 | 16 | class TestExplicitIncludes(unittest.TestCase): 17 | py2app_args = ["--includes=package1.subpackage.module,package3.mod"] 18 | 19 | python_args = [] 20 | app_dir = os.path.join(DIR_NAME, "basic_app") 21 | 22 | # Basic setup code 23 | # 24 | # The code in this block needs to be moved to 25 | # a base-class. 26 | @classmethod 27 | def setUpClass(cls): 28 | kill_child_processes() 29 | 30 | env = os.environ.copy() 31 | pp = os.path.dirname(os.path.dirname(py2app.__file__)) 32 | if "PYTHONPATH" in env: 33 | env["PYTHONPATH"] = pp + ":" + env["PYTHONPATH"] 34 | else: 35 | env["PYTHONPATH"] = pp 36 | 37 | if "LANG" not in env: 38 | # Ensure that testing though SSH works 39 | env["LANG"] = "en_US.UTF-8" 40 | 41 | p = subprocess.Popen( 42 | [sys.executable] 43 | + cls.python_args 44 | + ["setup.py", "py2app"] 45 | + cls.py2app_args, 46 | cwd=cls.app_dir, 47 | stdout=subprocess.PIPE, 48 | stderr=subprocess.STDOUT, 49 | close_fds=False, 50 | env=env, 51 | ) 52 | lines = p.communicate()[0] 53 | if p.wait() != 0: 54 | print(lines) 55 | raise AssertionError("Creating basic_app bundle failed") 56 | 57 | @classmethod 58 | def tearDownClass(cls): 59 | if os.path.exists(os.path.join(cls.app_dir, "build")): 60 | shutil.rmtree(os.path.join(cls.app_dir, "build")) 61 | 62 | if os.path.exists(os.path.join(cls.app_dir, "dist")): 63 | shutil.rmtree(os.path.join(cls.app_dir, "dist")) 64 | 65 | time.sleep(2) 66 | 67 | def tearDown(self): 68 | if hasattr(self, "_p"): 69 | try: 70 | self._p.communicate() 71 | except ValueError: 72 | pass 73 | 74 | self._p.send_signal(9) 75 | self._p.wait() 76 | kill_child_processes() 77 | time.sleep(1) 78 | 79 | def start_app(self): 80 | # Start the test app, return a subprocess object where 81 | # stdin and stdout are connected to pipes. 82 | path = os.path.join(self.app_dir, "dist/BasicApp.app/Contents/MacOS/BasicApp") 83 | 84 | self._p = p = subprocess.Popen( 85 | [path], 86 | stdin=subprocess.PIPE, 87 | stdout=subprocess.PIPE, 88 | close_fds=False, 89 | ) 90 | # stderr=subprocess.STDOUT) 91 | return p 92 | 93 | def wait_with_timeout(self, proc, timeout=10): 94 | for _ in range(timeout): 95 | x = proc.poll() 96 | if x is None: 97 | time.sleep(1) 98 | else: 99 | return x 100 | 101 | os.kill(proc.pid, signal.SIGKILL) 102 | return proc.wait() 103 | 104 | # 105 | # End of setup code 106 | # 107 | 108 | def test_simple_imports(self): 109 | p = self.start_app() 110 | 111 | # Basic module that is always present: 112 | p.stdin.write(b'import_module("package1.subpackage.module")\n') 113 | p.stdin.flush() 114 | ln = p.stdout.readline() 115 | self.assertEqual(ln.strip(), b"package1.subpackage.module") 116 | 117 | p.stdin.write(b'import_module("package3.mod")\n') 118 | p.stdin.flush() 119 | ln = p.stdout.readline() 120 | self.assertEqual(ln.strip(), b"package3.mod") 121 | --------------------------------------------------------------------------------