diff --git a/README.rst b/README.rst index 94c0ae94..2317b749 100644 --- a/README.rst +++ b/README.rst @@ -83,15 +83,6 @@ Each support package contains: * ``VERSIONS``, a text file describing the specific versions of code used to build the support package; -* ``platform-site``, a folder that contains site customization scripts that can be used - to make your local Python install look like it is an on-device install for each of the - underlying target architectures supported by the platform. This is needed because when - you run ``pip`` you'll be on a macOS machine with a specific architecture; if ``pip`` - tries to install a binary package, it will install a macOS binary wheel (which won't - work on iOS/tvOS/watchOS). However, if you add the ``platform-site`` folder to your - ``PYTHONPATH`` when invoking pip, the site customization will make your Python install - return ``platform`` and ``sysconfig`` responses consistent with on-device behavior, - which will cause ``pip`` to install platform-appropriate packages. * ``Python.xcframework``, a multi-architecture build of the Python runtime library On iOS/tvOS/watchOS, the ``Python.xcframework`` contains a @@ -105,6 +96,32 @@ needed to build packages. This is required because Xcode uses the ``xcrun`` alias to dynamically generate the name of binaries, but a lot of C tooling expects that ``CC`` will not contain spaces. +Each slice of an iOS/tvOS/watchOS XCframework also contains a +``platform-config`` folder with a subfolder for each supported architecture in +that slice. These subfolders can be used to make a macOS Python environment +behave as if it were on an iOS/tvOS/watchOS device. This works in one of two +ways: + +1. **A sitecustomize.py script**. If the ``platform-config`` subfolder is on + your ``PYTHONPATH`` when a Python interpreter is started, a site + customization will be applied that patches methods in ``sys``, ``sysconfig`` + and ``platform`` that are used to identify the system. + +2. **A make_cross_venv.py script**. If you call ``make_cross_venv.py``, + providing the location of a virtual environment, the script will add some + files to the ``site-packages`` folder of that environment that will + automatically apply the same set of patches as the ``sitecustomize.py`` + script whenever the environment is activated, without any need to modify + ``PYTHONPATH``. If you use ``build`` to create an isolated PEP 517 + environment to build a wheel, these patches will also be applied to the + isolated build environment that is created. + +iOS distributions also contain a copy of the iOS ``testbed`` project - an Xcode +project that can be used to run test suites of Python code. See the `CPython +documentation on testing packages +`__ for +details on how to use this testbed. + For a detailed instructions on using the support package in your own project, see the `usage guide <./USAGE.md>`__ diff --git a/USAGE.md b/USAGE.md index 40bbcf80..096f71b0 100644 --- a/USAGE.md +++ b/USAGE.md @@ -25,6 +25,75 @@ guides: For tvOS and watchOS, you should be able to broadly follow the instructions in the iOS guide. +### Using Objective C + +Once you've added the Python XCframework to your project, you'll need to +initialize the Python runtime in your Objective C code (This is step 10 of the +iOS guide linked above). This initialization should generally be done as early +as possible in the application's lifecycle, but definitely needs to be done +before you invoke Python code. + +As a *bare minimum*, you can do the following: + +1. Import the Python C API headers: + ```objc + #include + ``` + +2. Initialize the Python interpreter: + ```objc + NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; + NSString *pythonHome = [NSString stringWithFormat:@"%@/python", resourcePath, nil]; + NSString *pythonPath = [NSString stringWithFormat:@"%@/lib/python3.13", python_home, nil]; + NSString *libDynloadPath = [NSString stringWithFormat:@"%@/lib/python3.13/lib-dynload", python_home, nil]; + NSString *appPath = [NSString stringWithFormat:@"%@/app", resourcePath, nil]; + + setenv("PYTHONHOME", pythonHome, 1); + setenv("PYTHONPATH", [NSString stringWithFormat:@"%@:%@:%@", pythonpath, libDynloadPath, appPath, nil]); + + Py_Initialize(); + + // we now have a Python interpreter ready to be used + ``` + References to a specific Python version should reflect the version of + Python you are using. + +Again - this is the *bare minimum* initialization. In practice, you will likely +need to configure other aspects of the Python interpreter using the +`PyPreConfig` and `PyConfig` mechanisms. Consult the [Python documentation on +interpreter configuration](https://docs.python.org/3/c-api/init_config.html) for +more details on the configuration options that are available. You may find the +[bootstrap mainline code used by +Briefcase](https://github.com/beeware/briefcase-iOS-Xcode-template/blob/main/%7B%7B%20cookiecutter.format%20%7D%7D/%7B%7B%20cookiecutter.class_name%20%7D%7D/main.m) +a helpful point of comparison. + +### Using Swift + +If you want to use Swift instead of Objective C, the bare minimum initialization +code will look something like this: + +1. Import the Python framework: + ```swift + import Python + ``` + +2. Initialize the Python interpreter: + ```swift + guard let pythonHome = Bundle.main.path(forResource: "python", ofType: nil) else { return } + guard let pythonPath = Bundle.main.path(forResource: "python/lib/python3.13", ofType: nil) else { return } + guard let libDynloadPath = Bundle.main.path(forResource: "python/lib/python3.13/lib-dynload", ofType: nil) else { return } + let appPath = Bundle.main.path(forResource: "app", ofType: nil) + + setenv("PYTHONHOME", pythonHome, 1) + setenv("PYTHONPATH", [pythonPath, libDynloadPath, appPath].compactMap { $0 }.joined(separator: ":"), 1) + Py_Initialize() + // we now have a Python interpreter ready to be used + ``` + + Again, references to a specific Python version should reflect the version of + Python you are using; and you will likely need to use `PyPreConfig` and + `PreConfig` APIs. + ## Accessing the Python runtime There are 2 ways to access the Python runtime in your project code. @@ -32,53 +101,29 @@ There are 2 ways to access the Python runtime in your project code. ### Embedded C API You can use the [Python Embedded C -API](https://docs.python.org/3/extending/embedding.html) to instantiate a Python -interpreter. This is the approach taken by Briefcase; you may find the bootstrap -mainline code generated by Briefcase a helpful guide to what is needed to start -an interpreter and run Python code. +API](https://docs.python.org/3/extending/embedding.html) to invoke Python code +and interact with Python objects. This is a raw C API that is accesible to both +Objective C and Swift. ### PythonKit -An alternate approach is to use +If you're using Swift, an alternate approach is to use [PythonKit](https://github.com/pvieito/PythonKit). PythonKit is a package that provides a Swift API to running Python code. To use PythonKit in your project, add the Python Apple Support package to your -project as described above; then: - -1. Add PythonKit to your project using the Swift Package manager. See the - PythonKit documentation for details. +project and instantiate a Python interpreter as described above; then add +PythonKit to your project using the Swift Package manager (see the [PythonKit +documentation](https://github.com/pvieito/PythonKit) for details). -2. In your Swift code, initialize the Python runtime. This should generally be - done as early as possible in the application's lifecycle, but definitely - needs to be done before you invoke Python code. References to a specific - Python version should reflect the version of Python you are using: +Once you've done this, you can import PythonKit: ```swift -import Python - -guard let pythonHome = Bundle.main.path(forResource: "python", ofType: nil) else { return } -guard let pythonPath = Bundle.main.path(forResource: "python/lib/python3.13", ofType: nil) else { return } -guard let libDynloadPath = Bundle.main.path(forResource: "python/lib/python3.13/lib-dynload", ofType: nil) else { return } -let appPath = Bundle.main.path(forResource: "app", ofType: nil) - -setenv("PYTHONHOME", pythonHome, 1) -setenv("PYTHONPATH", [pythonPath, libDynloadPath, appPath].compactMap { $0 }.joined(separator: ":"), 1) -Py_Initialize() -// we now have a Python interpreter ready to be used +import PythonKit ``` - -3. Invoke Python code in your app. For example: +and use the PythonKit Swift API to interact with Python code: ```swift -import PythonKit - let sys = Python.import("sys") print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)") print("Python Encoding: \(sys.getdefaultencoding().upper())") print("Python Path: \(sys.path)") - -_ = Python.import("math") // verifies `lib-dynload` is found and signed successfully ``` - -To integrate 3rd party python code and dependencies, you will need to make sure -`PYTHONPATH` contains their paths; once this has been done, you can run -`Python.import("")`. to import that module from inside swift. diff --git a/patch/Python/module.modulemap b/patch/Python/module.modulemap index 9a4dcbb8..bc3b19e9 100644 --- a/patch/Python/module.modulemap +++ b/patch/Python/module.modulemap @@ -2,4 +2,164 @@ module Python { umbrella header "Python.h" export * link "Python" + + exclude header "datetime.h" + exclude header "dynamic_annotations.h" + exclude header "errcode.h" + exclude header "frameobject.h" + exclude header "marshal.h" + exclude header "opcode_ids.h" + exclude header "opcode.h" + exclude header "osdefs.h" + exclude header "py_curses.h" + exclude header "pyconfig-arm64.h" + exclude header "pyconfig-x86_64.h" + exclude header "pyconfig-arm32_64.h" + exclude header "pydtrace.h" + exclude header "pyexpat.h" + exclude header "structmember.h" + + exclude header "cpython/frameobject.h" + exclude header "cpython/pthread_stubs.h" + exclude header "cpython/pyatomic_msc.h" + exclude header "cpython/pyatomic_std.h" + exclude header "cpython/pystats.h" + + exclude header "internal/mimalloc/mimalloc.h" + exclude header "internal/mimalloc/mimalloc/atomic.h" + exclude header "internal/mimalloc/mimalloc/internal.h" + exclude header "internal/mimalloc/mimalloc/prim.h" + exclude header "internal/mimalloc/mimalloc/track.h" + exclude header "internal/mimalloc/mimalloc/types.h" + + exclude header "internal/pycore_abstract.h" + exclude header "internal/pycore_asdl.h" + exclude header "internal/pycore_ast_state.h" + exclude header "internal/pycore_ast.h" + exclude header "internal/pycore_atexit.h" + exclude header "internal/pycore_audit.h" + exclude header "internal/pycore_backoff.h" + exclude header "internal/pycore_bitutils.h" + exclude header "internal/pycore_blocks_output_buffer.h" + exclude header "internal/pycore_brc.h" + exclude header "internal/pycore_bytes_methods.h" + exclude header "internal/pycore_bytesobject.h" + exclude header "internal/pycore_call.h" + exclude header "internal/pycore_capsule.h" + exclude header "internal/pycore_cell.h" + exclude header "internal/pycore_ceval_state.h" + exclude header "internal/pycore_ceval.h" + exclude header "internal/pycore_code.h" + exclude header "internal/pycore_codecs.h" + exclude header "internal/pycore_compile.h" + exclude header "internal/pycore_complexobject.h" + exclude header "internal/pycore_condvar.h" + exclude header "internal/pycore_context.h" + exclude header "internal/pycore_critical_section.h" + exclude header "internal/pycore_crossinterp_data_registry.h" + exclude header "internal/pycore_crossinterp.h" + exclude header "internal/pycore_debug_offsets.h" + exclude header "internal/pycore_descrobject.h" + exclude header "internal/pycore_dict_state.h" + exclude header "internal/pycore_dict.h" + exclude header "internal/pycore_dtoa.h" + exclude header "internal/pycore_emscripten_signal.h" + exclude header "internal/pycore_emscripten_trampoline.h" + exclude header "internal/pycore_exceptions.h" + exclude header "internal/pycore_faulthandler.h" + exclude header "internal/pycore_fileutils_windows.h" + exclude header "internal/pycore_fileutils.h" + exclude header "internal/pycore_floatobject.h" + exclude header "internal/pycore_flowgraph.h" + exclude header "internal/pycore_format.h" + exclude header "internal/pycore_frame.h" + exclude header "internal/pycore_freelist_state.h" + exclude header "internal/pycore_freelist.h" + exclude header "internal/pycore_function.h" + exclude header "internal/pycore_gc.h" + exclude header "internal/pycore_genobject.h" + exclude header "internal/pycore_getopt.h" + exclude header "internal/pycore_gil.h" + exclude header "internal/pycore_global_objects_fini_generated.h" + exclude header "internal/pycore_global_objects.h" + exclude header "internal/pycore_global_strings.h" + exclude header "internal/pycore_hamt.h" + exclude header "internal/pycore_hashtable.h" + exclude header "internal/pycore_identifier.h" + exclude header "internal/pycore_import.h" + exclude header "internal/pycore_importdl.h" + exclude header "internal/pycore_index_pool.h" + exclude header "internal/pycore_initconfig.h" + exclude header "internal/pycore_instruction_sequence.h" + exclude header "internal/pycore_instruments.h" + exclude header "internal/pycore_interp.h" + exclude header "internal/pycore_intrinsics.h" + exclude header "internal/pycore_jit.h" + exclude header "internal/pycore_list.h" + exclude header "internal/pycore_llist.h" + exclude header "internal/pycore_lock.h" + exclude header "internal/pycore_long.h" + exclude header "internal/pycore_magic_number.h" + exclude header "internal/pycore_memoryobject.h" + exclude header "internal/pycore_mimalloc.h" + exclude header "internal/pycore_modsupport.h" + exclude header "internal/pycore_moduleobject.h" + exclude header "internal/pycore_namespace.h" + exclude header "internal/pycore_object_alloc.h" + exclude header "internal/pycore_object_deferred.h" + exclude header "internal/pycore_object_stack.h" + exclude header "internal/pycore_object_state.h" + exclude header "internal/pycore_object.h" + exclude header "internal/pycore_obmalloc_init.h" + exclude header "internal/pycore_obmalloc.h" + exclude header "internal/pycore_opcode_metadata.h" + exclude header "internal/pycore_opcode_utils.h" + exclude header "internal/pycore_optimizer.h" + exclude header "internal/pycore_parking_lot.h" + exclude header "internal/pycore_parser.h" + exclude header "internal/pycore_pathconfig.h" + exclude header "internal/pycore_pyarena.h" + exclude header "internal/pycore_pyatomic_ft_wrappers.h" + exclude header "internal/pycore_pybuffer.h" + exclude header "internal/pycore_pyerrors.h" + exclude header "internal/pycore_pyhash.h" + exclude header "internal/pycore_pylifecycle.h" + exclude header "internal/pycore_pymath.h" + exclude header "internal/pycore_pymem_init.h" + exclude header "internal/pycore_pymem.h" + exclude header "internal/pycore_pystate.h" + exclude header "internal/pycore_pystats.h" + exclude header "internal/pycore_pythonrun.h" + exclude header "internal/pycore_pythread.h" + exclude header "internal/pycore_qsbr.h" + exclude header "internal/pycore_range.h" + exclude header "internal/pycore_runtime_init_generated.h" + exclude header "internal/pycore_runtime_init.h" + exclude header "internal/pycore_runtime.h" + exclude header "internal/pycore_semaphore.h" + exclude header "internal/pycore_setobject.h" + exclude header "internal/pycore_signal.h" + exclude header "internal/pycore_sliceobject.h" + exclude header "internal/pycore_stackref.h" + exclude header "internal/pycore_strhex.h" + exclude header "internal/pycore_structseq.h" + exclude header "internal/pycore_symtable.h" + exclude header "internal/pycore_sysmodule.h" + exclude header "internal/pycore_time.h" + exclude header "internal/pycore_token.h" + exclude header "internal/pycore_traceback.h" + exclude header "internal/pycore_tracemalloc.h" + exclude header "internal/pycore_tstate.h" + exclude header "internal/pycore_tuple.h" + exclude header "internal/pycore_typeobject.h" + exclude header "internal/pycore_typevarobject.h" + exclude header "internal/pycore_ucnhash.h" + exclude header "internal/pycore_unicodeobject_generated.h" + exclude header "internal/pycore_unicodeobject.h" + exclude header "internal/pycore_unionobject.h" + exclude header "internal/pycore_uniqueid.h" + exclude header "internal/pycore_uop_ids.h" + exclude header "internal/pycore_uop_metadata.h" + exclude header "internal/pycore_warnings.h" + exclude header "internal/pycore_weakref.h" }