├── 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 |
--------------------------------------------------------------------------------