Merge remote-tracking branch 'origin/main' into windows-socket-sendfile

This commit is contained in:
AN Long 2025-12-02 23:39:34 +09:00
commit b153f22afe
3241 changed files with 530815 additions and 211222 deletions

View file

@ -1,11 +1,11 @@
trigger: ['main', '3.12', '3.11', '3.10', '3.9', '3.8', '3.7'] trigger: ['main', '3.*']
jobs: jobs:
- job: Prebuild - job: Prebuild
displayName: Pre-build checks displayName: Pre-build checks
pool: pool:
vmImage: ubuntu-22.04 vmImage: ubuntu-24.04
steps: steps:
- template: ./prebuild-checks.yml - template: ./prebuild-checks.yml

View file

@ -1,27 +0,0 @@
#!/bin/sh
apt-get update
apt-get -yq install \
build-essential \
zlib1g-dev \
libbz2-dev \
liblzma-dev \
libncurses5-dev \
libreadline6-dev \
libsqlite3-dev \
libssl-dev \
libgdbm-dev \
tk-dev \
lzma \
lzma-dev \
liblzma-dev \
libffi-dev \
uuid-dev \
xvfb
if [ ! -z "$1" ]
then
echo ##vso[task.prependpath]$PWD/multissl/openssl/$1
echo ##vso[task.setvariable variable=OPENSSL_DIR]$PWD/multissl/openssl/$1
python3 Tools/ssl/multissltests.py --steps=library --base-directory $PWD/multissl --openssl $1 --system Linux
fi

View file

@ -1,26 +0,0 @@
steps:
- checkout: self
clean: true
fetchDepth: 5
# Work around a known issue affecting Ubuntu VMs on Pipelines
- script: sudo setfacl -Rb /home/vsts
displayName: 'Workaround ACL issue'
- script: sudo ./.azure-pipelines/posix-deps-apt.sh $(openssl_version)
displayName: 'Install dependencies'
- script: ./configure --with-pydebug
displayName: 'Configure CPython (debug)'
- script: make -j4
displayName: 'Build CPython'
- script: make pythoninfo
displayName: 'Display build info'
- script: |
git fetch origin
./python Tools/patchcheck/patchcheck.py --ci true
displayName: 'Run patchcheck.py'
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))

View file

@ -1,28 +0,0 @@
pr: ['main', '3.12', '3.11', '3.10', '3.9', '3.8', '3.7']
jobs:
- job: Prebuild
displayName: Pre-build checks
pool:
vmImage: ubuntu-22.04
steps:
- template: ./prebuild-checks.yml
- job: Ubuntu_Patchcheck
displayName: Ubuntu patchcheck
dependsOn: Prebuild
condition: and(succeeded(), eq(dependencies.Prebuild.outputs['tests.run'], 'true'))
pool:
vmImage: ubuntu-22.04
variables:
testRunTitle: '$(system.pullRequest.TargetBranch)-linux'
testRunPlatform: linux
openssl_version: 1.1.1u
steps:
- template: ./posix-steps.yml

View file

@ -1,24 +0,0 @@
FROM docker.io/library/fedora:40
ENV CC=clang
ENV WASI_SDK_VERSION=21
ENV WASI_SDK_PATH=/opt/wasi-sdk
ENV WASMTIME_HOME=/opt/wasmtime
ENV WASMTIME_VERSION=18.0.3
ENV WASMTIME_CPU_ARCH=x86_64
RUN dnf -y --nodocs --setopt=install_weak_deps=False install /usr/bin/{blurb,clang,curl,git,ln,tar,xz} 'dnf-command(builddep)' && \
dnf -y --nodocs --setopt=install_weak_deps=False builddep python3 && \
dnf -y clean all
RUN mkdir ${WASI_SDK_PATH} && \
curl --location https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-linux.tar.gz | \
tar --strip-components 1 --directory ${WASI_SDK_PATH} --extract --gunzip
RUN mkdir --parents ${WASMTIME_HOME} && \
curl --location "https://github.com/bytecodealliance/wasmtime/releases/download/v${WASMTIME_VERSION}/wasmtime-v${WASMTIME_VERSION}-${WASMTIME_CPU_ARCH}-linux.tar.xz" | \
xz --decompress | \
tar --strip-components 1 --directory ${WASMTIME_HOME} -x && \
ln -s ${WASMTIME_HOME}/wasmtime /usr/local/bin

View file

@ -1,15 +1,10 @@
{ {
"build": { "image": "ghcr.io/python/devcontainer:latest",
"dockerfile": "Dockerfile"
},
"onCreateCommand": [ "onCreateCommand": [
// Install common tooling. // Install common tooling.
"dnf", "dnf",
"install", "install",
"-y", "-y",
"which",
"zsh",
"fish",
// For umask fix below. // For umask fix below.
"/usr/bin/setfacl" "/usr/bin/setfacl"
], ],

View file

@ -1,15 +1,15 @@
root = true root = true
[*.{py,c,cpp,h,js,rst,md,yml}] [*.{py,c,cpp,h,js,rst,md,yml,yaml,gram}]
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = space
[*.{py,c,cpp,h}] [*.{py,c,cpp,h,gram}]
indent_size = 4 indent_size = 4
[*.rst] [*.rst]
indent_size = 3 indent_size = 3
[*.{js,yml}] [*.{js,yml,yaml}]
indent_size = 2 indent_size = 2

5
.gitattributes vendored
View file

@ -10,6 +10,7 @@
*.ico binary *.ico binary
*.jpg binary *.jpg binary
*.pck binary *.pck binary
*.pdf binary
*.png binary *.png binary
*.psd binary *.psd binary
*.tar binary *.tar binary
@ -27,8 +28,6 @@ Lib/test/cjkencodings/* noeol
Lib/test/tokenizedata/coding20731.py noeol Lib/test/tokenizedata/coding20731.py noeol
Lib/test/decimaltestdata/*.decTest noeol Lib/test/decimaltestdata/*.decTest noeol
Lib/test/test_email/data/*.txt noeol Lib/test/test_email/data/*.txt noeol
Lib/test/test_importlib/resources/data01/* noeol
Lib/test/test_importlib/resources/namespacedata01/* noeol
Lib/test/xmltestdata/* noeol Lib/test/xmltestdata/* noeol
# Shell scripts should have LF even on Windows because of Cygwin # Shell scripts should have LF even on Windows because of Cygwin
@ -69,6 +68,7 @@ PCbuild/readme.txt dos
**/clinic/*.cpp.h generated **/clinic/*.cpp.h generated
**/clinic/*.h.h generated **/clinic/*.h.h generated
*_db.h generated *_db.h generated
Doc/c-api/lifecycle.dot.svg generated
Doc/data/stable_abi.dat generated Doc/data/stable_abi.dat generated
Doc/library/token-list.inc generated Doc/library/token-list.inc generated
Include/internal/pycore_ast.h generated Include/internal/pycore_ast.h generated
@ -103,3 +103,4 @@ Python/stdlib_module_names.h generated
Tools/peg_generator/pegen/grammar_parser.py generated Tools/peg_generator/pegen/grammar_parser.py generated
aclocal.m4 generated aclocal.m4 generated
configure generated configure generated
*.min.js generated

760
.github/CODEOWNERS vendored
View file

@ -1,111 +1,258 @@
# See https://help.github.com/articles/about-codeowners/ # See https://help.github.com/articles/about-codeowners/
# for more info about CODEOWNERS file # for further details about the .github/CODEOWNERS file.
# It uses the same pattern rule for gitignore file # It uses the same pattern rule for gitignore file
# https://git-scm.com/docs/gitignore#_pattern_format # https://git-scm.com/docs/gitignore#_pattern_format
# Notably, a later match overrides earlier matches, so order matters.
# If using a wildcard pattern, try to be as specific as possible to avoid
# matching unintended files or overriding previous entries.
# To exclude a file from ownership, add a line with only the file.
# See the exclusions section at the end of the file for examples.
# GitHub # =======
.github/** @ezio-melotti @hugovk # Purpose
# =======
#
# An entry in this file does not imply 'ownership', despite the name of the
# file, but instead that those listed take an interest in that part of the
# project and will automatically be added as reviewers to PRs that affect
# the matching files.
# See also the Experts Index in the Python Developer's Guide:
# https://devguide.python.org/core-developers/experts/
#
# =========
# Structure
# =========
#
# The CODEOWNERS file is organised by topic area.
# Please add new entries in alphabetical order within the relevant section.
# Where possible, keep related files together. For example, documentation,
# code, and tests for a given item should all be listed in the same place.
#
# GitHub usernames should be aligned to column 31, or the next multiple
# of three if the relevant paths are too long to fit.
#
# Top-level sections are:
#
# * Buildbots, Continuous Integration, and Testing
# project-wide configuration files, internal tools for use in CI,
# linting.
# * Build System
# the Makefile, autoconf, and other autotools files.
# * Documentation
# broader sections of documentation, documentation tools
# * Internal Tools & Data
# internal tools, integration with external systems,
# entries that don't fit elsewhere
# * Platform Support
# relating to support for specific platforms
# * Interpreter Core
# the grammar, parser, compiler, interpreter, etc.
# * Standard Library
# standard library modules (from both Lib and Modules)
# and related files (such as their tests and docs)
# * Exclusions
# exclusions from .github/CODEOWNERS should go at the very end
# because the final matching pattern will take precedence.
# pre-commit # ----------------------------------------------------------------------------
.pre-commit-config.yaml @hugovk @AlexWaygood # Buildbots, Continuous Integration, and Testing
.ruff.toml @hugovk @AlexWaygood # ----------------------------------------------------------------------------
# Build system # Azure Pipelines
configure* @erlend-aasland @corona10 .azure-pipelines/ @AA-Turner
# asyncio # GitHub & related scripts
**/*asyncio* @1st1 @asvetlov @gvanrossum @kumaraditya303 @willingc .github/ @ezio-melotti @hugovk @AA-Turner
Tools/build/compute-changes.py @AA-Turner
Tools/build/verify_ensurepip_wheels.py @AA-Turner @pfmoore @pradyunsg
# Core # Pre-commit
**/*context* @1st1 .pre-commit-config.yaml @hugovk
**/*genobject* @markshannon .ruff.toml @hugovk @AlexWaygood @AA-Turner
**/*hamt* @1st1
**/*jit* @brandtbucher # Patchcheck
Objects/set* @rhettinger Tools/patchcheck/ @AA-Turner
Objects/dict* @methane @markshannon
Objects/typevarobject.c @JelleZijlstra
Objects/type* @markshannon # ----------------------------------------------------------------------------
Objects/codeobject.c @markshannon # Build System
Objects/frameobject.c @markshannon # ----------------------------------------------------------------------------
Objects/call.c @markshannon
Python/ceval*.c @markshannon @gvanrossum # Autotools
Python/ceval*.h @markshannon @gvanrossum configure* @erlend-aasland @corona10 @AA-Turner @emmatyping
Python/compile.c @markshannon @iritkatriel Makefile.pre.in @erlend-aasland @AA-Turner @emmatyping
Python/assemble.c @markshannon @iritkatriel Modules/makesetup @erlend-aasland @AA-Turner @emmatyping
Python/flowgraph.c @markshannon @iritkatriel Modules/Setup* @erlend-aasland @AA-Turner @emmatyping
Python/ast_opt.c @isidentical Tools/build/regen-configure.sh @AA-Turner
Python/bytecodes.c @markshannon @gvanrossum
Python/optimizer*.c @markshannon @gvanrossum # generate-build-details
Python/optimizer_analysis.c @Fidget-Spinner Tools/build/generate-build-details.py @FFY00
Python/optimizer_bytecodes.c @Fidget-Spinner Lib/test/test_build_details.py @FFY00
Lib/test/test_patma.py @brandtbucher
Lib/test/test_type_*.py @JelleZijlstra
Lib/test/test_capi/test_misc.py @markshannon @gvanrossum # ----------------------------------------------------------------------------
# Documentation
# ----------------------------------------------------------------------------
# Internal Docs
InternalDocs/ @AA-Turner
# Tools, Configuration, etc
Doc/Makefile @AA-Turner @hugovk
Doc/_static/ @AA-Turner @hugovk
Doc/conf.py @AA-Turner @hugovk
Doc/make.bat @AA-Turner @hugovk
Doc/requirements.txt @AA-Turner @hugovk
Doc/tools/ @AA-Turner @hugovk
# PR Previews
.readthedocs.yml @AA-Turner
# Sections
Doc/reference/ @willingc @AA-Turner
Doc/whatsnew/ @AA-Turner
# ----------------------------------------------------------------------------
# Internal Tools and Data
# ----------------------------------------------------------------------------
# Argument Clinic
Tools/clinic/ @erlend-aasland @AA-Turner
Lib/test/test_clinic.py @erlend-aasland @AA-Turner
Doc/howto/clinic.rst @erlend-aasland @AA-Turner
# C Analyser
Tools/c-analyzer/ @ericsnowcurrently Tools/c-analyzer/ @ericsnowcurrently
# dbm # Fuzzing
**/*dbm* @corona10 @erlend-aasland @serhiy-storchaka Modules/_xxtestfuzz/ @ammaraskar
# runtime state/lifecycle # Limited C API & Stable ABI
**/*pylifecycle* @ericsnowcurrently Doc/c-api/stable.rst @encukou
**/*pystate* @ericsnowcurrently Doc/data/*.abi @encukou
**/*preconfig* @ericsnowcurrently Misc/stable_abi.toml @encukou
**/*initconfig* @ericsnowcurrently Tools/build/stable_abi.py @encukou
**/*pathconfig* @ericsnowcurrently
**/*sysmodule* @ericsnowcurrently # SBOM
Misc/externals.spdx.json @sethmlarson
Misc/sbom.spdx.json @sethmlarson
Tools/build/generate_sbom.py @sethmlarson
# ----------------------------------------------------------------------------
# Platform Support
# ----------------------------------------------------------------------------
# Android
Android/ @mhsmith @freakboy3742
Doc/using/android.rst @mhsmith @freakboy3742
Lib/_android_support.py @mhsmith @freakboy3742
Lib/test/test_android.py @mhsmith @freakboy3742
# iOS
Doc/using/ios.rst @freakboy3742
Lib/_ios_support.py @freakboy3742
Apple/ @freakboy3742
iOS/ @freakboy3742
# macOS
Mac/ @python/macos-team
Lib/_osx_support.py @python/macos-team
Lib/test/test__osx_support.py @python/macos-team
# WebAssembly
Tools/wasm/README.md @brettcannon @freakboy3742 @emmatyping
# WebAssembly (Emscripten)
Tools/wasm/config.site-wasm32-emscripten @freakboy3742 @emmatyping
Tools/wasm/emscripten @freakboy3742 @emmatyping
# WebAssembly (WASI)
Tools/wasm/wasi-env @brettcannon @emmatyping
Tools/wasm/wasi.py @brettcannon @emmatyping
Tools/wasm/wasi @brettcannon @emmatyping
# Windows
PC/ @python/windows-team
PCbuild/ @python/windows-team
# Windows installer packages
Tools/msi/ @python/windows-team
Tools/nuget/ @python/windows-team
# Windows Launcher
PC/launcher.c @python/windows-team @vsajip
# ----------------------------------------------------------------------------
# Interpreter Core
# ----------------------------------------------------------------------------
# AST
Lib/_ast_unparse.py @isidentical @JelleZijlstra @eclips4 @tomasr8
Lib/ast.py @isidentical @JelleZijlstra @eclips4 @tomasr8
Lib/test/test_ast/ @eclips4 @tomasr8
Parser/asdl.py @isidentical @JelleZijlstra @eclips4 @tomasr8
Parser/asdl_c.py @isidentical @JelleZijlstra @eclips4 @tomasr8
Python/ast.c @isidentical @JelleZijlstra @eclips4 @tomasr8
Python/ast_preprocess.c @isidentical @eclips4 @tomasr8
# Built-in types
Objects/call.c @markshannon
Objects/codeobject.c @markshannon
Objects/dict* @methane @markshannon
Objects/frameobject.c @markshannon
**/*genobject* @markshannon
Objects/object.c @ZeroIntensity
Objects/set* @rhettinger
Objects/type* @markshannon
Objects/typevarobject.c @JelleZijlstra
Objects/unionobject.c @JelleZijlstra
# Byte code interpreter ('the eval loop')
Python/bytecodes.c @markshannon
Python/ceval* @markshannon
Tools/cases_generator/ @markshannon
# Compiler (AST to byte code)
Python/assemble.c @markshannon @iritkatriel
Python/codegen.c @markshannon @iritkatriel
Python/compile.c @markshannon @iritkatriel
Python/flowgraph.c @markshannon @iritkatriel
Python/instruction_sequence.c @iritkatriel
Python/symtable.c @JelleZijlstra @carljm
# Context variables & HAMT
**/contextvars* @1st1
**/*hamt* @1st1
Include/cpython/context.h @1st1
Include/internal/pycore_context.h @1st1
Lib/test/test_context.py @1st1
Python/context.c @1st1
# Core Modules
**/*bltinmodule* @ericsnowcurrently **/*bltinmodule* @ericsnowcurrently
**/*gil* @ericsnowcurrently **/*sysmodule* @ericsnowcurrently
Include/internal/pycore_runtime.h @ericsnowcurrently
Include/internal/pycore_interp.h @ericsnowcurrently
Include/internal/pycore_tstate.h @ericsnowcurrently
Include/internal/pycore_*_state.h @ericsnowcurrently
Include/internal/pycore_*_init.h @ericsnowcurrently
Include/internal/pycore_atexit.h @ericsnowcurrently
Include/internal/pycore_freelist.h @ericsnowcurrently
Include/internal/pycore_global_objects.h @ericsnowcurrently
Include/internal/pycore_obmalloc.h @ericsnowcurrently
Include/internal/pycore_pymem.h @ericsnowcurrently
Modules/main.c @ericsnowcurrently
Programs/_bootstrap_python.c @ericsnowcurrently
Programs/python.c @ericsnowcurrently
Tools/build/generate_global_objects.py @ericsnowcurrently
# Exceptions # Exceptions
Lib/traceback.py @iritkatriel
Lib/test/test_except*.py @iritkatriel Lib/test/test_except*.py @iritkatriel
Lib/test/test_traceback.py @iritkatriel
Objects/exceptions.c @iritkatriel Objects/exceptions.c @iritkatriel
Python/traceback.c @iritkatriel
# Hashing # Getpath
**/*hashlib* @gpshead @tiran Lib/test/test_getpath.py @FFY00
**/*pyhash* @gpshead @tiran Modules/getpath* @FFY00
**/sha* @gpshead @tiran
Modules/md5* @gpshead @tiran
**/*blake* @gpshead @tiran
Modules/_blake2/** @gpshead @tiran
Modules/_hacl/** @gpshead
# logging # Hashing / ``hash()`` and related
**/*logging* @vsajip Include/cpython/pyhash.h @gpshead @picnixz
Include/internal/pycore_pyhash.h @gpshead @picnixz
Include/pyhash.h @gpshead @picnixz
Python/pyhash.c @gpshead @picnixz
# venv # The import system (including importlib)
**/*venv* @vsajip
# Launcher
/PC/launcher.c @vsajip
# HTML
/Lib/html/ @ezio-melotti
/Lib/_markupbase.py @ezio-melotti
/Lib/test/test_html*.py @ezio-melotti
/Tools/build/parse_html5_entities.py @ezio-melotti
# Import (including importlib).
**/*import* @brettcannon @ericsnowcurrently @ncoghlan @warsaw **/*import* @brettcannon @ericsnowcurrently @ncoghlan @warsaw
/Python/import.c @kumaraditya303 Python/import.c @brettcannon @ericsnowcurrently @ncoghlan @warsaw @kumaraditya303
Python/dynload_*.c @ericsnowcurrently
**/*freeze* @ericsnowcurrently **/*freeze* @ericsnowcurrently
**/*frozen* @ericsnowcurrently **/*frozen* @ericsnowcurrently
**/*modsupport* @ericsnowcurrently **/*modsupport* @ericsnowcurrently
@ -116,19 +263,171 @@ Python/dynload_*.c @ericsnowcurrently
**/*pythonrun* @ericsnowcurrently **/*pythonrun* @ericsnowcurrently
**/*runpy* @ericsnowcurrently **/*runpy* @ericsnowcurrently
**/*singlephase* @ericsnowcurrently **/*singlephase* @ericsnowcurrently
Lib/test/test_module/ @ericsnowcurrently
Doc/c-api/module.rst @ericsnowcurrently Doc/c-api/module.rst @ericsnowcurrently
**/*importlib/resources/* @jaraco @warsaw @FFY00 Lib/test/test_module/ @ericsnowcurrently
**/*importlib/metadata/* @jaraco @warsaw Python/dynload_*.c @ericsnowcurrently
# Initialisation
**/*initconfig* @ericsnowcurrently
**/*pathconfig* @ericsnowcurrently
**/*preconfig* @ericsnowcurrently
Doc/library/sys_path_init.rst @FFY00
Doc/c-api/init_config.rst @FFY00
# Interpreter main program
Modules/main.c @ericsnowcurrently
Programs/_bootstrap_python.c @ericsnowcurrently
Programs/python.c @ericsnowcurrently
# JIT
Include/internal/pycore_jit.h @brandtbucher @savannahostrowski @diegorusso
Python/jit.c @brandtbucher @savannahostrowski @diegorusso
Tools/jit/ @brandtbucher @savannahostrowski @diegorusso
InternalDocs/jit.md @brandtbucher @savannahostrowski @diegorusso @AA-Turner
# Micro-op / μop / Tier 2 Optimiser
Python/optimizer.c @markshannon
Python/optimizer_analysis.c @markshannon @tomasr8 @Fidget-Spinner
Python/optimizer_bytecodes.c @markshannon @tomasr8 @Fidget-Spinner
Python/optimizer_symbols.c @markshannon @tomasr8
# Parser, Lexer, and Grammar
Grammar/python.gram @pablogsal @lysnikolaou
Lib/test/test_peg_generator/ @pablogsal @lysnikolaou
Lib/test/test_tokenize.py @pablogsal @lysnikolaou
Lib/tokenize.py @pablogsal @lysnikolaou
Parser/ @pablogsal @lysnikolaou
Tools/peg_generator/ @pablogsal @lysnikolaou
# Runtime state/lifecycle
**/*gil* @ericsnowcurrently
**/*pylifecycle* @ericsnowcurrently @ZeroIntensity
**/*pystate* @ericsnowcurrently @ZeroIntensity
Include/internal/pycore_*_init.h @ericsnowcurrently
Include/internal/pycore_*_state.h @ericsnowcurrently
Include/internal/pycore_atexit.h @ericsnowcurrently
Include/internal/pycore_freelist.h @ericsnowcurrently
Include/internal/pycore_global_objects.h @ericsnowcurrently
Include/internal/pycore_interp.h @ericsnowcurrently
Include/internal/pycore_obmalloc.h @ericsnowcurrently
Include/internal/pycore_pymem.h @ericsnowcurrently
Include/internal/pycore_runtime.h @ericsnowcurrently
Include/internal/pycore_stackref.h @Fidget-Spinner
Include/internal/pycore_tstate.h @ericsnowcurrently
Tools/build/generate_global_objects.py @ericsnowcurrently
# Remote Debugging
Python/remote_debug.h @pablogsal
Python/remote_debugging.c @pablogsal
Modules/_remote_debugging_module.c @pablogsal @ambv @1st1
# Sub-Interpreters
**/*crossinterp* @ericsnowcurrently
**/*interpreteridobject.* @ericsnowcurrently
Doc/library/concurrent.interpreters.rst @ericsnowcurrently
Lib/concurrent/futures/interpreter.py @ericsnowcurrently
Lib/concurrent/interpreters/ @ericsnowcurrently
Lib/test/support/channels.py @ericsnowcurrently
Lib/test/test__interp*.py @ericsnowcurrently
Lib/test/test_interpreters/ @ericsnowcurrently
Modules/_interp*module.c @ericsnowcurrently
# Template string literals (t-strings)
Lib/test/test_tstring.py @lysnikolaou
Objects/interpolationobject.c @lysnikolaou
Objects/templateobject.c @lysnikolaou
# Tests
Lib/test/test_patma.py @brandtbucher
Lib/test/test_type_*.py @JelleZijlstra
Lib/test/test_capi/test_misc.py @markshannon
# ----------------------------------------------------------------------------
# Standard Library
# ----------------------------------------------------------------------------
# Annotationlib
Doc/library/annotationlib.rst @JelleZijlstra
Lib/annotationlib.py @JelleZijlstra
Lib/test/test_annotationlib.py @JelleZijlstra
# Argparse
Doc/**/argparse*.rst @savannahostrowski
Lib/argparse.py @savannahostrowski
Lib/test/test_argparse.py @savannahostrowski
# Asyncio
Doc/library/asyncio*.rst @1st1 @asvetlov @kumaraditya303 @willingc
InternalDocs/asyncio.md @1st1 @asvetlov @kumaraditya303 @willingc @AA-Turner
Lib/asyncio/ @1st1 @asvetlov @kumaraditya303 @willingc
Lib/test/test_asyncio/ @1st1 @asvetlov @kumaraditya303 @willingc
Modules/_asynciomodule.c @1st1 @asvetlov @kumaraditya303 @willingc
# Bisect
Doc/library/bisect.rst @rhettinger
Lib/bisect.py @rhettinger
Lib/test/test_bisect.py @rhettinger
Modules/_bisectmodule.c @rhettinger
# Calendar
Lib/calendar.py @AA-Turner
Lib/test/test_calendar.py @AA-Turner
# Cryptographic Primitives and Applications
**/*hashlib* @gpshead @picnixz
**/*hashopenssl* @gpshead @picnixz
**/*hmac* @gpshead @picnixz
**/*ssl* @gpshead @picnixz
Modules/_hacl/ @gpshead @picnixz
Modules/*blake* @gpshead @picnixz
Modules/*md5* @gpshead @picnixz
Modules/*sha* @gpshead @picnixz
# Codecs
Modules/cjkcodecs/ @corona10
Tools/unicode/gencjkcodecs.py @corona10
# Collections
Doc/library/collections.abc.rst @rhettinger
Doc/library/collections.rst @rhettinger
Lib/_collections_abc.py @rhettinger
Lib/collections/ @rhettinger
Lib/test/test_collections.py @rhettinger
Modules/_collectionsmodule.c @rhettinger
# Colorize
Lib/_colorize.py @hugovk
Lib/test/test__colorize.py @hugovk
# Config Parser
Lib/configparser.py @jaraco
Lib/test/test_configparser.py @jaraco
# Dataclasses
Doc/library/dataclasses.rst @ericvsmith
Lib/dataclasses.py @ericvsmith
Lib/test/test_dataclasses/ @ericvsmith
# Dates and times # Dates and times
**/*datetime* @pganssle @abalkin Doc/**/*time.rst @pganssle @abalkin
**/*str*time* @pganssle @abalkin Doc/library/zoneinfo.rst @pganssle
Doc/library/time.rst @pganssle @abalkin Include/datetime.h @pganssle @abalkin
Lib/test/test_time.py @pganssle @abalkin Include/internal/pycore_time.h @pganssle @abalkin
Modules/timemodule.c @pganssle @abalkin Lib/test/test_zoneinfo/ @pganssle
Python/pytime.c @pganssle @abalkin Lib/zoneinfo/ @pganssle
Include/internal/pycore_time.h @pganssle @abalkin Lib/*time.py @pganssle @abalkin
Lib/test/datetimetester.py @pganssle @abalkin
Lib/test/test_*time.py @pganssle @abalkin
Modules/*zoneinfo* @pganssle
Modules/*time* @pganssle @abalkin
Python/pytime.c @pganssle @abalkin
# Dbm
Doc/library/dbm.rst @corona10 @erlend-aasland @serhiy-storchaka
Lib/dbm/ @corona10 @erlend-aasland @serhiy-storchaka
Lib/test/test_dbm*.py @corona10 @erlend-aasland @serhiy-storchaka
Modules/*dbm* @corona10 @erlend-aasland @serhiy-storchaka
# Email and related # Email and related
**/*mail* @python/email-team **/*mail* @python/email-team
@ -137,120 +436,197 @@ Include/internal/pycore_time.h @pganssle @abalkin
**/*imap* @python/email-team **/*imap* @python/email-team
**/*poplib* @python/email-team **/*poplib* @python/email-team
# Ensurepip
Doc/library/ensurepip.rst @pfmoore @pradyunsg
Lib/ensurepip/ @pfmoore @pradyunsg
Lib/test/test_ensurepip.py @pfmoore @pradyunsg
# Enum
Doc/howto/enum.rst @ethanfurman
Doc/library/enum.rst @ethanfurman
Lib/enum.py @ethanfurman
Lib/test/test_enum.py @ethanfurman
Lib/test/test_json/test_enum.py @ethanfurman
# FTP
Doc/library/ftplib.rst @giampaolo
Lib/ftplib.py @giampaolo
Lib/test/test_ftplib.py @giampaolo
# Functools
Doc/library/functools.rst @rhettinger
Lib/functools.py @rhettinger
Lib/test/test_functools.py @rhettinger
Modules/_functoolsmodule.c @rhettinger
# Garbage collector # Garbage collector
/Modules/gcmodule.c @pablogsal Modules/gcmodule.c @pablogsal
/Doc/library/gc.rst @pablogsal Doc/library/gc.rst @pablogsal
# Parser # Gettext
/Parser/ @pablogsal @lysnikolaou Doc/library/gettext.rst @tomasr8
/Tools/peg_generator/ @pablogsal @lysnikolaou Lib/gettext.py @tomasr8
/Lib/test/test_peg_generator/ @pablogsal @lysnikolaou Lib/test/test_gettext.py @tomasr8
/Grammar/python.gram @pablogsal @lysnikolaou Tools/i18n/pygettext.py @tomasr8
/Lib/tokenize.py @pablogsal @lysnikolaou
/Lib/test/test_tokenize.py @pablogsal @lysnikolaou
# Code generator # Heapq
/Tools/cases_generator/ @gvanrossum Doc/library/heapq* @rhettinger
Lib/heapq.py @rhettinger
Lib/test/test_heapq.py @rhettinger
Modules/_heapqmodule.c @rhettinger
# AST # HTML
Python/ast.c @isidentical Doc/library/html* @ezio-melotti
Parser/asdl.py @isidentical Lib/html/ @ezio-melotti
Parser/asdl_c.py @isidentical Lib/_markupbase.py @ezio-melotti
Lib/ast.py @isidentical Lib/test/test_html*.py @ezio-melotti
Tools/build/parse_html5_entities.py @ezio-melotti
# Mock # IDLE
/Lib/unittest/mock.py @cjw296 Doc/library/idle.rst @terryjreedy
/Lib/test/test_unittest/testmock/* @cjw296 Lib/idlelib/ @terryjreedy
Lib/turtledemo/ @terryjreedy
# multiprocessing # importlib.metadata
**/*multiprocessing* @gpshead Doc/library/importlib.metadata.rst @jaraco @warsaw
Lib/importlib/metadata/ @jaraco @warsaw
Lib/test/test_importlib/metadata/ @jaraco @warsaw
# importlib.resources
Doc/library/importlib.resources.abc.rst @jaraco @warsaw
Doc/library/importlib.resources.rst @jaraco @warsaw
Lib/importlib/resources/ @jaraco @warsaw @FFY00
Lib/test/test_importlib/resources/ @jaraco @warsaw @FFY00
# Itertools
Doc/library/itertools.rst @rhettinger
Lib/test/test_itertools.py @rhettinger
Modules/itertoolsmodule.c @rhettinger
# Logging
Doc/**/logging* @vsajip
Lib/logging/ @vsajip
Lib/test/test_logging.py @vsajip
# Multiprocessing
Doc/library/multiprocessing*.rst @gpshead
Lib/multiprocessing/ @gpshead
Lib/test/*multiprocessing.py @gpshead
Lib/test/test_multiprocessing*/ @gpshead
Modules/_multiprocessing/ @gpshead
# Pathlib
Doc/library/pathlib.rst @barneygale
Lib/pathlib/ @barneygale
Lib/test/test_pathlib/ @barneygale
# Pdb & Bdb
Doc/library/bdb.rst @gaogaotiantian
Doc/library/pdb.rst @gaogaotiantian
Lib/bdb.py @gaogaotiantian
Lib/pdb.py @gaogaotiantian
Lib/test/test_bdb.py @gaogaotiantian
Lib/test/test_pdb.py @gaogaotiantian
Lib/test/test_remote_pdb.py @gaogaotiantian
# Pydoc
Lib/pydoc.py @AA-Turner
Lib/pydoc_data/ @AA-Turner
Lib/test/test_pydoc/ @AA-Turner
# PyREPL
Lib/_pyrepl/ @pablogsal @lysnikolaou @ambv
Lib/test/test_pyrepl/ @pablogsal @lysnikolaou @ambv
# Random
Doc/library/random.rst @rhettinger
Lib/random.py @rhettinger
Lib/test/test_random.py @rhettinger
Modules/_randommodule.c @rhettinger
# Shutil
Doc/library/shutil.rst @giampaolo
Lib/shutil.py @giampaolo
Lib/test/test_shutil.py @giampaolo
# Site
Lib/site.py @FFY00
Lib/test/test_site.py @FFY00
Doc/library/site.rst @FFY00
# string.templatelib
Doc/library/string.templatelib.rst @lysnikolaou @AA-Turner
Lib/string/templatelib.py @lysnikolaou @AA-Turner
Lib/test/test_string/test_templatelib.py @lysnikolaou @AA-Turner
# Sysconfig
**/*sysconfig* @FFY00
# SQLite 3 # SQLite 3
**/*sqlite* @berkerpeksag @erlend-aasland Doc/library/sqlite3.rst @berkerpeksag @erlend-aasland
Lib/sqlite3/ @berkerpeksag @erlend-aasland
Lib/test/test_sqlite3/ @berkerpeksag @erlend-aasland
Modules/_sqlite/ @berkerpeksag @erlend-aasland
# subprocess # Subprocess
/Lib/subprocess.py @gpshead Lib/subprocess.py @gpshead
/Lib/test/test_subprocess.py @gpshead Lib/test/test_subprocess.py @gpshead
/Modules/*subprocess* @gpshead Modules/*subprocess* @gpshead
# Limited C API & stable ABI # Tarfile
Tools/build/stable_abi.py @encukou Doc/library/tarfile.rst @ethanfurman
Misc/stable_abi.toml @encukou Lib/tarfile.py @ethanfurman
Doc/data/*.abi @encukou Lib/test/test_tarfile.py @ethanfurman
Doc/c-api/stable.rst @encukou
# Windows # TOML
/PC/ @python/windows-team Doc/library/tomllib.rst @encukou @hauntsaninja
/PCbuild/ @python/windows-team Lib/test/test_tomllib/ @encukou @hauntsaninja
Lib/tomllib/ @encukou @hauntsaninja
# Typing
Doc/library/typing.rst @JelleZijlstra @AlexWaygood
Lib/test/test_typing.py @JelleZijlstra @AlexWaygood
Lib/test/typinganndata/ @JelleZijlstra @AlexWaygood
Lib/typing.py @JelleZijlstra @AlexWaygood
Modules/_typingmodule.c @JelleZijlstra @AlexWaygood
# Types
Lib/test/test_types.py @AA-Turner
Lib/types.py @AA-Turner
Modules/_typesmodule.c @AA-Turner
# Unittest
Lib/unittest/mock.py @cjw296
Lib/test/test_unittest/testmock/ @cjw296
# Urllib # Urllib
**/*robotparser* @berkerpeksag **/*robotparser* @berkerpeksag
# Windows installer packages # Venv
/Tools/msi/ @python/windows-team **/*venv* @vsajip @FFY00
/Tools/nuget/ @python/windows-team
# Misc # Weakref
**/*itertools* @rhettinger **/*weakref* @kumaraditya303
**/*collections* @rhettinger
**/*random* @rhettinger
**/*queue* @rhettinger
**/*bisect* @rhettinger
**/*heapq* @rhettinger
**/*functools* @rhettinger
**/*decimal* @rhettinger
**/*dataclasses* @ericvsmith # Zipfile.Path
Lib/test/test_zipfile/_path/ @jaraco
Lib/zipfile/_path/ @jaraco
**/*ensurepip* @pfmoore @pradyunsg # Zstandard
Lib/compression/zstd/ @AA-Turner @emmatyping
Lib/test/test_zstd.py @AA-Turner @emmatyping
Modules/_zstd/ @AA-Turner @emmatyping
**/*idlelib* @terryjreedy # ----------------------------------------------------------------------------
**/*typing* @JelleZijlstra @AlexWaygood # Exclusions from .github/CODEOWNERS should go at the very end
# because the final matching pattern will take precedence.
**/*ftplib @giampaolo # Exclude .mailmap from being owned by @python/email-team
**/*shutil @giampaolo .mailmap
**/*enum* @ethanfurman # Exclude Argument Clinic directories
**/*cgi* @ethanfurman Modules/**/clinic/
**/*tarfile* @ethanfurman Objects/**/clinic/
PC/**/clinic/
**/*tomllib* @encukou @hauntsaninja Python/**/clinic/
**/*sysconfig* @FFY00
**/*cjkcodecs* @corona10
# macOS
/Mac/ @python/macos-team
**/*osx_support* @python/macos-team
# pathlib
**/*pathlib* @barneygale
# zipfile.Path
**/*zipfile/_path/* @jaraco
# Argument Clinic
/Tools/clinic/** @erlend-aasland
/Lib/test/test_clinic.py @erlend-aasland
Doc/howto/clinic.rst @erlend-aasland
# Subinterpreters
**/*interpreteridobject.* @ericsnowcurrently
**/*crossinterp* @ericsnowcurrently
Lib/test/support/interpreters/ @ericsnowcurrently
Modules/_xx*interp*module.c @ericsnowcurrently
Lib/test/test_interpreters/ @ericsnowcurrently
# WebAssembly
/Tools/wasm/ @brettcannon
# SBOM
/Misc/externals.spdx.json @sethmlarson
/Misc/sbom.spdx.json @sethmlarson
/Tools/build/generate_sbom.py @sethmlarson
# Config Parser
Lib/configparser.py @jaraco
Lib/test/test_configparser.py @jaraco

View file

@ -4,7 +4,7 @@ Contributing to Python
Build Status Build Status
------------ ------------
- `Buildbot status overview <https://buildbot.python.org/all/#/release_status>`_ - `Buildbot status overview <https://buildbot.python.org/#/release_status>`_
- `GitHub Actions status <https://github.com/python/cpython/actions/workflows/build.yml>`_ - `GitHub Actions status <https://github.com/python/cpython/actions/workflows/build.yml>`_
@ -34,17 +34,18 @@ our workflow that are not covered by a bot or status check are:
- All discussions that are not directly related to the code in the pull request - All discussions that are not directly related to the code in the pull request
should happen on `GitHub Issues <https://github.com/python/cpython/issues>`_. should happen on `GitHub Issues <https://github.com/python/cpython/issues>`_.
- Upon your first non-trivial pull request (which includes documentation changes), - Upon your first non-trivial pull request (which includes documentation changes),
feel free to add yourself to ``Misc/ACKS`` feel free to add yourself to ``Misc/ACKS``.
Setting Expectations Setting Expectations
-------------------- --------------------
Due to the fact that this project is entirely volunteer-run (i.e. no one is paid Due to the fact that this project is run by volunteers,
to work on Python full-time), we unfortunately can make no guarantees as to if unfortunately we cannot make any guarantees as to if
or when a core developer will get around to reviewing your pull request. or when a core developer will get around to reviewing your pull request.
If no core developer has done a review or responded to changes made because of a If no core developer has done a review or responded to changes made because of a
"changes requested" review, please feel free to email python-dev to ask if "changes requested" review within a month, you can ask for someone to
someone could take a look at your pull request. review your pull request via a post in the `Core Development Discourse
category <https://discuss.python.org/c/core-dev/23>`__.
Code of Conduct Code of Conduct

View file

@ -34,12 +34,13 @@ body:
label: "CPython versions tested on:" label: "CPython versions tested on:"
multiple: true multiple: true
options: options:
- "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13" - "3.13"
- "3.14"
- "3.15"
- "CPython main branch" - "CPython main branch"
validations: validations:
required: true required: true

View file

@ -27,11 +27,13 @@ body:
label: "CPython versions tested on:" label: "CPython versions tested on:"
multiple: true multiple: true
options: options:
- "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13"
- "3.14"
- "3.15"
- "CPython main branch" - "CPython main branch"
validations: validations:
required: true required: true

View file

@ -7,10 +7,10 @@ # Pull Request title
It should be in the following format: It should be in the following format:
``` ```
gh-NNNNN: Summary of the changes made gh-NNNNNN: Summary of the changes made
``` ```
Where: gh-NNNNN refers to the GitHub issue number. Where: gh-NNNNNN refers to the GitHub issue number.
Most PRs will require an issue number. Trivial changes, like fixing a typo, do not need an issue. Most PRs will require an issue number. Trivial changes, like fixing a typo, do not need an issue.
@ -20,11 +20,11 @@ # Backport Pull Request title
please ensure that the PR title is in the following format: please ensure that the PR title is in the following format:
``` ```
[X.Y] <title from the original PR> (GH-NNNN) [X.Y] <title from the original PR> (GH-NNNNNN)
``` ```
Where: [X.Y] is the branch name, e.g. [3.6]. Where: [X.Y] is the branch name, for example: [3.13].
GH-NNNN refers to the PR number from `main`. GH-NNNNNN refers to the PR number from `main`.
--> -->

12
.github/actionlint.yaml vendored Normal file
View file

@ -0,0 +1,12 @@
self-hosted-runner:
# Pending https://github.com/rhysd/actionlint/issues/533
# and https://github.com/rhysd/actionlint/issues/571
labels: ["windows-11-arm", "macos-15-intel"]
config-variables: null
paths:
.github/workflows/**/*.yml:
ignore:
- 1st argument of function call is not assignable
- SC2(015|038|086|091|097|098|129|155)

View file

@ -18,6 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
issues: write issues: write
timeout-minutes: 5
steps: steps:
- uses: actions/github-script@v7 - uses: actions/github-script@v7
with: with:

File diff suppressed because it is too large Load diff

View file

@ -1,40 +0,0 @@
name: TestsMSI
on:
workflow_dispatch:
push:
branches:
- 'main'
- '3.*'
paths:
- 'Tools/msi/**'
- '.github/workflows/build_msi.yml'
pull_request:
branches:
- 'main'
- '3.*'
paths:
- 'Tools/msi/**'
- '.github/workflows/build_msi.yml'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
name: Windows Installer
runs-on: windows-latest
timeout-minutes: 60
strategy:
matrix:
type: [x86, x64, arm64]
env:
IncludeFreethreaded: true
steps:
- uses: actions/checkout@v4
- name: Build CPython installer
run: .\Tools\msi\build.bat --doc -${{ matrix.type }}

View file

@ -10,9 +10,6 @@ on:
- 'Doc/**' - 'Doc/**'
- '.github/workflows/doc.yml' - '.github/workflows/doc.yml'
permissions:
pull-requests: write
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
@ -20,6 +17,10 @@ concurrency:
jobs: jobs:
documentation-links: documentation-links:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
timeout-minutes: 5
steps: steps:
- uses: readthedocs/actions/preview@v1 - uses: readthedocs/actions/preview@v1
with: with:

View file

@ -5,11 +5,21 @@ on:
- '**jit**' - '**jit**'
- 'Python/bytecodes.c' - 'Python/bytecodes.c'
- 'Python/optimizer*.c' - 'Python/optimizer*.c'
- 'Python/executor_cases.c.h'
- 'Python/optimizer_cases.c.h'
- '!Python/perf_jit_trampoline.c'
- '!**/*.md'
- '!**/*.ini'
push: push:
paths: paths:
- '**jit**' - '**jit**'
- 'Python/bytecodes.c' - 'Python/bytecodes.c'
- 'Python/optimizer*.c' - 'Python/optimizer*.c'
- 'Python/executor_cases.c.h'
- 'Python/optimizer_cases.c.h'
- '!Python/perf_jit_trampoline.c'
- '!**/*.md'
- '!**/*.ini'
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -19,11 +29,30 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
interpreter:
name: Interpreter (Debug)
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build tier two interpreter
run: |
./configure --enable-experimental-jit=interpreter --with-pydebug
make all --jobs 4
- name: Test tier two interpreter
run: |
./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
jit: jit:
name: ${{ matrix.target }} (${{ matrix.debug && 'Debug' || 'Release' }}) name: ${{ matrix.target }} (${{ matrix.debug && 'Debug' || 'Release' }})
needs: interpreter
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
timeout-minutes: 75 timeout-minutes: 90
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -34,111 +63,123 @@ jobs:
- x86_64-apple-darwin/clang - x86_64-apple-darwin/clang
- aarch64-apple-darwin/clang - aarch64-apple-darwin/clang
- x86_64-unknown-linux-gnu/gcc - x86_64-unknown-linux-gnu/gcc
- x86_64-unknown-linux-gnu/clang
- aarch64-unknown-linux-gnu/gcc - aarch64-unknown-linux-gnu/gcc
- aarch64-unknown-linux-gnu/clang
debug: debug:
- true - true
- false - false
llvm: llvm:
- 16 - 19
include: include:
- target: i686-pc-windows-msvc/msvc - target: i686-pc-windows-msvc/msvc
architecture: Win32 architecture: Win32
runner: windows-latest runner: windows-2022
compiler: msvc
- target: x86_64-pc-windows-msvc/msvc - target: x86_64-pc-windows-msvc/msvc
architecture: x64 architecture: x64
runner: windows-latest runner: windows-2022
compiler: msvc
- target: aarch64-pc-windows-msvc/msvc - target: aarch64-pc-windows-msvc/msvc
architecture: ARM64 architecture: ARM64
runner: windows-latest runner: windows-11-arm
compiler: msvc
- target: x86_64-apple-darwin/clang - target: x86_64-apple-darwin/clang
architecture: x86_64 architecture: x86_64
runner: macos-13 runner: macos-15-intel
compiler: clang
- target: aarch64-apple-darwin/clang - target: aarch64-apple-darwin/clang
architecture: aarch64 architecture: aarch64
runner: macos-14 runner: macos-14
compiler: clang
- target: x86_64-unknown-linux-gnu/gcc - target: x86_64-unknown-linux-gnu/gcc
architecture: x86_64 architecture: x86_64
runner: ubuntu-latest runner: ubuntu-24.04
compiler: gcc
- target: x86_64-unknown-linux-gnu/clang
architecture: x86_64
runner: ubuntu-latest
compiler: clang
- target: aarch64-unknown-linux-gnu/gcc - target: aarch64-unknown-linux-gnu/gcc
architecture: aarch64 architecture: aarch64
runner: ubuntu-latest runner: ubuntu-24.04-arm
compiler: gcc
# These fail because of emulation, not because of the JIT:
exclude: test_unix_events test_init test_process_pool test_shutdown test_multiprocessing_fork test_cmd_line test_faulthandler test_os test_perf_profiler test_posix test_signal test_socket test_subprocess test_threading test_venv test_external_inspection
- target: aarch64-unknown-linux-gnu/clang
architecture: aarch64
runner: ubuntu-latest
compiler: clang
# These fail because of emulation, not because of the JIT:
exclude: test_unix_events test_init test_process_pool test_shutdown test_multiprocessing_fork test_cmd_line test_faulthandler test_os test_perf_profiler test_posix test_signal test_socket test_subprocess test_threading test_venv test_external_inspection
env:
CC: ${{ matrix.compiler }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: '3.11'
- name: Native Windows # PCbuild downloads LLVM automatically:
if: runner.os == 'Windows' && matrix.architecture != 'ARM64' - name: Windows
if: runner.os == 'Windows'
run: | run: |
choco install llvm --allow-downgrade --no-progress --version ${{ matrix.llvm }}
./PCbuild/build.bat --experimental-jit ${{ matrix.debug && '-d' || '--pgo' }} -p ${{ matrix.architecture }}
./PCbuild/rt.bat ${{ matrix.debug && '-d' }} -p ${{ matrix.architecture }} -q --exclude ${{ matrix.exclude }} --multiprocess 0 --timeout 4500 --verbose2 --verbose3
# No PGO or tests (yet):
- name: Emulated Windows
if: runner.os == 'Windows' && matrix.architecture == 'ARM64'
run: |
choco install llvm --allow-downgrade --no-progress --version ${{ matrix.llvm }}
./PCbuild/build.bat --experimental-jit ${{ matrix.debug && '-d' || '' }} -p ${{ matrix.architecture }} ./PCbuild/build.bat --experimental-jit ${{ matrix.debug && '-d' || '' }} -p ${{ matrix.architecture }}
./PCbuild/rt.bat ${{ matrix.debug && '-d' || '' }} -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3
- name: Native macOS - name: macOS
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: | run: |
brew update
brew install llvm@${{ matrix.llvm }} brew install llvm@${{ matrix.llvm }}
SDKROOT="$(xcrun --show-sdk-path)" \ export SDKROOT="$(xcrun --show-sdk-path)"
./configure --enable-experimental-jit ${{ matrix.debug && '--with-pydebug' || '--enable-optimizations --with-lto' }} # Set MACOSX_DEPLOYMENT_TARGET and -Werror=unguarded-availability to
# make sure we don't break downstream distributors (like uv):
export CFLAGS_JIT='-Werror=unguarded-availability'
export MACOSX_DEPLOYMENT_TARGET=10.15
./configure --enable-experimental-jit --enable-universalsdk --with-universal-archs=universal2 ${{ matrix.debug && '--with-pydebug' || '' }}
make all --jobs 4 make all --jobs 4
./python.exe -m test --exclude ${{ matrix.exclude }} --multiprocess 0 --timeout 4500 --verbose2 --verbose3 ./python.exe -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
- name: Native Linux - name: Linux
if: runner.os == 'Linux' && matrix.architecture == 'x86_64' if: runner.os == 'Linux'
run: | run: |
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }} sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH" export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
./configure --enable-experimental-jit ${{ matrix.debug && '--with-pydebug' || '--enable-optimizations --with-lto' }} ./configure --enable-experimental-jit ${{ matrix.debug && '--with-pydebug' || '' }}
make all --jobs 4 make all --jobs 4
./python -m test --exclude ${{ matrix.exclude }} --multiprocess 0 --timeout 4500 --verbose2 --verbose3 ./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
- name: Emulated Linux jit-with-disabled-gil:
if: runner.os == 'Linux' && matrix.architecture != 'x86_64' name: Free-Threaded (Debug)
needs: interpreter
runs-on: ubuntu-24.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
llvm:
- 19
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build with JIT enabled and GIL disabled
run: | run: |
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }} sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH" export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
./configure --prefix="$(pwd)/../build" ./configure --enable-experimental-jit --with-pydebug --disable-gil
make install --jobs 4
make clean --jobs 4
export HOST=${{ matrix.architecture }}-linux-gnu
sudo apt install --yes "gcc-$HOST" qemu-user
${{ !matrix.debug && matrix.compiler == 'clang' && './configure --enable-optimizations' || '' }}
${{ !matrix.debug && matrix.compiler == 'clang' && 'make profile-run-stamp --jobs 4' || '' }}
export QEMU_LD_PREFIX="/usr/$HOST"
CC="${{ matrix.compiler == 'clang' && 'clang --target=$HOST' || '$HOST-gcc' }}" \
CPP="$CC --preprocess" \
HOSTRUNNER=qemu-${{ matrix.architecture }} \
./configure --enable-experimental-jit ${{ matrix.debug && '--with-pydebug' || '--enable-optimizations --with-lto' }} --build=x86_64-linux-gnu --host="$HOST" --with-build-python=../build/bin/python3 --with-pkg-config=no ac_cv_buggy_getaddrinfo=no ac_cv_file__dev_ptc=no ac_cv_file__dev_ptmx=yes
make all --jobs 4 make all --jobs 4
./python -m test --exclude ${{ matrix.exclude }} --multiprocess 0 --timeout 4500 --verbose2 --verbose3 - name: Run tests
run: |
./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
continue-on-error: true
no-opt-jit:
name: JIT without optimizations (Debug)
needs: interpreter
runs-on: ubuntu-24.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
llvm:
- 19
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build with JIT
run: |
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
./configure --enable-experimental-jit --with-pydebug
make all --jobs 4
- name: Run tests without optimizations
run: |
PYTHON_UOPS_OPTIMIZE=0 ./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3

View file

@ -20,6 +20,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"

View file

@ -8,14 +8,28 @@ on:
pull_request: pull_request:
paths: paths:
- ".github/workflows/mypy.yml" - ".github/workflows/mypy.yml"
- "Lib/_colorize.py"
- "Lib/_pyrepl/**"
- "Lib/test/libregrtest/**" - "Lib/test/libregrtest/**"
- "Lib/tomllib/**"
- "Misc/mypy/**"
- "Tools/build/check_extension_modules.py"
- "Tools/build/check_warnings.py"
- "Tools/build/compute-changes.py"
- "Tools/build/consts_getter.py"
- "Tools/build/deepfreeze.py"
- "Tools/build/generate-build-details.py"
- "Tools/build/generate_sbom.py" - "Tools/build/generate_sbom.py"
- "Tools/build/generate_stdlib_module_names.py"
- "Tools/build/mypy.ini"
- "Tools/build/umarshal.py"
- "Tools/build/update_file.py"
- "Tools/build/verify_ensurepip_wheels.py"
- "Tools/cases_generator/**" - "Tools/cases_generator/**"
- "Tools/clinic/**" - "Tools/clinic/**"
- "Tools/jit/**" - "Tools/jit/**"
- "Tools/peg_generator/**" - "Tools/peg_generator/**"
- "Tools/requirements-dev.txt" - "Tools/requirements-dev.txt"
- "Tools/wasm/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -32,26 +46,31 @@ concurrency:
jobs: jobs:
mypy: mypy:
name: Run mypy on ${{ matrix.target }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy: strategy:
fail-fast: false
matrix: matrix:
target: [ target: [
"Lib/_pyrepl",
"Lib/test/libregrtest", "Lib/test/libregrtest",
"Tools/build/", "Lib/tomllib",
"Tools/build",
"Tools/cases_generator", "Tools/cases_generator",
"Tools/clinic", "Tools/clinic",
"Tools/jit", "Tools/jit",
"Tools/peg_generator", "Tools/peg_generator",
"Tools/wasm",
] ]
name: Run mypy on ${{ matrix.target }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.13"
cache: pip cache: pip
cache-dependency-path: Tools/requirements-dev.txt cache-dependency-path: Tools/requirements-dev.txt
- run: pip install -r Tools/requirements-dev.txt - run: pip install -r Tools/requirements-dev.txt
- run: python3 Misc/mypy/make_symlinks.py --symlink
- run: mypy --config-file ${{ matrix.target }}/mypy.ini - run: mypy --config-file ${{ matrix.target }}/mypy.ini

View file

@ -1,12 +1,11 @@
#!/bin/sh #!/bin/sh
apt-get update apt-get update
# autoconf-archive is needed by autoreconf (check_generated_files job)
apt-get -yq install \ apt-get -yq install \
build-essential \ build-essential \
pkg-config \ pkg-config \
autoconf-archive \
ccache \ ccache \
cmake \
gdb \ gdb \
lcov \ lcov \
libb2-dev \ libb2-dev \
@ -19,6 +18,7 @@ apt-get -yq install \
libreadline6-dev \ libreadline6-dev \
libsqlite3-dev \ libsqlite3-dev \
libssl-dev \ libssl-dev \
libzstd-dev \
lzma \ lzma \
lzma-dev \ lzma-dev \
strace \ strace \
@ -26,3 +26,10 @@ apt-get -yq install \
uuid-dev \ uuid-dev \
xvfb \ xvfb \
zlib1g-dev zlib1g-dev
# Workaround missing libmpdec-dev on ubuntu 24.04:
# https://launchpad.net/~ondrej/+archive/ubuntu/php
# https://deb.sury.org/
sudo add-apt-repository ppa:ondrej/php
apt-get update
apt-get -yq install libmpdec-dev

View file

@ -1,30 +0,0 @@
name: Update GH projects
on:
issues:
types:
- opened
- labeled
permissions:
contents: read
jobs:
add-to-project:
name: Add issues to projects
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
include:
# if an issue has any of these labels, it will be added
# to the corresponding project
- { project: 2, label: "release-blocker, deferred-blocker" }
- { project: 32, label: sprint }
steps:
- uses: actions/add-to-project@v1.0.0
with:
project-url: https://github.com/orgs/python/projects/${{ matrix.project }}
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
labeled: ${{ matrix.label }}

View file

@ -4,20 +4,58 @@ on:
pull_request: pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize] types: [opened, reopened, labeled, unlabeled, synchronize]
permissions:
issues: write
pull-requests: write
jobs: jobs:
label: label-dnm:
name: DO-NOT-MERGE / unresolved review name: DO-NOT-MERGE
if: github.repository_owner == 'python' if: github.repository_owner == 'python'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: read
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: mheap/github-action-required-labels@v5 - name: Check there's no DO-NOT-MERGE
uses: mheap/github-action-required-labels@v5
with: with:
mode: exactly mode: exactly
count: 0 count: 0
labels: "DO-NOT-MERGE, awaiting changes, awaiting change review" labels: |
DO-NOT-MERGE
label-reviews:
name: Unresolved review
if: github.repository_owner == 'python'
runs-on: ubuntu-latest
permissions:
pull-requests: read
timeout-minutes: 10
steps:
# Check that the PR is not awaiting changes from the author due to previous review.
- name: Check there's no required changes
uses: mheap/github-action-required-labels@v5
with:
mode: exactly
count: 0
labels: |
awaiting changes
awaiting change review
- id: is-feature
name: Check whether this PR is a feature (contains a "type-feature" label)
uses: mheap/github-action-required-labels@v5
with:
mode: exactly
count: 1
labels: |
type-feature
exit_type: success # don't fail the check if the PR is not a feature, just record the result
# In case of a feature PR, check for a complete review (contains an "awaiting merge" label).
- id: awaiting-merge
if: steps.is-feature.outputs.status == 'success'
name: Check for complete review
uses: mheap/github-action-required-labels@v5
with:
mode: exactly
count: 1
labels: |
awaiting merge

107
.github/workflows/reusable-context.yml vendored Normal file
View file

@ -0,0 +1,107 @@
name: Reusable build context
on: # yamllint disable-line rule:truthy
workflow_call:
outputs:
# Every referenced step MUST always set its output variable,
# either via ``Tools/build/compute-changes.py`` or in this workflow file.
# Boolean outputs (generally prefixed ``run-``) can then later be used
# safely through the following idiom in job conditionals and other
# expressions. Here's some examples:
#
# if: fromJSON(needs.build-context.outputs.run-tests)
#
# ${{
# fromJSON(needs.build-context.outputs.run-tests)
# && 'truthy-branch'
# || 'falsy-branch'
# }}
#
config-hash:
description: Config hash value for use in cache keys
value: ${{ jobs.compute-changes.outputs.config-hash }} # str
run-docs:
description: Whether to build the docs
value: ${{ jobs.compute-changes.outputs.run-docs }} # bool
run-tests:
description: Whether to run the regular tests
value: ${{ jobs.compute-changes.outputs.run-tests }} # bool
run-windows-tests:
description: Whether to run the Windows tests
value: ${{ jobs.compute-changes.outputs.run-windows-tests }} # bool
run-windows-msi:
description: Whether to run the MSI installer smoke tests
value: ${{ jobs.compute-changes.outputs.run-windows-msi }} # bool
run-ci-fuzz:
description: Whether to run the CIFuzz job
value: ${{ jobs.compute-changes.outputs.run-ci-fuzz }} # bool
jobs:
compute-changes:
name: Create context from changed files
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
config-hash: ${{ steps.config-hash.outputs.hash }}
run-ci-fuzz: ${{ steps.changes.outputs.run-ci-fuzz }}
run-docs: ${{ steps.changes.outputs.run-docs }}
run-tests: ${{ steps.changes.outputs.run-tests }}
run-windows-msi: ${{ steps.changes.outputs.run-windows-msi }}
run-windows-tests: ${{ steps.changes.outputs.run-windows-tests }}
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"
- run: >-
echo '${{ github.event_name }}'
- uses: actions/checkout@v4
with:
persist-credentials: false
ref: >-
${{
github.event_name == 'pull_request'
&& github.event.pull_request.head.sha
|| ''
}}
# Adapted from https://github.com/actions/checkout/issues/520#issuecomment-1167205721
- name: Fetch commits to get branch diff
if: github.event_name == 'pull_request'
run: |
set -eux
# Fetch enough history to find a common ancestor commit (aka merge-base):
git fetch origin "${refspec_pr}" --depth=$(( commits + 1 )) \
--no-tags --prune --no-recurse-submodules
# This should get the oldest commit in the local fetched history (which may not be the commit the PR branched from):
COMMON_ANCESTOR=$( git rev-list --first-parent --max-parents=0 --max-count=1 "${branch_pr}" )
DATE=$( git log --date=iso8601 --format=%cd "${COMMON_ANCESTOR}" )
# Get all commits since that commit date from the base branch (eg: main):
git fetch origin "${refspec_base}" --shallow-since="${DATE}" \
--no-tags --prune --no-recurse-submodules
env:
branch_pr: 'origin/${{ github.event.pull_request.head.ref }}'
commits: ${{ github.event.pull_request.commits }}
refspec_base: '+${{ github.event.pull_request.base.sha }}:remotes/origin/${{ github.event.pull_request.base.ref }}'
refspec_pr: '+${{ github.event.pull_request.head.sha }}:remotes/origin/${{ github.event.pull_request.head.ref }}'
# We only want to run tests on PRs when related files are changed,
# or when someone triggers a manual workflow run.
- name: Compute changed files
id: changes
run: python Tools/build/compute-changes.py
env:
GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
CCF_TARGET_REF: ${{ github.base_ref || github.event.repository.default_branch }}
CCF_HEAD_REF: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Compute hash for config cache key
id: config-hash
run: |
echo "hash=${{ hashFiles('configure', 'configure.ac', '.github/workflows/build.yml') }}" >> "$GITHUB_OUTPUT"

View file

@ -1,4 +1,4 @@
name: Docs name: Reusable Docs
on: on:
workflow_call: workflow_call:
@ -11,34 +11,45 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
build_doc: build-doc:
name: 'Docs' name: 'Docs'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
env: env:
branch_base: 'origin/${{ github.event.pull_request.base.ref }}' branch_base: 'origin/${{ github.event.pull_request.base.ref }}'
branch_pr: 'origin/${{ github.event.pull_request.head.ref }}' branch_pr: 'origin/${{ github.event.pull_request.head.ref }}'
commits: ${{ github.event.pull_request.commits }}
refspec_base: '+${{ github.event.pull_request.base.sha }}:remotes/origin/${{ github.event.pull_request.base.ref }}' refspec_base: '+${{ github.event.pull_request.base.sha }}:remotes/origin/${{ github.event.pull_request.base.ref }}'
refspec_pr: '+${{ github.event.pull_request.head.sha }}:remotes/origin/${{ github.event.pull_request.head.ref }}' refspec_pr: '+${{ github.event.pull_request.head.sha }}:remotes/origin/${{ github.event.pull_request.head.ref }}'
steps: steps:
- name: 'Check out latest PR branch commit' - name: 'Check out latest PR branch commit'
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false
ref: >-
${{
github.event_name == 'pull_request'
&& github.event.pull_request.head.sha
|| ''
}}
# Adapted from https://github.com/actions/checkout/issues/520#issuecomment-1167205721 # Adapted from https://github.com/actions/checkout/issues/520#issuecomment-1167205721
- name: 'Fetch commits to get branch diff' - name: 'Fetch commits to get branch diff'
if: github.event_name == 'pull_request'
run: | run: |
# Fetch enough history to find a common ancestor commit (aka merge-base): # Fetch enough history to find a common ancestor commit (aka merge-base):
git fetch origin ${{ env.refspec_pr }} --depth=$(( ${{ github.event.pull_request.commits }} + 1 )) \ git fetch origin "${refspec_pr}" --depth=$(( commits + 1 )) \
--no-tags --prune --no-recurse-submodules --no-tags --prune --no-recurse-submodules
# This should get the oldest commit in the local fetched history (which may not be the commit the PR branched from): # This should get the oldest commit in the local fetched history (which may not be the commit the PR branched from):
COMMON_ANCESTOR=$( git rev-list --first-parent --max-parents=0 --max-count=1 ${{ env.branch_pr }} ) COMMON_ANCESTOR=$( git rev-list --first-parent --max-parents=0 --max-count=1 "${branch_pr}" )
DATE=$( git log --date=iso8601 --format=%cd "${COMMON_ANCESTOR}" ) DATE=$( git log --date=iso8601 --format=%cd "${COMMON_ANCESTOR}" )
# Get all commits since that commit date from the base branch (eg: master or main): # Get all commits since that commit date from the base branch (eg: master or main):
git fetch origin ${{ env.refspec_base }} --shallow-since="${DATE}" \ git fetch origin "${refspec_base}" --shallow-since="${DATE}" \
--no-tags --prune --no-recurse-submodules --no-tags --prune --no-recurse-submodules
- name: 'Set up Python' - name: 'Set up Python'
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@ -54,41 +65,26 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
set -Eeuo pipefail set -Eeuo pipefail
# Build docs with the '-n' (nit-picky) option; write warnings to file # Build docs with the nit-picky option; write warnings to file
make -C Doc/ PYTHON=../python SPHINXOPTS="-q -n -W --keep-going -w sphinx-warnings.txt" html make -C Doc/ PYTHON=../python SPHINXOPTS="--quiet --nitpicky --warning-file sphinx-warnings.txt" html
- name: 'Check warnings' - name: 'Check warnings'
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
run: | run: |
python Doc/tools/check-warnings.py \ python Doc/tools/check-warnings.py \
--annotate-diff '${{ env.branch_base }}' '${{ env.branch_pr }}' \ --annotate-diff "${branch_base}" "${branch_pr}" \
--fail-if-regression \ --fail-if-regression \
--fail-if-improved --fail-if-improved \
--fail-if-new-news-nit
# This build doesn't use problem matchers or check annotations
build_doc_oldest_supported_sphinx:
name: 'Docs (Oldest Sphinx)'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: 'Set up Python'
uses: actions/setup-python@v5
with:
python-version: '3.11' # known to work with Sphinx 4.2
cache: 'pip'
cache-dependency-path: 'Doc/requirements-oldest-sphinx.txt'
- name: 'Install build dependencies'
run: make -C Doc/ venv REQUIREMENTS="requirements-oldest-sphinx.txt"
- name: 'Build HTML documentation'
run: make -C Doc/ SPHINXOPTS="-q" SPHINXERRORHANDLING="-W --keep-going" html
# Run "doctest" on HEAD as new syntax doesn't exist in the latest stable release # Run "doctest" on HEAD as new syntax doesn't exist in the latest stable release
doctest: doctest:
name: 'Doctest' name: 'Doctest'
runs-on: ubuntu-latest runs-on: ubuntu-24.04
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
@ -105,4 +101,31 @@ jobs:
run: make -C Doc/ PYTHON=../python venv run: make -C Doc/ PYTHON=../python venv
# Use "xvfb-run" since some doctest tests open GUI windows # Use "xvfb-run" since some doctest tests open GUI windows
- name: 'Run documentation doctest' - name: 'Run documentation doctest'
run: xvfb-run make -C Doc/ PYTHON=../python SPHINXERRORHANDLING="-W --keep-going" doctest run: xvfb-run make -C Doc/ PYTHON=../python SPHINXERRORHANDLING="--fail-on-warning" doctest
check-epub:
name: 'Check EPUB'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: 'Set up Python'
uses: actions/setup-python@v5
with:
python-version: '3'
cache: 'pip'
cache-dependency-path: 'Doc/requirements.txt'
- name: 'Install build dependencies'
run: |
make -C Doc/ venv
python -m pip install epubcheck
- name: 'Build EPUB documentation'
run: make -C Doc/ PYTHON=../python epub
- name: 'Run epubcheck'
continue-on-error: true
run: epubcheck Doc/build/epub/Python.epub &> Doc/epubcheck.txt
- run: cat Doc/epubcheck.txt
- name: 'Check for fatal errors in EPUB'
run: python Doc/tools/check-epub.py

View file

@ -1,3 +1,5 @@
name: Reusable macOS
on: on:
workflow_call: workflow_call:
inputs: inputs:
@ -8,13 +10,18 @@ on:
required: false required: false
type: boolean type: boolean
default: false default: false
os-matrix: os:
required: false description: OS to run the job
required: true
type: string type: string
env:
FORCE_COLOR: 1
jobs: jobs:
build_macos: build-macos:
name: 'build and test' name: build and test (${{ inputs.os }})
runs-on: ${{ inputs.os }}
timeout-minutes: 60 timeout-minutes: 60
env: env:
HOMEBREW_NO_ANALYTICS: 1 HOMEBREW_NO_ANALYTICS: 1
@ -22,35 +29,53 @@ jobs:
HOMEBREW_NO_INSTALL_CLEANUP: 1 HOMEBREW_NO_INSTALL_CLEANUP: 1
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
PYTHONSTRICTEXTENSIONBUILD: 1 PYTHONSTRICTEXTENSIONBUILD: 1
strategy: TERM: linux
fail-fast: false
matrix:
os: ${{fromJson(inputs.os-matrix)}}
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Runner image version - name: Runner image version
run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- name: Restore config.cache - name: Restore config.cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: config.cache path: config.cache
key: ${{ github.job }}-${{ matrix.os }}-${{ env.IMAGE_VERSION }}-${{ inputs.config_hash }} key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.config_hash }}
- name: Install Homebrew dependencies - name: Install Homebrew dependencies
run: brew install pkg-config openssl@3.0 xz gdbm tcl-tk run: |
brew install pkg-config openssl@3.0 xz gdbm tcl-tk@9 make
# Because alternate versions are not symlinked into place by default:
brew link --overwrite tcl-tk@9
- name: Configure CPython - name: Configure CPython
run: | run: |
MACOSX_DEPLOYMENT_TARGET=10.15 \
GDBM_CFLAGS="-I$(brew --prefix gdbm)/include" \ GDBM_CFLAGS="-I$(brew --prefix gdbm)/include" \
GDBM_LIBS="-L$(brew --prefix gdbm)/lib -lgdbm" \ GDBM_LIBS="-L$(brew --prefix gdbm)/lib -lgdbm" \
./configure \ ./configure \
--config-cache \ --config-cache \
--with-pydebug \ --with-pydebug \
--enable-slower-safety \
--enable-safety \
${{ inputs.free-threading && '--disable-gil' || '' }} \ ${{ inputs.free-threading && '--disable-gil' || '' }} \
--prefix=/opt/python-dev \ --prefix=/opt/python-dev \
--with-openssl="$(brew --prefix openssl@3.0)" --with-openssl="$(brew --prefix openssl@3.0)"
- name: Build CPython - name: Build CPython
run: make -j4 if : ${{ inputs.free-threading || inputs.os != 'macos-15-intel' }}
run: gmake -j8
- name: Build CPython for compiler warning check
if : ${{ !inputs.free-threading && inputs.os == 'macos-15-intel' }}
run: set -o pipefail; gmake -j8 --output-sync 2>&1 | tee compiler_output_macos.txt
- name: Display build info - name: Display build info
run: make pythoninfo run: make pythoninfo
- name: Check compiler warnings
if : ${{ !inputs.free-threading && inputs.os == 'macos-15-intel' }}
run: >-
python3 Tools/build/check_warnings.py
--compiler-output-file-path=compiler_output_macos.txt
--warning-ignore-file-path=Tools/build/.warningignore_macos
--compiler-output-type=clang
--fail-on-regression
--fail-on-improvement
--path-prefix="./"
- name: Tests - name: Tests
run: make test run: make ci

124
.github/workflows/reusable-san.yml vendored Normal file
View file

@ -0,0 +1,124 @@
name: Reusable Sanitizer
on:
workflow_call:
inputs:
sanitizer:
required: true
type: string
config_hash:
required: true
type: string
free-threading:
description: Whether to use free-threaded mode
required: false
type: boolean
default: false
env:
FORCE_COLOR: 1
jobs:
build-san-reusable:
name: >-
${{ inputs.sanitizer }}${{
inputs.free-threading
&& ' (free-threading)'
|| ''
}}
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- name: Restore config.cache
uses: actions/cache@v4
with:
path: config.cache
key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.sanitizer }}-${{ inputs.config_hash }}
- name: Install dependencies
run: |
sudo ./.github/workflows/posix-deps-apt.sh
# Install clang
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
if [ "${SANITIZER}" = "TSan" ]; then
sudo ./llvm.sh 17 # gh-121946: llvm-18 package is temporarily broken
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 100
sudo update-alternatives --set clang /usr/bin/clang-17
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-17 100
sudo update-alternatives --set clang++ /usr/bin/clang++-17
# Reduce ASLR to avoid TSan crashing
sudo sysctl -w vm.mmap_rnd_bits=28
else
sudo ./llvm.sh 20
fi
- name: Sanitizer option setup
run: |
if [ "${SANITIZER}" = "TSan" ]; then
echo "TSAN_OPTIONS=${SAN_LOG_OPTION} suppressions=${GITHUB_WORKSPACE}/Tools/tsan/suppressions${{
fromJSON(inputs.free-threading)
&& '_free_threading'
|| ''
}}.txt handle_segv=0" >> "$GITHUB_ENV"
else
echo "UBSAN_OPTIONS=${SAN_LOG_OPTION}" >> "$GITHUB_ENV"
fi
echo "CC=clang" >> "$GITHUB_ENV"
echo "CXX=clang++" >> "$GITHUB_ENV"
env:
SANITIZER: ${{ inputs.sanitizer }}
SAN_LOG_OPTION: log_path=${{ github.workspace }}/san_log
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- name: Configure ccache action
uses: hendrikmuhs/ccache-action@v1.2
with:
save: ${{ github.event_name == 'push' }}
max-size: "200M"
- name: Configure CPython
run: >-
./configure
--config-cache
${{
inputs.sanitizer == 'TSan'
&& '--with-thread-sanitizer'
|| '--with-undefined-behavior-sanitizer'
}}
--with-pydebug
${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }}
- name: Build CPython
run: make -j4
- name: Display build info
run: make pythoninfo
- name: Tests
run: >-
./python -m test
${{ inputs.sanitizer == 'TSan' && '--tsan' || '' }}
-j4
- name: Parallel tests
if: >-
inputs.sanitizer == 'TSan'
&& fromJSON(inputs.free-threading)
run: ./python -m test --tsan-parallel --parallel-threads=4 -j4
- name: Display logs
if: always()
run: find "${GITHUB_WORKSPACE}" -name 'san_log.*' | xargs head -n 1000
- name: Archive logs
if: always()
uses: actions/upload-artifact@v4
with:
name: >-
${{ inputs.sanitizer }}-logs-${{
fromJSON(inputs.free-threading)
&& 'free-threading'
|| 'default'
}}
path: san_log.*
if-no-files-found: ignore

View file

@ -1,55 +0,0 @@
on:
workflow_call:
inputs:
config_hash:
required: true
type: string
options:
required: true
type: string
suppressions_path:
description: 'A repo relative path to the suppressions file'
required: true
type: string
jobs:
build_tsan_reusable:
name: 'Thread sanitizer'
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Runner image version
run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV
- name: Restore config.cache
uses: actions/cache@v4
with:
path: config.cache
key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ inputs.config_hash }}
- name: Install Dependencies
run: |
sudo ./.github/workflows/posix-deps-apt.sh
sudo apt install -y clang
# Reduce ASLR to avoid TSAN crashing
sudo sysctl -w vm.mmap_rnd_bits=28
- name: TSAN Option Setup
run: |
echo "TSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/${{ inputs.suppressions_path }}" >> $GITHUB_ENV
echo "CC=clang" >> $GITHUB_ENV
echo "CXX=clang++" >> $GITHUB_ENV
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV
- name: Configure ccache action
uses: hendrikmuhs/ccache-action@v1.2
with:
save: ${{ github.event_name == 'push' }}
max-size: "200M"
- name: Configure CPython
run: ${{ inputs.options }}
- name: Build CPython
run: make -j4
- name: Display build info
run: make pythoninfo
- name: Tests
run: ./python -m test --tsan -j4

View file

@ -1,44 +1,69 @@
name: Reusable Ubuntu
on: on:
workflow_call: workflow_call:
inputs: inputs:
config_hash: config_hash:
required: true required: true
type: string type: string
options: bolt-optimizations:
required: true description: Whether to enable BOLT optimizations
type: string required: false
type: boolean
default: false
free-threading:
description: Whether to use free-threaded mode
required: false
type: boolean
default: false
os:
description: OS to run the job
required: true
type: string
env:
FORCE_COLOR: 1
jobs: jobs:
build_ubuntu_reusable: build-ubuntu-reusable:
name: 'build and test' name: build and test (${{ inputs.os }})
runs-on: ${{ inputs.os }}
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-20.04
env: env:
OPENSSL_VER: 3.0.13 OPENSSL_VER: 3.0.18
PYTHONSTRICTEXTENSIONBUILD: 1 PYTHONSTRICTEXTENSIONBUILD: 1
TERM: linux
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Register gcc problem matcher - name: Register gcc problem matcher
run: echo "::add-matcher::.github/problem-matchers/gcc.json" run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Install dependencies - name: Install dependencies
run: sudo ./.github/workflows/posix-deps-apt.sh run: sudo ./.github/workflows/posix-deps-apt.sh
- name: Install Clang and BOLT
if: ${{ fromJSON(inputs.bolt-optimizations) }}
run: |
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh 19
sudo apt-get install bolt-19
echo PATH="$(llvm-config-19 --bindir):$PATH" >> $GITHUB_ENV
- name: Configure OpenSSL env vars - name: Configure OpenSSL env vars
run: | run: |
echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> $GITHUB_ENV echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> "$GITHUB_ENV"
echo "OPENSSL_DIR=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}" >> $GITHUB_ENV echo "OPENSSL_DIR=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}/lib" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}/lib" >> "$GITHUB_ENV"
- name: 'Restore OpenSSL build' - name: 'Restore OpenSSL build'
id: cache-openssl id: cache-openssl
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ./multissl/openssl/${{ env.OPENSSL_VER }} path: ./multissl/openssl/${{ env.OPENSSL_VER }}
key: ${{ runner.os }}-multissl-openssl-${{ env.OPENSSL_VER }} key: ${{ inputs.os }}-multissl-openssl-${{ env.OPENSSL_VER }}
- name: Install OpenSSL - name: Install OpenSSL
if: steps.cache-openssl.outputs.cache-hit != 'true' if: steps.cache-openssl.outputs.cache-hit != 'true'
run: python3 Tools/ssl/multissltests.py --steps=library --base-directory $MULTISSL_DIR --openssl $OPENSSL_VER --system Linux run: python3 Tools/ssl/multissltests.py --steps=library --base-directory "$MULTISSL_DIR" --openssl "$OPENSSL_VER" --system Linux
- name: Add ccache to PATH - name: Add ccache to PATH
run: | run: |
echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- name: Configure ccache action - name: Configure ccache action
uses: hendrikmuhs/ccache-action@v1.2 uses: hendrikmuhs/ccache-action@v1.2
with: with:
@ -46,31 +71,57 @@ jobs:
max-size: "200M" max-size: "200M"
- name: Setup directory envs for out-of-tree builds - name: Setup directory envs for out-of-tree builds
run: | run: |
echo "CPYTHON_RO_SRCDIR=$(realpath -m ${GITHUB_WORKSPACE}/../cpython-ro-srcdir)" >> $GITHUB_ENV echo "CPYTHON_RO_SRCDIR=$(realpath -m "${GITHUB_WORKSPACE}"/../cpython-ro-srcdir)" >> "$GITHUB_ENV"
echo "CPYTHON_BUILDDIR=$(realpath -m ${GITHUB_WORKSPACE}/../cpython-builddir)" >> $GITHUB_ENV echo "CPYTHON_BUILDDIR=$(realpath -m "${GITHUB_WORKSPACE}"/../cpython-builddir)" >> "$GITHUB_ENV"
- name: Create directories for read-only out-of-tree builds - name: Create directories for read-only out-of-tree builds
run: mkdir -p $CPYTHON_RO_SRCDIR $CPYTHON_BUILDDIR run: mkdir -p "$CPYTHON_RO_SRCDIR" "$CPYTHON_BUILDDIR"
- name: Bind mount sources read-only - name: Bind mount sources read-only
run: sudo mount --bind -o ro $GITHUB_WORKSPACE $CPYTHON_RO_SRCDIR run: sudo mount --bind -o ro "$GITHUB_WORKSPACE" "$CPYTHON_RO_SRCDIR"
- name: Runner image version - name: Runner image version
run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- name: Restore config.cache - name: Restore config.cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ${{ env.CPYTHON_BUILDDIR }}/config.cache path: ${{ env.CPYTHON_BUILDDIR }}/config.cache
key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ inputs.config_hash }} key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.config_hash }}
- name: Configure CPython out-of-tree - name: Configure CPython out-of-tree
working-directory: ${{ env.CPYTHON_BUILDDIR }} working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: ${{ inputs.options }} # `test_unpickle_module_race` writes to the source directory, which is
# read-only during builds — so we exclude it from profiling with BOLT.
run: >-
PROFILE_TASK='-m test --pgo --ignore test_unpickle_module_race'
../cpython-ro-srcdir/configure
--config-cache
--with-pydebug
--enable-slower-safety
--enable-safety
--with-openssl="$OPENSSL_DIR"
${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }}
${{ fromJSON(inputs.bolt-optimizations) && '--enable-bolt' || '' }}
- name: Build CPython out-of-tree - name: Build CPython out-of-tree
if: ${{ inputs.free-threading }}
working-directory: ${{ env.CPYTHON_BUILDDIR }} working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: make -j4 run: make -j
- name: Build CPython out-of-tree (for compiler warning check)
if: ${{ !inputs.free-threading }}
working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: set -o pipefail; make -j --output-sync 2>&1 | tee compiler_output_ubuntu.txt
- name: Display build info - name: Display build info
working-directory: ${{ env.CPYTHON_BUILDDIR }} working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: make pythoninfo run: make pythoninfo
- name: Check compiler warnings
if: ${{ !inputs.free-threading }}
run: >-
python Tools/build/check_warnings.py
--compiler-output-file-path="${CPYTHON_BUILDDIR}/compiler_output_ubuntu.txt"
--warning-ignore-file-path "${GITHUB_WORKSPACE}/Tools/build/.warningignore_ubuntu"
--compiler-output-type=gcc
--fail-on-regression
--fail-on-improvement
--path-prefix="../cpython-ro-srcdir/"
- name: Remount sources writable for tests - name: Remount sources writable for tests
# some tests write to srcdir, lack of pyc files slows down testing # some tests write to srcdir, lack of pyc files slows down testing
run: sudo mount $CPYTHON_RO_SRCDIR -oremount,rw run: sudo mount "$CPYTHON_RO_SRCDIR" -oremount,rw
- name: Tests - name: Tests
working-directory: ${{ env.CPYTHON_BUILDDIR }} working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: xvfb-run make test run: xvfb-run make ci

View file

@ -1,3 +1,5 @@
name: Reusable WASI
on: on:
workflow_call: workflow_call:
inputs: inputs:
@ -5,53 +7,62 @@ on:
required: true required: true
type: string type: string
env:
FORCE_COLOR: 1
jobs: jobs:
build_wasi_reusable: build-wasi-reusable:
name: 'build and test' name: 'build and test'
runs-on: ubuntu-24.04
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-20.04
env: env:
WASMTIME_VERSION: 18.0.3 WASMTIME_VERSION: 22.0.0
WASI_SDK_VERSION: 21 WASI_SDK_VERSION: 24
WASI_SDK_PATH: /opt/wasi-sdk WASI_SDK_PATH: /opt/wasi-sdk
CROSS_BUILD_PYTHON: cross-build/build CROSS_BUILD_PYTHON: cross-build/build
CROSS_BUILD_WASI: cross-build/wasm32-wasi CROSS_BUILD_WASI: cross-build/wasm32-wasip1
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
# No problem resolver registered as one doesn't currently exist for Clang. # No problem resolver registered as one doesn't currently exist for Clang.
- name: "Install wasmtime" - name: "Install wasmtime"
uses: jcbhmr/setup-wasmtime@v2 uses: bytecodealliance/actions/wasmtime/setup@v1
with: with:
wasmtime-version: ${{ env.WASMTIME_VERSION }} version: ${{ env.WASMTIME_VERSION }}
- name: "Restore WASI SDK" - name: "Restore WASI SDK"
id: cache-wasi-sdk id: cache-wasi-sdk
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ${{ env.WASI_SDK_PATH }} path: ${{ env.WASI_SDK_PATH }}
key: ${{ runner.os }}-wasi-sdk-${{ env.WASI_SDK_VERSION }} key: ${{ runner.os }}-wasi-sdk-${{ env.WASI_SDK_VERSION }}
- name: "Install WASI SDK" - name: "Install WASI SDK" # Hard-coded to x64.
if: steps.cache-wasi-sdk.outputs.cache-hit != 'true' if: steps.cache-wasi-sdk.outputs.cache-hit != 'true'
run: | run: |
mkdir ${{ env.WASI_SDK_PATH }} && \ mkdir "${WASI_SDK_PATH}" && \
curl -s -S --location https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${{ env.WASI_SDK_VERSION }}/wasi-sdk-${{ env.WASI_SDK_VERSION }}.0-linux.tar.gz | \ curl -s -S --location "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" | \
tar --strip-components 1 --directory ${{ env.WASI_SDK_PATH }} --extract --gunzip tar --strip-components 1 --directory "${WASI_SDK_PATH}" --extract --gunzip
- name: "Configure ccache action" - name: "Configure ccache action"
uses: hendrikmuhs/ccache-action@v1.2 uses: hendrikmuhs/ccache-action@v1.2
with: with:
save: ${{ github.event_name == 'push' }} save: ${{ github.event_name == 'push' }}
max-size: "200M" max-size: "200M"
- name: "Add ccache to PATH" - name: "Add ccache to PATH"
run: echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV run: echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- name: "Install Python" - name: "Install Python"
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.x' python-version: '3.x'
- name: "Runner image version"
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- name: "Restore Python build config.cache" - name: "Restore Python build config.cache"
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ${{ env.CROSS_BUILD_PYTHON }}/config.cache path: ${{ env.CROSS_BUILD_PYTHON }}/config.cache
# Include env.pythonLocation in key to avoid changes in environment when setup-python updates Python # Include env.pythonLocation in key to avoid changes in environment when setup-python updates Python.
key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ inputs.config_hash }}-${{ env.pythonLocation }} # Include the hash of `Tools/wasm/wasi.py` as it may change the environment variables.
# (Make sure to keep the key in sync with the other config.cache step below.)
key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ env.WASI_SDK_VERSION }}-${{ env.WASMTIME_VERSION }}-${{ inputs.config_hash }}-${{ hashFiles('Tools/wasm/wasi.py') }}-${{ env.pythonLocation }}
- name: "Configure build Python" - name: "Configure build Python"
run: python3 Tools/wasm/wasi.py configure-build-python -- --config-cache --with-pydebug run: python3 Tools/wasm/wasi.py configure-build-python -- --config-cache --with-pydebug
- name: "Make build Python" - name: "Make build Python"
@ -60,14 +71,14 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ${{ env.CROSS_BUILD_WASI }}/config.cache path: ${{ env.CROSS_BUILD_WASI }}/config.cache
# Include env.pythonLocation in key to avoid changes in environment when setup-python updates Python # Should be kept in sync with the other config.cache step above.
key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-wasi-sdk-${{ env.WASI_SDK_VERSION }}-${{ inputs.config_hash }}-${{ env.pythonLocation }} key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ env.WASI_SDK_VERSION }}-${{ env.WASMTIME_VERSION }}-${{ inputs.config_hash }}-${{ hashFiles('Tools/wasm/wasi.py') }}-${{ env.pythonLocation }}
- name: "Configure host" - name: "Configure host"
# `--with-pydebug` inferred from configure-build-python # `--with-pydebug` inferred from configure-build-python
run: python3 Tools/wasm/wasi.py configure-host -- --config-cache run: python3 Tools/wasm/wasi.py configure-host -- --config-cache
- name: "Make host" - name: "Make host"
run: python3 Tools/wasm/wasi.py make-host run: python3 Tools/wasm/wasi.py make-host
- name: "Display build info" - name: "Display build info"
run: make --directory ${{ env.CROSS_BUILD_WASI }} pythoninfo run: make --directory "${CROSS_BUILD_WASI}" pythoninfo
- name: "Test" - name: "Test"
run: make --directory ${{ env.CROSS_BUILD_WASI }} test run: make --directory "${CROSS_BUILD_WASI}" test

View file

@ -0,0 +1,31 @@
name: Reusable Windows MSI
on:
workflow_call:
inputs:
arch:
description: CPU architecture
required: true
type: string
permissions:
contents: read
env:
FORCE_COLOR: 1
jobs:
build:
name: installer for ${{ inputs.arch }}
runs-on: ${{ inputs.arch == 'arm64' && 'windows-11-arm' || 'windows-2022' }}
timeout-minutes: 60
env:
ARCH: ${{ inputs.arch }}
IncludeFreethreaded: true
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build CPython installer
run: ./Tools/msi/build.bat --doc -"${ARCH}"
shell: bash

View file

@ -1,53 +1,50 @@
name: Reusable Windows
on: on:
workflow_call: workflow_call:
inputs: inputs:
arch:
description: CPU architecture
required: true
type: string
free-threading: free-threading:
description: Whether to compile CPython in free-threading mode
required: false required: false
type: boolean type: boolean
default: false default: false
env:
FORCE_COLOR: 1
IncludeUwp: >-
true
jobs: jobs:
build_win32: build:
name: 'build and test (x86)' name: Build and test (${{ inputs.arch }})
runs-on: windows-latest runs-on: ${{ inputs.arch == 'arm64' && 'windows-11-arm' || 'windows-2022' }}
timeout-minutes: 60 timeout-minutes: 60
env: env:
IncludeUwp: 'true' ARCH: ${{ inputs.arch }}
steps:
- uses: actions/checkout@v4
- name: Build CPython
run: .\PCbuild\build.bat -e -d -v -p Win32 ${{ inputs.free-threading && '--disable-gil' || '' }}
- name: Display build info
run: .\python.bat -m test.pythoninfo
- name: Tests
run: .\PCbuild\rt.bat -p Win32 -d -q --fast-ci ${{ inputs.free-threading && '--disable-gil' || '' }}
build_win_amd64:
name: 'build and test (x64)'
runs-on: windows-latest
timeout-minutes: 60
env:
IncludeUwp: 'true'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Register MSVC problem matcher - name: Register MSVC problem matcher
if: inputs.arch != 'Win32'
run: echo "::add-matcher::.github/problem-matchers/msvc.json" run: echo "::add-matcher::.github/problem-matchers/msvc.json"
- name: Build CPython - name: Build CPython
run: .\PCbuild\build.bat -e -d -v -p x64 ${{ inputs.free-threading && '--disable-gil' || '' }} run: >-
.\\PCbuild\\build.bat
-e -d -v
-p "${ARCH}"
${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }}
shell: bash
- name: Display build info - name: Display build info
run: .\python.bat -m test.pythoninfo run: .\\python.bat -m test.pythoninfo
- name: Tests - name: Tests
run: .\PCbuild\rt.bat -p x64 -d -q --fast-ci ${{ inputs.free-threading && '--disable-gil' || '' }} run: >-
.\\PCbuild\\rt.bat
build_win_arm64: -p "${ARCH}"
name: 'build (arm64)' -d -q --fast-ci
runs-on: windows-latest ${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }}
timeout-minutes: 60 shell: bash
env:
IncludeUwp: 'true'
steps:
- uses: actions/checkout@v4
- name: Register MSVC problem matcher
run: echo "::add-matcher::.github/problem-matchers/msvc.json"
- name: Build CPython
run: .\PCbuild\build.bat -e -d -v -p arm64 ${{ inputs.free-threading && '--disable-gil' || '' }}

View file

@ -4,14 +4,12 @@ on:
schedule: schedule:
- cron: "0 */6 * * *" - cron: "0 */6 * * *"
permissions:
pull-requests: write
jobs: jobs:
stale: stale:
if: github.repository_owner == 'python' if: github.repository_owner == 'python'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
timeout-minutes: 10 timeout-minutes: 10
steps: steps:

132
.github/workflows/tail-call.yml vendored Normal file
View file

@ -0,0 +1,132 @@
name: Tail calling interpreter
on:
pull_request:
paths:
- '.github/workflows/tail-call.yml'
- 'Python/bytecodes.c'
- 'Python/ceval.c'
- 'Python/ceval_macros.h'
- 'Python/generated_cases.c.h'
push:
paths:
- '.github/workflows/tail-call.yml'
- 'Python/bytecodes.c'
- 'Python/ceval.c'
- 'Python/ceval_macros.h'
- 'Python/generated_cases.c.h'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
tail-call:
name: ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
target:
# Un-comment as we add support for more platforms for tail-calling interpreters.
# - i686-pc-windows-msvc/msvc
- x86_64-pc-windows-msvc/msvc
# - aarch64-pc-windows-msvc/msvc
- x86_64-apple-darwin/clang
- aarch64-apple-darwin/clang
- x86_64-unknown-linux-gnu/gcc
- aarch64-unknown-linux-gnu/gcc
- free-threading
llvm:
- 20
include:
# - target: i686-pc-windows-msvc/msvc
# architecture: Win32
# runner: windows-2022
- target: x86_64-pc-windows-msvc/msvc
architecture: x64
runner: windows-2022
# - target: aarch64-pc-windows-msvc/msvc
# architecture: ARM64
# runner: windows-2022
- target: x86_64-apple-darwin/clang
architecture: x86_64
runner: macos-15-intel
- target: aarch64-apple-darwin/clang
architecture: aarch64
runner: macos-14
- target: x86_64-unknown-linux-gnu/gcc
architecture: x86_64
runner: ubuntu-24.04
- target: aarch64-unknown-linux-gnu/gcc
architecture: aarch64
runner: ubuntu-24.04-arm
- target: free-threading
architecture: x86_64
runner: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Native Windows (debug)
if: runner.os == 'Windows' && matrix.architecture != 'ARM64'
shell: cmd
run: |
choco install llvm --allow-downgrade --no-progress --version ${{ matrix.llvm }}.1.0
set PlatformToolset=clangcl
set LLVMToolsVersion=${{ matrix.llvm }}.1.0
set LLVMInstallDir=C:\Program Files\LLVM
call ./PCbuild/build.bat --tail-call-interp -d -p ${{ matrix.architecture }}
call ./PCbuild/rt.bat -d -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3
# No tests (yet):
- name: Emulated Windows (release)
if: runner.os == 'Windows' && matrix.architecture == 'ARM64'
shell: cmd
run: |
choco install llvm --allow-downgrade --no-progress --version ${{ matrix.llvm }}.1.0
set PlatformToolset=clangcl
set LLVMToolsVersion=${{ matrix.llvm }}.1.0
set LLVMInstallDir=C:\Program Files\LLVM
./PCbuild/build.bat --tail-call-interp -p ${{ matrix.architecture }}
- name: Native macOS (release)
if: runner.os == 'macOS'
run: |
brew update
brew install llvm@${{ matrix.llvm }}
export SDKROOT="$(xcrun --show-sdk-path)"
export PATH="/usr/local/opt/llvm@${{ matrix.llvm }}/bin:$PATH"
export PATH="/opt/homebrew/opt/llvm@${{ matrix.llvm }}/bin:$PATH"
CC=clang-20 ./configure --with-tail-call-interp
make all --jobs 4
./python.exe -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
- name: Native Linux (debug)
if: runner.os == 'Linux' && matrix.target != 'free-threading'
run: |
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
CC=clang-20 ./configure --with-tail-call-interp --with-pydebug
make all --jobs 4
./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
- name: Native Linux with free-threading (release)
if: matrix.target == 'free-threading'
run: |
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
CC=clang-20 ./configure --with-tail-call-interp --disable-gil
make all --jobs 4
./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3

View file

@ -26,6 +26,8 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3' python-version: '3'

10
.github/zizmor.yml vendored Normal file
View file

@ -0,0 +1,10 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
rules:
dangerous-triggers:
ignore:
- documentation-links.yml
unpinned-uses:
config:
policies:
"*": ref-pin

29
.gitignore vendored
View file

@ -38,6 +38,7 @@ tags
TAGS TAGS
.vs/ .vs/
.vscode/ .vscode/
.cache/
gmon.out gmon.out
.coverage .coverage
.mypy_cache/ .mypy_cache/
@ -70,16 +71,15 @@ Lib/test/data/*
/Makefile /Makefile
/Makefile.pre /Makefile.pre
/iOSTestbed.* /iOSTestbed.*
iOS/Frameworks/ Apple/iOS/Frameworks/
iOS/Resources/Info.plist Apple/iOS/Resources/Info.plist
iOS/testbed/build Apple/testbed/build
iOS/testbed/Python.xcframework/ios-*/bin Apple/testbed/Python.xcframework/*/bin
iOS/testbed/Python.xcframework/ios-*/include Apple/testbed/Python.xcframework/*/include
iOS/testbed/Python.xcframework/ios-*/lib Apple/testbed/Python.xcframework/*/lib
iOS/testbed/Python.xcframework/ios-*/Python.framework Apple/testbed/Python.xcframework/*/Python.framework
iOS/testbed/iOSTestbed.xcodeproj/project.xcworkspace Apple/testbed/*Testbed.xcodeproj/project.xcworkspace
iOS/testbed/iOSTestbed.xcodeproj/xcuserdata Apple/testbed/*Testbed.xcodeproj/xcuserdata
iOS/testbed/iOSTestbed.xcodeproj/xcshareddata
Mac/Makefile Mac/Makefile
Mac/PythonLauncher/Info.plist Mac/PythonLauncher/Info.plist
Mac/PythonLauncher/Makefile Mac/PythonLauncher/Makefile
@ -130,6 +130,7 @@ Tools/unicode/data/
/autom4te.cache /autom4te.cache
/build/ /build/
/builddir/ /builddir/
/compile_commands.json
/config.cache /config.cache
/config.log /config.log
/config.status /config.status
@ -137,11 +138,12 @@ Tools/unicode/data/
# hendrikmuhs/ccache-action@v1 # hendrikmuhs/ccache-action@v1
/.ccache /.ccache
/cross-build/ /cross-build/
/jit_stencils.h /jit_stencils*.h
/platform /platform
/profile-clean-stamp /profile-clean-stamp
/profile-run-stamp /profile-run-stamp
/profile-bolt-stamp /profile-bolt-stamp
/profile-gen-stamp
/pybuilddir.txt /pybuilddir.txt
/pyconfig.h /pyconfig.h
/python-config /python-config
@ -169,5 +171,10 @@ Python/frozen_modules/MANIFEST
/python /python
!/Python/ !/Python/
# People's custom https://docs.anthropic.com/en/docs/claude-code/memory configs.
/.claude/
CLAUDE.local.md
#### main branch only stuff below this line, things to backport go above. ####
# main branch only: ABI files are not checked/maintained. # main branch only: ABI files are not checked/maintained.
Doc/data/python*.abi Doc/data/python*.abi

View file

@ -1,3 +1,4 @@
# This file sets the canonical name for contributors to the repository. # This file sets the canonical name for contributors to the repository.
# Documentation: https://git-scm.com/docs/gitmailmap # Documentation: https://git-scm.com/docs/gitmailmap
Willow Chargin <wchargin@gmail.com>
Amethyst Reese <amethyst@n7.gg> <john@noswap.com> Amethyst Reese <amethyst@n7.gg> <john@noswap.com>

View file

@ -1,18 +1,63 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.4 rev: v0.13.2
hooks: hooks:
- id: ruff - id: ruff-check
name: Run Ruff on Lib/test/ name: Run Ruff (lint) on Doc/
args: [--exit-non-zero-on-fix]
files: ^Doc/
- id: ruff-check
name: Run Ruff (lint) on Lib/test/
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
files: ^Lib/test/ files: ^Lib/test/
- id: ruff - id: ruff-check
name: Run Ruff on Argument Clinic name: Run Ruff (lint) on Tools/build/
args: [--exit-non-zero-on-fix, --config=Tools/build/.ruff.toml]
files: ^Tools/build/
- id: ruff-check
name: Run Ruff (lint) on Tools/i18n/
args: [--exit-non-zero-on-fix, --config=Tools/i18n/.ruff.toml]
files: ^Tools/i18n/
- id: ruff-check
name: Run Ruff (lint) on Argument Clinic
args: [--exit-non-zero-on-fix, --config=Tools/clinic/.ruff.toml] args: [--exit-non-zero-on-fix, --config=Tools/clinic/.ruff.toml]
files: ^Tools/clinic/|Lib/test/test_clinic.py files: ^Tools/clinic/|Lib/test/test_clinic.py
- id: ruff-check
name: Run Ruff (lint) on Tools/peg_generator/
args: [--exit-non-zero-on-fix, --config=Tools/peg_generator/.ruff.toml]
files: ^Tools/peg_generator/
- id: ruff-check
name: Run Ruff (lint) on Tools/wasm/
args: [--exit-non-zero-on-fix, --config=Tools/wasm/.ruff.toml]
files: ^Tools/wasm/
- id: ruff-format
name: Run Ruff (format) on Doc/
args: [--check]
files: ^Doc/
- id: ruff-format
name: Run Ruff (format) on Tools/build/check_warnings.py
args: [--check, --config=Tools/build/.ruff.toml]
files: ^Tools/build/check_warnings.py
- id: ruff-format
name: Run Ruff (format) on Tools/wasm/
args: [--check, --config=Tools/wasm/.ruff.toml]
files: ^Tools/wasm/
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.9.0
hooks:
- id: black
name: Run Black on Tools/jit/
files: ^Tools/jit/
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
hooks:
- id: remove-tabs
types: [python]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0 rev: v6.0.0
hooks: hooks:
- id: check-case-conflict - id: check-case-conflict
- id: check-merge-conflict - id: check-merge-conflict
@ -20,18 +65,55 @@ repos:
exclude: ^Lib/test/test_tomllib/ exclude: ^Lib/test/test_tomllib/
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
types: [python] types_or: [python, yaml]
exclude: Lib/test/tokenizedata/coding20731.py exclude: Lib/test/tokenizedata/coding20731.py
- id: end-of-file-fixer
files: '^\.github/CODEOWNERS$'
- id: trailing-whitespace - id: trailing-whitespace
types_or: [c, inc, python, rst] types_or: [c, inc, python, rst, yaml]
- id: trailing-whitespace
files: '^\.github/CODEOWNERS|\.(gram)$'
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.34.0
hooks:
- id: check-dependabot
- id: check-github-workflows
- id: check-readthedocs
- repo: https://github.com/rhysd/actionlint
rev: v1.7.7
hooks:
- id: actionlint
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.14.1
hooks:
- id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.9.1 rev: v1.0.0
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
args: [--enable=default-role] args: [--enable=default-role]
files: ^Doc/|^Misc/NEWS.d/ files: ^Doc/|^Misc/NEWS.d/
- repo: local
hooks:
- id: blurb-no-space-c-api
name: Check C API news entries
language: fail
entry: Space found in path, move to Misc/NEWS.d/next/C_API/
files: Misc/NEWS.d/next/C API/20.*.rst
- repo: local
hooks:
- id: blurb-no-space-core-and-builtins
name: Check Core and Builtins news entries
language: fail
entry: Space found in path, move to Misc/NEWS.d/next/Core_and_Builtins/
files: Misc/NEWS.d/next/Core and Builtins/20.*.rst
- repo: meta - repo: meta
hooks: hooks:
- id: check-hooks-apply - id: check-hooks-apply

View file

@ -8,7 +8,7 @@ sphinx:
configuration: Doc/conf.py configuration: Doc/conf.py
build: build:
os: ubuntu-22.04 os: ubuntu-24.04
tools: tools:
python: "3" python: "3"
@ -26,7 +26,9 @@ build:
exit 183; exit 183;
fi fi
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
- make -C Doc venv html - make -C Doc venv html
- mkdir _readthedocs - mkdir _readthedocs
- mv Doc/build/html _readthedocs/html - mv Doc/build/html _readthedocs/html

12
.ruff.toml Normal file
View file

@ -0,0 +1,12 @@
# Default settings for Ruff in CPython
# PYTHON_FOR_REGEN
target-version = "py310"
# PEP 8
line-length = 79
# Enable automatic fixes by default.
# To override this, use ``fix = false`` in a subdirectory's config file
# or ``--no-fix`` on the command line.
fix = true

View file

@ -0,0 +1 @@
https://www.python.org/funding.json

View file

@ -1,19 +1,22 @@
# Python for Android # Python for Android
These instructions are only needed if you're planning to compile Python for If you obtained this README as part of a release package, then the only
Android yourself. Most users should *not* need to do this. If you're looking to applicable sections are "Prerequisites", "Testing", and "Using in your own app".
use Python on Android, one of the following tools will provide a much more
approachable user experience:
* [Briefcase](https://briefcase.readthedocs.io), from the BeeWare project If you obtained this README as part of the CPython source tree, then you can
* [Buildozer](https://buildozer.readthedocs.io), from the Kivy project also follow the other sections to compile Python for Android yourself.
* [Chaquopy](https://chaquo.com/chaquopy/)
However, most app developers should not need to do any of these things manually.
Instead, use one of the tools listed
[here](https://docs.python.org/3/using/android.html), which will provide a much
easier experience.
## Prerequisites ## Prerequisites
Export the `ANDROID_HOME` environment variable to point at your Android SDK. If If you already have an Android SDK installed, export the `ANDROID_HOME`
you don't already have the SDK, here's how to install it: environment variable to point at its location. Otherwise, here's how to install
it:
* Download the "Command line tools" from <https://developer.android.com/studio>. * Download the "Command line tools" from <https://developer.android.com/studio>.
* Create a directory `android-sdk/cmdline-tools`, and unzip the command line * Create a directory `android-sdk/cmdline-tools`, and unzip the command line
@ -22,12 +25,23 @@ ## Prerequisites
`android-sdk/cmdline-tools/latest`. `android-sdk/cmdline-tools/latest`.
* `export ANDROID_HOME=/path/to/android-sdk` * `export ANDROID_HOME=/path/to/android-sdk`
The `android.py` script will automatically use the SDK's `sdkmanager` to install
any packages it needs.
The script also requires the following commands to be on the `PATH`:
* `curl`
* `java` (or set the `JAVA_HOME` environment variable)
## Building ## Building
Building for Android requires doing a cross-build where you have a "build" Python can be built for Android on any POSIX platform supported by the Android
Python to help produce an Android build of CPython. This procedure has been development tools, which currently means Linux or macOS.
tested on Linux and macOS.
First we'll make a "build" Python (for your development machine), then use it to
help produce a "host" Python for Android. So make sure you have all the usual
tools and libraries needed to build Python for your development machine.
The easiest way to do a build is to use the `android.py` script. You can either The easiest way to do a build is to use the `android.py` script. You can either
have it perform the entire build process from start to finish in one step, or have it perform the entire build process from start to finish in one step, or
@ -43,16 +57,17 @@ ## Building
./android.py make-host HOST ./android.py make-host HOST
``` ```
To see the possible values of HOST, run `./android.py configure-host --help`. `HOST` identifies which architecture to build. To see the possible values, run
`./android.py configure-host --help`.
Or to do it all in a single command, run: To do all steps in a single command, run:
```sh ```sh
./android.py build HOST ./android.py build HOST
``` ```
In the end you should have a build Python in `cross-build/build`, and an Android In the end you should have a build Python in `cross-build/build`, and a host
build in `cross-build/HOST`. Python in `cross-build/HOST`.
You can use `--` as a separator for any of the `configure`-related commands You can use `--` as a separator for any of the `configure`-related commands
including `build` itself to pass arguments to the underlying `configure` including `build` itself to pass arguments to the underlying `configure`
@ -62,3 +77,92 @@ ## Building
```sh ```sh
./android.py build HOST -- -C --with-pydebug ./android.py build HOST -- -C --with-pydebug
``` ```
## Packaging
After building an architecture as described in the section above, you can
package it for release with this command:
```sh
./android.py package HOST
```
`HOST` is defined in the section above.
This will generate a tarball in `cross-build/HOST/dist`, whose structure is
similar to the `Android` directory of the CPython source tree.
## Testing
The Python test suite can be run on Linux, macOS, or Windows.
On Linux, the emulator needs access to the KVM virtualization interface. This may
require adding your user to a group, or changing your udev rules. On GitHub
Actions, the test script will do this automatically using the commands shown
[here](https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/).
The test suite can usually be run on a device with 2 GB of RAM, but this is
borderline, so you may need to increase it to 4 GB. As of Android
Studio Koala, 2 GB is the default for all emulators, although the user interface
may indicate otherwise. Locate the emulator's directory under `~/.android/avd`,
and find `hw.ramSize` in both config.ini and hardware-qemu.ini. Either set these
manually to the same value, or use the Android Studio Device Manager, which will
update both files.
You can run the test suite either:
* Within the CPython repository, after doing a build as described above. On
Windows, you won't be able to do the build on the same machine, so you'll have
to copy the `cross-build/HOST/prefix` directory from somewhere else.
* Or by taking a release package built using the `package` command, extracting
it wherever you want, and using its own copy of `android.py`.
The test script supports the following modes:
* In `--connected` mode, it runs on a device or emulator you have already
connected to the build machine. List the available devices with
`$ANDROID_HOME/platform-tools/adb devices -l`, then pass a device ID to the
script like this:
```sh
./android.py test --connected emulator-5554
```
* In `--managed` mode, it uses a temporary headless emulator defined in the
`managedDevices` section of testbed/app/build.gradle.kts. This mode is slower,
but more reproducible.
We currently define two devices: `minVersion` and `maxVersion`, corresponding
to our minimum and maximum supported Android versions. For example:
```sh
./android.py test --managed maxVersion
```
By default, the only messages the script will show are Python's own stdout and
stderr. Add the `-v` option to also show Gradle output, and non-Python logcat
messages.
Any other arguments on the `android.py test` command line will be passed through
to `python -m test` use `--` to separate them from android.py's own options.
See the [Python Developer's
Guide](https://devguide.python.org/testing/run-write-tests/) for common options
most of them will work on Android, except for those that involve subprocesses,
such as `-j`.
Every time you run `android.py test`, changes in pure-Python files in the
repository's `Lib` directory will be picked up immediately. Changes in C files,
and architecture-specific files such as sysconfigdata, will not take effect
until you re-run `android.py make-host` or `build`.
The testbed app can also be used to test third-party packages. For more details,
run `android.py test --help`, paying attention to the options `--site-packages`,
`--cwd`, `-c` and `-m`.
## Using in your own app
See https://docs.python.org/3/using/android.html.

View file

@ -1,10 +1,10 @@
# This script must be sourced with the following variables already set: # This script must be sourced with the following variables already set:
: ${ANDROID_HOME:?} # Path to Android SDK : "${ANDROID_HOME:?}" # Path to Android SDK
: ${HOST:?} # GNU target triplet : "${HOST:?}" # GNU target triplet
# You may also override the following: # You may also override the following:
: ${api_level:=21} # Minimum Android API level the build will run on : "${ANDROID_API_LEVEL:=24}" # Minimum Android API level the build will run on
: ${PREFIX:-} # Path in which to find required libraries : "${PREFIX:-}" # Path in which to find required libraries
# Print all messages on stderr so they're visible when running within build-wheel. # Print all messages on stderr so they're visible when running within build-wheel.
@ -24,26 +24,26 @@ fail() {
# * https://android.googlesource.com/platform/ndk/+/ndk-rXX-release/docs/BuildSystemMaintainers.md # * https://android.googlesource.com/platform/ndk/+/ndk-rXX-release/docs/BuildSystemMaintainers.md
# where XX is the NDK version. Do a diff against the version you're upgrading from, e.g.: # where XX is the NDK version. Do a diff against the version you're upgrading from, e.g.:
# https://android.googlesource.com/platform/ndk/+/ndk-r25-release..ndk-r26-release/docs/BuildSystemMaintainers.md # https://android.googlesource.com/platform/ndk/+/ndk-r25-release..ndk-r26-release/docs/BuildSystemMaintainers.md
ndk_version=26.2.11394342 ndk_version=27.3.13750724
ndk=$ANDROID_HOME/ndk/$ndk_version ndk=$ANDROID_HOME/ndk/$ndk_version
if ! [ -e $ndk ]; then if ! [ -e "$ndk" ]; then
log "Installing NDK: this may take several minutes" log "Installing NDK - this may take several minutes"
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;$ndk_version" yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;$ndk_version"
fi fi
if [ $HOST = "arm-linux-androideabi" ]; then if [ "$HOST" = "arm-linux-androideabi" ]; then
clang_triplet=armv7a-linux-androideabi clang_triplet=armv7a-linux-androideabi
else else
clang_triplet=$HOST clang_triplet="$HOST"
fi fi
# These variables are based on BuildSystemMaintainers.md above, and # These variables are based on BuildSystemMaintainers.md above, and
# $ndk/build/cmake/android.toolchain.cmake. # $ndk/build/cmake/android.toolchain.cmake.
toolchain=$(echo $ndk/toolchains/llvm/prebuilt/*) toolchain=$(echo "$ndk"/toolchains/llvm/prebuilt/*)
export AR="$toolchain/bin/llvm-ar" export AR="$toolchain/bin/llvm-ar"
export AS="$toolchain/bin/llvm-as" export AS="$toolchain/bin/llvm-as"
export CC="$toolchain/bin/${clang_triplet}${api_level}-clang" export CC="$toolchain/bin/${clang_triplet}${ANDROID_API_LEVEL}-clang"
export CXX="${CC}++" export CXX="${CC}++"
export LD="$toolchain/bin/ld" export LD="$toolchain/bin/ld"
export NM="$toolchain/bin/llvm-nm" export NM="$toolchain/bin/llvm-nm"
@ -58,20 +58,26 @@ for path in "$AR" "$AS" "$CC" "$CXX" "$LD" "$NM" "$RANLIB" "$READELF" "$STRIP";
fi fi
done done
export CFLAGS="" export CFLAGS="-D__BIONIC_NO_PAGE_SIZE_MACRO"
export LDFLAGS="-Wl,--build-id=sha1 -Wl,--no-rosegment" export LDFLAGS="-Wl,--build-id=sha1 -Wl,--no-rosegment -Wl,-z,max-page-size=16384"
# Unlike Linux, Android does not implicitly use a dlopened library to resolve
# relocations in subsequently-loaded libraries, even if RTLD_GLOBAL is used
# (https://github.com/android/ndk/issues/1244). So any library that fails to
# build with this flag, would also fail to load at runtime.
LDFLAGS="$LDFLAGS -Wl,--no-undefined"
# Many packages get away with omitting -lm on Linux, but Android is stricter. # Many packages get away with omitting -lm on Linux, but Android is stricter.
LDFLAGS="$LDFLAGS -lm" LDFLAGS="$LDFLAGS -lm"
# -mstackrealign is included where necessary in the clang launcher scripts which are # -mstackrealign is included where necessary in the clang launcher scripts which are
# pointed to by $CC, so we don't need to include it here. # pointed to by $CC, so we don't need to include it here.
if [ $HOST = "arm-linux-androideabi" ]; then if [ "$HOST" = "arm-linux-androideabi" ]; then
CFLAGS="$CFLAGS -march=armv7-a -mthumb" CFLAGS="$CFLAGS -march=armv7-a -mthumb"
fi fi
if [ -n "${PREFIX:-}" ]; then if [ -n "${PREFIX:-}" ]; then
abs_prefix=$(realpath $PREFIX) abs_prefix="$(realpath "$PREFIX")"
CFLAGS="$CFLAGS -I$abs_prefix/include" CFLAGS="$CFLAGS -I$abs_prefix/include"
LDFLAGS="$LDFLAGS -L$abs_prefix/lib" LDFLAGS="$LDFLAGS -L$abs_prefix/lib"
@ -79,9 +85,15 @@ if [ -n "${PREFIX:-}" ]; then
export PKG_CONFIG_LIBDIR="$abs_prefix/lib/pkgconfig" export PKG_CONFIG_LIBDIR="$abs_prefix/lib/pkgconfig"
fi fi
# When compiling C++, some build systems will combine CFLAGS and CXXFLAGS, and some will
# use CXXFLAGS alone.
export CXXFLAGS="$CFLAGS"
# Use the same variable name as conda-build # Use the same variable name as conda-build
if [ $(uname) = "Darwin" ]; then if [ "$(uname)" = "Darwin" ]; then
export CPU_COUNT=$(sysctl -n hw.ncpu) CPU_COUNT="$(sysctl -n hw.ncpu)"
export CPU_COUNT
else else
export CPU_COUNT=$(nproc) CPU_COUNT="$(nproc)"
export CPU_COUNT
fi fi

View file

@ -1,32 +1,87 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import asyncio
import argparse import argparse
import json
import os import os
import platform
import re import re
import shlex
import shutil import shutil
import signal
import subprocess import subprocess
import sys import sys
import sysconfig import sysconfig
from os.path import relpath from asyncio import wait_for
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from glob import glob
from os.path import abspath, basename, relpath
from pathlib import Path from pathlib import Path
from subprocess import CalledProcessError
from tempfile import TemporaryDirectory
SCRIPT_NAME = Path(__file__).name SCRIPT_NAME = Path(__file__).name
CHECKOUT = Path(__file__).resolve().parent.parent ANDROID_DIR = Path(__file__).resolve().parent
CROSS_BUILD_DIR = CHECKOUT / "cross-build" PYTHON_DIR = ANDROID_DIR.parent
in_source_tree = (
ANDROID_DIR.name == "Android" and (PYTHON_DIR / "pyconfig.h.in").exists()
)
TESTBED_DIR = ANDROID_DIR / "testbed"
CROSS_BUILD_DIR = PYTHON_DIR / "cross-build"
HOSTS = ["aarch64-linux-android", "x86_64-linux-android"]
APP_ID = "org.python.testbed"
DECODE_ARGS = ("UTF-8", "backslashreplace")
def delete_if_exists(path): try:
if path.exists(): android_home = Path(os.environ['ANDROID_HOME'])
except KeyError:
sys.exit("The ANDROID_HOME environment variable is required.")
adb = Path(
f"{android_home}/platform-tools/adb"
+ (".exe" if os.name == "nt" else "")
)
gradlew = Path(
f"{TESTBED_DIR}/gradlew"
+ (".bat" if os.name == "nt" else "")
)
# Whether we've seen any output from Python yet.
python_started = False
# Buffer for verbose output which will be displayed only if a test fails and
# there has been no output from Python.
hidden_output = []
def log_verbose(context, line, stream=sys.stdout):
if context.verbose:
stream.write(line)
else:
hidden_output.append((stream, line))
def delete_glob(pattern):
# Path.glob doesn't accept non-relative patterns.
for path in glob(str(pattern)):
path = Path(path)
print(f"Deleting {path} ...") print(f"Deleting {path} ...")
shutil.rmtree(path) if path.is_dir() and not path.is_symlink():
shutil.rmtree(path)
else:
path.unlink()
def subdir(name, *, clean=None): def subdir(*parts, create=False):
path = CROSS_BUILD_DIR / name path = CROSS_BUILD_DIR.joinpath(*parts)
if clean:
delete_if_exists(path)
if not path.exists(): if not path.exists():
if clean is None: if not create:
sys.exit( sys.exit(
f"{path} does not exist. Create it by running the appropriate " f"{path} does not exist. Create it by running the appropriate "
f"`configure` subcommand of {SCRIPT_NAME}.") f"`configure` subcommand of {SCRIPT_NAME}.")
@ -35,39 +90,70 @@ def subdir(name, *, clean=None):
return path return path
def run(command, *, host=None, **kwargs): def run(command, *, host=None, env=None, log=True, **kwargs):
env = os.environ.copy() kwargs.setdefault("check", True)
if env is None:
env = os.environ.copy()
if host: if host:
env_script = CHECKOUT / "Android/android-env.sh" host_env = android_env(host)
env_output = subprocess.run( print_env(host_env)
f"set -eu; " env.update(host_env)
f"HOST={host}; "
f"PREFIX={subdir(host)}/prefix; "
f". {env_script}; "
f"export",
check=True, shell=True, text=True, stdout=subprocess.PIPE
).stdout
for line in env_output.splitlines(): if log:
# We don't require every line to match, as there may be some other print(">", join_command(command))
# output from installing the NDK. return subprocess.run(command, env=env, **kwargs)
if match := re.search(
"^(declare -x |export )?(\\w+)=['\"]?(.*?)['\"]?$", line
):
key, value = match[2], match[3]
if env.get(key) != value:
print(line)
env[key] = value
if env == os.environ:
raise ValueError(f"Found no variables in {env_script.name} output:\n"
+ env_output)
print(">", " ".join(map(str, command))) # Format a command so it can be copied into a shell. Like shlex.join, but also
try: # accepts arguments which are Paths, or a single string/Path outside of a list.
subprocess.run(command, check=True, env=env, **kwargs) def join_command(args):
except subprocess.CalledProcessError as e: if isinstance(args, (str, Path)):
sys.exit(e) return str(args)
else:
return shlex.join(map(str, args))
# Format the environment so it can be pasted into a shell.
def print_env(env):
for key, value in sorted(env.items()):
print(f"export {key}={shlex.quote(value)}")
def android_env(host):
if host:
prefix = subdir(host) / "prefix"
else:
prefix = ANDROID_DIR / "prefix"
sysconfig_files = prefix.glob("lib/python*/_sysconfigdata__android_*.py")
sysconfig_filename = next(sysconfig_files).name
host = re.fullmatch(r"_sysconfigdata__android_(.+).py", sysconfig_filename)[1]
env_script = ANDROID_DIR / "android-env.sh"
env_output = subprocess.run(
f"set -eu; "
f"HOST={host}; "
f"PREFIX={prefix}; "
f". {env_script}; "
f"export",
check=True, shell=True, capture_output=True, encoding='utf-8',
).stdout
env = {}
for line in env_output.splitlines():
# We don't require every line to match, as there may be some other
# output from installing the NDK.
if match := re.search(
"^(declare -x |export )?(\\w+)=['\"]?(.*?)['\"]?$", line
):
key, value = match[2], match[3]
if os.environ.get(key) != value:
env[key] = value
if not env:
raise ValueError(f"Found no variables in {env_script.name} output:\n"
+ env_output)
return env
def build_python_path(): def build_python_path():
@ -84,9 +170,11 @@ def build_python_path():
def configure_build_python(context): def configure_build_python(context):
os.chdir(subdir("build", clean=context.clean)) if context.clean:
clean("build")
os.chdir(subdir("build", create=True))
command = [relpath(CHECKOUT / "configure")] command = [relpath(PYTHON_DIR / "configure")]
if context.args: if context.args:
command.extend(context.args) command.extend(context.args)
run(command) run(command)
@ -97,32 +185,43 @@ def make_build_python(context):
run(["make", "-j", str(os.cpu_count())]) run(["make", "-j", str(os.cpu_count())])
def unpack_deps(host): # To create new builds of these dependencies, usually all that's necessary is to
# push a tag to the cpython-android-source-deps repository, and GitHub Actions
# will do the rest.
#
# If you're a member of the Python core team, and you'd like to be able to push
# these tags yourself, please contact Malcolm Smith or Russell Keith-Magee.
def unpack_deps(host, prefix_dir):
os.chdir(prefix_dir)
deps_url = "https://github.com/beeware/cpython-android-source-deps/releases/download" deps_url = "https://github.com/beeware/cpython-android-source-deps/releases/download"
for name_ver in ["bzip2-1.0.8-1", "libffi-3.4.4-2", "openssl-3.0.13-1", for name_ver in ["bzip2-1.0.8-3", "libffi-3.4.4-3", "openssl-3.0.18-0",
"sqlite-3.45.1-0", "xz-5.4.6-0"]: "sqlite-3.50.4-0", "xz-5.4.6-1", "zstd-1.5.7-1"]:
filename = f"{name_ver}-{host}.tar.gz" filename = f"{name_ver}-{host}.tar.gz"
run(["wget", f"{deps_url}/{name_ver}/{filename}"]) download(f"{deps_url}/{name_ver}/{filename}")
run(["tar", "-xf", filename]) shutil.unpack_archive(filename)
os.remove(filename) os.remove(filename)
def configure_host_python(context): def download(url, target_dir="."):
host_dir = subdir(context.host, clean=context.clean) out_path = f"{target_dir}/{basename(url)}"
run(["curl", "-Lf", "--retry", "5", "--retry-all-errors", "-o", out_path, url])
return out_path
def configure_host_python(context):
if context.clean:
clean(context.host)
host_dir = subdir(context.host, create=True)
prefix_dir = host_dir / "prefix" prefix_dir = host_dir / "prefix"
if not prefix_dir.exists(): if not prefix_dir.exists():
prefix_dir.mkdir() prefix_dir.mkdir()
os.chdir(prefix_dir) unpack_deps(context.host, prefix_dir)
unpack_deps(context.host)
build_dir = host_dir / "build"
build_dir.mkdir(exist_ok=True)
os.chdir(build_dir)
os.chdir(host_dir)
command = [ command = [
# Basic cross-compiling configuration # Basic cross-compiling configuration
relpath(CHECKOUT / "configure"), relpath(PYTHON_DIR / "configure"),
f"--host={context.host}", f"--host={context.host}",
f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}", f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}",
f"--with-build-python={build_python_path()}", f"--with-build-python={build_python_path()}",
@ -143,10 +242,26 @@ def configure_host_python(context):
def make_host_python(context): def make_host_python(context):
# The CFLAGS and LDFLAGS set in android-env include the prefix dir, so
# delete any previous Python installation to prevent it being used during
# the build.
host_dir = subdir(context.host) host_dir = subdir(context.host)
os.chdir(host_dir / "build") prefix_dir = host_dir / "prefix"
run(["make", "-j", str(os.cpu_count())], host=context.host) for pattern in ("include/python*", "lib/libpython*", "lib/python*"):
run(["make", "install", f"prefix={host_dir}/prefix"], host=context.host) delete_glob(f"{prefix_dir}/{pattern}")
# The Android environment variables were already captured in the Makefile by
# `configure`, and passing them again when running `make` may cause some
# flags to be duplicated. So we don't use the `host` argument here.
os.chdir(host_dir)
run(["make", "-j", str(os.cpu_count())])
# The `make install` output is very verbose and rarely useful, so
# suppress it by default.
run(
["make", "install", f"prefix={prefix_dir}"],
capture_output=not context.verbose,
)
def build_all(context): def build_all(context):
@ -156,46 +271,645 @@ def build_all(context):
step(context) step(context)
def clean(host):
delete_glob(CROSS_BUILD_DIR / host)
def clean_all(context): def clean_all(context):
delete_if_exists(CROSS_BUILD_DIR) for host in HOSTS + ["build"]:
clean(host)
def main(): def setup_ci():
# https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/
if "GITHUB_ACTIONS" in os.environ and platform.system() == "Linux":
run(
["sudo", "tee", "/etc/udev/rules.d/99-kvm4all.rules"],
input='KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"\n',
text=True,
)
run(["sudo", "udevadm", "control", "--reload-rules"])
run(["sudo", "udevadm", "trigger", "--name-match=kvm"])
def setup_sdk():
sdkmanager = android_home / (
"cmdline-tools/latest/bin/sdkmanager"
+ (".bat" if os.name == "nt" else "")
)
# Gradle will fail if it needs to install an SDK package whose license
# hasn't been accepted, so pre-accept all licenses.
if not all((android_home / "licenses" / path).exists() for path in [
"android-sdk-arm-dbt-license", "android-sdk-license"
]):
run(
[sdkmanager, "--licenses"],
text=True,
capture_output=True,
input="y\n" * 100,
)
# Gradle may install this automatically, but we can't rely on that because
# we need to run adb within the logcat task.
if not adb.exists():
run([sdkmanager, "platform-tools"])
# To avoid distributing compiled artifacts without corresponding source code,
# the Gradle wrapper is not included in the CPython repository. Instead, we
# extract it from the Gradle GitHub repository.
def setup_testbed():
paths = ["gradlew", "gradlew.bat", "gradle/wrapper/gradle-wrapper.jar"]
if all((TESTBED_DIR / path).exists() for path in paths):
return
# The wrapper version isn't important, as any version of the wrapper can
# download any version of Gradle. The Gradle version actually used for the
# build is specified in testbed/gradle/wrapper/gradle-wrapper.properties.
version = "8.9.0"
for path in paths:
out_path = TESTBED_DIR / path
out_path.parent.mkdir(exist_ok=True)
download(
f"https://raw.githubusercontent.com/gradle/gradle/v{version}/{path}",
out_path.parent,
)
os.chmod(out_path, 0o755)
# run_testbed will build the app automatically, but it's useful to have this as
# a separate command to allow running the app outside of this script.
def build_testbed(context):
setup_sdk()
setup_testbed()
run(
[gradlew, "--console", "plain", "packageDebug", "packageDebugAndroidTest"],
cwd=TESTBED_DIR,
)
# Work around a bug involving sys.exit and TaskGroups
# (https://github.com/python/cpython/issues/101515).
def exit(*args):
raise MySystemExit(*args)
class MySystemExit(Exception):
pass
# The `test` subcommand runs all subprocesses through this context manager so
# that no matter what happens, they can always be cancelled from another task,
# and they will always be cleaned up on exit.
@asynccontextmanager
async def async_process(*args, **kwargs):
process = await asyncio.create_subprocess_exec(*args, **kwargs)
try:
yield process
finally:
if process.returncode is None:
# Allow a reasonably long time for Gradle to clean itself up,
# because we don't want stale emulators left behind.
timeout = 10
process.terminate()
try:
await wait_for(process.wait(), timeout)
except TimeoutError:
print(
f"Command {args} did not terminate after {timeout} seconds "
f" - sending SIGKILL"
)
process.kill()
# Even after killing the process we must still wait for it,
# otherwise we'll get the warning "Exception ignored in __del__".
await wait_for(process.wait(), timeout=1)
async def async_check_output(*args, **kwargs):
async with async_process(
*args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs
) as process:
stdout, stderr = await process.communicate()
if process.returncode == 0:
return stdout.decode(*DECODE_ARGS)
else:
raise CalledProcessError(
process.returncode, args,
stdout.decode(*DECODE_ARGS), stderr.decode(*DECODE_ARGS)
)
# Return a list of the serial numbers of connected devices. Emulators will have
# serials of the form "emulator-5678".
async def list_devices():
serials = []
header_found = False
lines = (await async_check_output(adb, "devices")).splitlines()
for line in lines:
# Ignore blank lines, and all lines before the header.
line = line.strip()
if line == "List of devices attached":
header_found = True
elif header_found and line:
try:
serial, status = line.split()
except ValueError:
raise ValueError(f"failed to parse {line!r}")
if status == "device":
serials.append(serial)
if not header_found:
raise ValueError(f"failed to parse {lines}")
return serials
async def find_device(context, initial_devices):
if context.managed:
print("Waiting for managed device - this may take several minutes")
while True:
new_devices = set(await list_devices()).difference(initial_devices)
if len(new_devices) == 0:
await asyncio.sleep(1)
elif len(new_devices) == 1:
serial = new_devices.pop()
print(f"Serial: {serial}")
return serial
else:
exit(f"Found more than one new device: {new_devices}")
else:
return context.connected
# An older version of this script in #121595 filtered the logs by UID instead.
# But logcat can't filter by UID until API level 31. If we ever switch back to
# filtering by UID, we'll also have to filter by time so we only show messages
# produced after the initial call to `stop_app`.
#
# We're more likely to miss the PID because it's shorter-lived, so there's a
# workaround in PythonSuite.kt to stop it being *too* short-lived.
async def find_pid(serial):
print("Waiting for app to start - this may take several minutes")
shown_error = False
while True:
try:
# `pidof` requires API level 24 or higher. The level 23 emulator
# includes it, but it doesn't work (it returns all processes).
pid = (await async_check_output(
adb, "-s", serial, "shell", "pidof", "-s", APP_ID
)).strip()
except CalledProcessError as e:
# If the app isn't running yet, pidof gives no output. So if there
# is output, there must have been some other error. However, this
# sometimes happens transiently, especially when running a managed
# emulator for the first time, so don't make it fatal.
if (e.stdout or e.stderr) and not shown_error:
print_called_process_error(e)
print("This may be transient, so continuing to wait")
shown_error = True
else:
# Some older devices (e.g. Nexus 4) return zero even when no process
# was found, so check whether we actually got any output.
if pid:
print(f"PID: {pid}")
return pid
# Loop fairly rapidly to avoid missing a short-lived process.
await asyncio.sleep(0.2)
async def logcat_task(context, initial_devices):
# Gradle may need to do some large downloads of libraries and emulator
# images. This will happen during find_device in --managed mode, or find_pid
# in --connected mode.
startup_timeout = 600
serial = await wait_for(find_device(context, initial_devices), startup_timeout)
pid = await wait_for(find_pid(serial), startup_timeout)
# `--pid` requires API level 24 or higher.
args = [adb, "-s", serial, "logcat", "--pid", pid, "--format", "tag"]
logcat_started = False
async with async_process(
*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
) as process:
while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
if match := re.fullmatch(r"([A-Z])/(.*)", line, re.DOTALL):
logcat_started = True
level, message = match.groups()
else:
# If the regex doesn't match, this is either a logcat startup
# error, or the second or subsequent line of a multi-line
# message. Python won't produce multi-line messages, but other
# components might.
level, message = None, line
# Exclude high-volume messages which are rarely useful.
if context.verbose < 2 and "from python test_syslog" in message:
continue
# Put high-level messages on stderr so they're highlighted in the
# buildbot logs. This will include Python's own stderr.
stream = (
sys.stderr
if level in ["W", "E", "F"] # WARNING, ERROR, FATAL (aka ASSERT)
else sys.stdout
)
# To simplify automated processing of the output, e.g. a buildbot
# posting a failure notice on a GitHub PR, we strip the level and
# tag indicators from Python's stdout and stderr.
for prefix in ["python.stdout: ", "python.stderr: "]:
if message.startswith(prefix):
global python_started
python_started = True
stream.write(message.removeprefix(prefix))
break
else:
# Non-Python messages add a lot of noise, but they may
# sometimes help explain a failure.
log_verbose(context, line, stream)
# If the device disconnects while logcat is running, which always
# happens in --managed mode, some versions of adb return non-zero.
# Distinguish this from a logcat startup error by checking whether we've
# received any logcat messages yet.
status = await wait_for(process.wait(), timeout=1)
if status != 0 and not logcat_started:
raise CalledProcessError(status, args)
def stop_app(serial):
run([adb, "-s", serial, "shell", "am", "force-stop", APP_ID], log=False)
async def gradle_task(context):
env = os.environ.copy()
if context.managed:
task_prefix = context.managed
else:
task_prefix = "connected"
env["ANDROID_SERIAL"] = context.connected
if context.ci_mode:
context.args[0:0] = [
# See _add_ci_python_opts in libregrtest/main.py.
"-W", "error", "-bb", "-E",
# Randomization is disabled because order-dependent failures are
# much less likely to pass on a rerun in single-process mode.
"-m", "test",
f"--{context.ci_mode}-ci", "--single-process", "--no-randomize"
]
if not any(arg in context.args for arg in ["-c", "-m"]):
context.args[0:0] = ["-m", "test"]
args = [
gradlew, "--console", "plain", f"{task_prefix}DebugAndroidTest",
] + [
f"-P{name}={value}"
for name, value in [
("python.sitePackages", context.site_packages),
("python.cwd", context.cwd),
(
"android.testInstrumentationRunnerArguments.pythonArgs",
json.dumps(context.args),
),
]
if value
]
if context.verbose >= 2:
args.append("--info")
log_verbose(context, f"> {join_command(args)}\n")
try:
async with async_process(
*args, cwd=TESTBED_DIR, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
) as process:
while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
# Gradle may take several minutes to install SDK packages, so
# it's worth showing those messages even in non-verbose mode.
if line.startswith('Preparing "Install'):
sys.stdout.write(line)
else:
log_verbose(context, line)
status = await wait_for(process.wait(), timeout=1)
if status == 0:
exit(0)
else:
raise CalledProcessError(status, args)
finally:
# Gradle does not stop the tests when interrupted.
if context.connected:
stop_app(context.connected)
async def run_testbed(context):
setup_ci()
setup_sdk()
setup_testbed()
if context.managed:
# In this mode, Gradle will create a device with an unpredictable name.
# So we save a list of the running devices before starting Gradle, and
# find_device then waits for a new device to appear.
initial_devices = await list_devices()
else:
# In case the previous shutdown was unclean, make sure the app isn't
# running, otherwise we might show logs from a previous run. This is
# unnecessary in --managed mode, because Gradle creates a new emulator
# every time.
stop_app(context.connected)
initial_devices = None
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(logcat_task(context, initial_devices))
tg.create_task(gradle_task(context))
except* MySystemExit as e:
raise SystemExit(*e.exceptions[0].args) from None
except* CalledProcessError as e:
# If Python produced no output, then the user probably wants to see the
# verbose output to explain why the test failed.
if not python_started:
for stream, line in hidden_output:
stream.write(line)
# Extract it from the ExceptionGroup so it can be handled by `main`.
raise e.exceptions[0]
def package_version(prefix_dir):
patchlevel_glob = f"{prefix_dir}/include/python*/patchlevel.h"
patchlevel_paths = glob(patchlevel_glob)
if len(patchlevel_paths) != 1:
sys.exit(f"{patchlevel_glob} matched {len(patchlevel_paths)} paths.")
for line in open(patchlevel_paths[0]):
if match := re.fullmatch(r'\s*#define\s+PY_VERSION\s+"(.+)"\s*', line):
version = match[1]
break
else:
sys.exit(f"Failed to find Python version in {patchlevel_paths[0]}.")
# If not building against a tagged commit, add a timestamp to the version.
# Follow the PyPA version number rules, as this will make it easier to
# process with other tools.
if version.endswith("+"):
version += datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S")
return version
def package(context):
prefix_dir = subdir(context.host, "prefix")
version = package_version(prefix_dir)
with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir:
temp_dir = Path(temp_dir)
# Include all tracked files from the Android directory.
for line in run(
["git", "ls-files"],
cwd=ANDROID_DIR, capture_output=True, text=True, log=False,
).stdout.splitlines():
src = ANDROID_DIR / line
dst = temp_dir / line
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst, follow_symlinks=False)
# Include anything from the prefix directory which could be useful
# either for embedding Python in an app, or building third-party
# packages against it.
for rel_dir, patterns in [
("include", ["openssl*", "python*", "sqlite*"]),
("lib", ["engines-3", "libcrypto*.so", "libpython*", "libsqlite*",
"libssl*.so", "ossl-modules", "python*"]),
("lib/pkgconfig", ["*crypto*", "*ssl*", "*python*", "*sqlite*"]),
]:
for pattern in patterns:
for src in glob(f"{prefix_dir}/{rel_dir}/{pattern}"):
dst = temp_dir / relpath(src, prefix_dir.parent)
dst.parent.mkdir(parents=True, exist_ok=True)
if Path(src).is_dir():
shutil.copytree(
src, dst, symlinks=True,
ignore=lambda *args: ["__pycache__"]
)
else:
shutil.copy2(src, dst, follow_symlinks=False)
# Strip debug information.
if not context.debug:
so_files = glob(f"{temp_dir}/**/*.so", recursive=True)
run([android_env(context.host)["STRIP"], *so_files], log=False)
dist_dir = subdir(context.host, "dist", create=True)
package_path = shutil.make_archive(
f"{dist_dir}/python-{version}-{context.host}", "gztar", temp_dir
)
print(f"Wrote {package_path}")
return package_path
def ci(context):
for step in [
configure_build_python,
make_build_python,
configure_host_python,
make_host_python,
package,
]:
caption = (
step.__name__.replace("_", " ")
.capitalize()
.replace("python", "Python")
)
print(f"::group::{caption}")
result = step(context)
if step is package:
package_path = result
print("::endgroup::")
if (
"GITHUB_ACTIONS" in os.environ
and (platform.system(), platform.machine()) != ("Linux", "x86_64")
):
print(
"Skipping tests: GitHub Actions does not support the Android "
"emulator on this platform."
)
else:
with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir:
print("::group::Tests")
# Prove the package is self-contained by using it to run the tests.
shutil.unpack_archive(package_path, temp_dir)
launcher_args = [
"--managed", "maxVersion", "-v", f"--{context.ci_mode}-ci"
]
run(
["./android.py", "test", *launcher_args],
cwd=temp_dir
)
print("::endgroup::")
def env(context):
print_env(android_env(getattr(context, "host", None)))
# Handle SIGTERM the same way as SIGINT. This ensures that if we're terminated
# by the buildbot worker, we'll make an attempt to clean up our subprocesses.
def install_signal_handler():
def signal_handler(*args):
os.kill(os.getpid(), signal.SIGINT)
signal.signal(signal.SIGTERM, signal_handler)
def parse_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subcommands = parser.add_subparsers(dest="subcommand") subcommands = parser.add_subparsers(dest="subcommand", required=True)
build = subcommands.add_parser("build", help="Build everything")
configure_build = subcommands.add_parser("configure-build", def add_parser(*args, **kwargs):
help="Run `configure` for the " parser = subcommands.add_parser(*args, **kwargs)
"build Python") parser.add_argument(
make_build = subcommands.add_parser("make-build", "-v", "--verbose", action="count", default=0,
help="Run `make` for the build Python") help="Show verbose output. Use twice to be even more verbose.")
configure_host = subcommands.add_parser("configure-host", return parser
help="Run `configure` for Android")
make_host = subcommands.add_parser("make-host", # Subcommands
help="Run `make` for Android") build = add_parser(
clean = subcommands.add_parser("clean", help="Delete files and directories " "build", help="Run configure-build, make-build, configure-host and "
"created by this script") "make-host")
for subcommand in build, configure_build, configure_host: configure_build = add_parser(
"configure-build", help="Run `configure` for the build Python")
add_parser(
"make-build", help="Run `make` for the build Python")
configure_host = add_parser(
"configure-host", help="Run `configure` for Android")
make_host = add_parser(
"make-host", help="Run `make` for Android")
add_parser("clean", help="Delete all build directories")
add_parser("build-testbed", help="Build the testbed app")
test = add_parser("test", help="Run the testbed app")
package = add_parser("package", help="Make a release package")
ci = add_parser("ci", help="Run build, package and test")
env = add_parser("env", help="Print environment variables")
# Common arguments
for subcommand in [build, configure_build, configure_host, ci]:
subcommand.add_argument( subcommand.add_argument(
"--clean", action="store_true", default=False, dest="clean", "--clean", action="store_true", default=False, dest="clean",
help="Delete any relevant directories before building") help="Delete the relevant build directories first")
for subcommand in build, configure_host, make_host:
host_commands = [build, configure_host, make_host, package, ci]
if in_source_tree:
host_commands.append(env)
for subcommand in host_commands:
subcommand.add_argument( subcommand.add_argument(
"host", metavar="HOST", "host", metavar="HOST", choices=HOSTS,
choices=["aarch64-linux-android", "x86_64-linux-android"],
help="Host triplet: choices=[%(choices)s]") help="Host triplet: choices=[%(choices)s]")
for subcommand in build, configure_build, configure_host:
for subcommand in [build, configure_build, configure_host, ci]:
subcommand.add_argument("args", nargs="*", subcommand.add_argument("args", nargs="*",
help="Extra arguments to pass to `configure`") help="Extra arguments to pass to `configure`")
context = parser.parse_args() # Test arguments
dispatch = {"configure-build": configure_build_python, device_group = test.add_mutually_exclusive_group(required=True)
"make-build": make_build_python, device_group.add_argument(
"configure-host": configure_host_python, "--connected", metavar="SERIAL", help="Run on a connected device. "
"make-host": make_host_python, "Connect it yourself, then get its serial from `adb devices`.")
"build": build_all, device_group.add_argument(
"clean": clean_all} "--managed", metavar="NAME", help="Run on a Gradle-managed device. "
dispatch[context.subcommand](context) "These are defined in `managedDevices` in testbed/app/build.gradle.kts.")
test.add_argument(
"--site-packages", metavar="DIR", type=abspath,
help="Directory to copy as the app's site-packages.")
test.add_argument(
"--cwd", metavar="DIR", type=abspath,
help="Directory to copy as the app's working directory.")
test.add_argument(
"args", nargs="*", help=f"Python command-line arguments. "
f"Separate them from {SCRIPT_NAME}'s own arguments with `--`. "
f"If neither -c nor -m are included, `-m test` will be prepended, "
f"which will run Python's own test suite.")
# Package arguments.
for subcommand in [package, ci]:
subcommand.add_argument(
"-g", action="store_true", default=False, dest="debug",
help="Include debug information in package")
# CI arguments
for subcommand in [test, ci]:
group = subcommand.add_mutually_exclusive_group(required=subcommand is ci)
group.add_argument(
"--fast-ci", action="store_const", dest="ci_mode", const="fast",
help="Add test arguments for GitHub Actions")
group.add_argument(
"--slow-ci", action="store_const", dest="ci_mode", const="slow",
help="Add test arguments for buildbots")
return parser.parse_args()
def main():
install_signal_handler()
# Under the buildbot, stdout is not a TTY, but we must still flush after
# every line to make sure our output appears in the correct order relative
# to the output of our subprocesses.
for stream in [sys.stdout, sys.stderr]:
stream.reconfigure(line_buffering=True)
context = parse_args()
dispatch = {
"configure-build": configure_build_python,
"make-build": make_build_python,
"configure-host": configure_host_python,
"make-host": make_host_python,
"build": build_all,
"clean": clean_all,
"build-testbed": build_testbed,
"test": run_testbed,
"package": package,
"ci": ci,
"env": env,
}
try:
result = dispatch[context.subcommand](context)
if asyncio.iscoroutine(result):
asyncio.run(result)
except CalledProcessError as e:
print_called_process_error(e)
sys.exit(1)
def print_called_process_error(e):
for stream_name in ["stdout", "stderr"]:
content = getattr(e, stream_name)
if isinstance(content, bytes):
content = content.decode(*DECODE_ARGS)
stream = getattr(sys, stream_name)
if content:
stream.write(content)
if not content.endswith("\n"):
stream.write("\n")
# shlex uses single quotes, so we surround the command with double quotes.
print(
f'Command "{join_command(e.cmd)}" returned exit status {e.returncode}'
)
if __name__ == "__main__": if __name__ == "__main__":

22
Android/testbed/.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# The Gradle wrapper can be downloaded by running the `test` or `build-testbed`
# commands of android.py.
/gradlew
/gradlew.bat
/gradle/wrapper/gradle-wrapper.jar
# The repository's top-level .gitignore file ignores all .idea directories, but
# we want to keep any files which can't be regenerated from the Gradle
# configuration.
!.idea/
/.idea/*
!/.idea/inspectionProfiles
*.iml
.gradle
/local.properties
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

View file

@ -0,0 +1,8 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintGradleDependency" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
<inspection_tool class="AndroidLintOldTargetApi" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
<inspection_tool class="UnstableApiUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
</profile>
</component>

1
Android/testbed/app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,269 @@
import com.android.build.api.variant.*
import kotlin.math.max
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
val ANDROID_DIR = file("../..")
val PYTHON_DIR = ANDROID_DIR.parentFile!!
val PYTHON_CROSS_DIR = file("$PYTHON_DIR/cross-build")
val inSourceTree = (
ANDROID_DIR.name == "Android" && file("$PYTHON_DIR/pyconfig.h.in").exists()
)
val KNOWN_ABIS = mapOf(
"aarch64-linux-android" to "arm64-v8a",
"x86_64-linux-android" to "x86_64",
)
// Discover prefixes.
val prefixes = ArrayList<File>()
if (inSourceTree) {
for ((triplet, _) in KNOWN_ABIS.entries) {
val prefix = file("$PYTHON_CROSS_DIR/$triplet/prefix")
if (prefix.exists()) {
prefixes.add(prefix)
}
}
} else {
// Testbed is inside a release package.
val prefix = file("$ANDROID_DIR/prefix")
if (prefix.exists()) {
prefixes.add(prefix)
}
}
if (prefixes.isEmpty()) {
throw GradleException(
"No Android prefixes found: see README.md for testing instructions"
)
}
// Detect Python versions and ABIs.
lateinit var pythonVersion: String
var abis = HashMap<File, String>()
for ((i, prefix) in prefixes.withIndex()) {
val libDir = file("$prefix/lib")
val version = run {
for (filename in libDir.list()!!) {
"""python(\d+\.\d+[a-z]*)""".toRegex().matchEntire(filename)?.let {
return@run it.groupValues[1]
}
}
throw GradleException("Failed to find Python version in $libDir")
}
if (i == 0) {
pythonVersion = version
} else if (pythonVersion != version) {
throw GradleException(
"${prefixes[0]} is Python $pythonVersion, but $prefix is Python $version"
)
}
val libPythonDir = file("$libDir/python$pythonVersion")
val triplet = run {
for (filename in libPythonDir.list()!!) {
"""_sysconfigdata_[a-z]*_android_(.+).py""".toRegex()
.matchEntire(filename)?.let {
return@run it.groupValues[1]
}
}
throw GradleException("Failed to find Python triplet in $libPythonDir")
}
abis[prefix] = KNOWN_ABIS[triplet]!!
}
android {
val androidEnvFile = file("../../android-env.sh").absoluteFile
namespace = "org.python.testbed"
compileSdk = 34
defaultConfig {
applicationId = "org.python.testbed"
minSdk = androidEnvFile.useLines {
for (line in it) {
"""ANDROID_API_LEVEL:=(\d+)""".toRegex().find(line)?.let {
return@useLines it.groupValues[1].toInt()
}
}
throw GradleException("Failed to find API level in $androidEnvFile")
}
targetSdk = 34
versionCode = 1
versionName = "1.0"
ndk.abiFilters.addAll(abis.values)
externalNativeBuild.cmake.arguments(
"-DPYTHON_PREFIX_DIR=" + if (inSourceTree) {
// AGP uses the ${} syntax for its own purposes, so use a Jinja style
// placeholder.
"$PYTHON_CROSS_DIR/{{triplet}}/prefix"
} else {
prefixes[0]
},
"-DPYTHON_VERSION=$pythonVersion",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
ndkVersion = androidEnvFile.useLines {
for (line in it) {
"""ndk_version=(\S+)""".toRegex().find(line)?.let {
return@useLines it.groupValues[1]
}
}
throw GradleException("Failed to find NDK version in $androidEnvFile")
}
externalNativeBuild.cmake {
path("src/main/c/CMakeLists.txt")
}
// Set this property to something non-empty, otherwise it'll use the default
// list, which ignores asset directories beginning with an underscore.
aaptOptions.ignoreAssetsPattern = ".git"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
testOptions {
managedDevices {
localDevices {
create("minVersion") {
device = "Small Phone"
// Managed devices have a minimum API level of 27.
apiLevel = max(27, defaultConfig.minSdk!!)
// ATD devices are smaller and faster, but have a minimum
// API level of 30.
systemImageSource = if (apiLevel >= 30) "aosp-atd" else "aosp"
}
create("maxVersion") {
device = "Small Phone"
apiLevel = defaultConfig.targetSdk!!
systemImageSource = "aosp-atd"
}
}
// If the previous test run succeeded and nothing has changed,
// Gradle thinks there's no need to run it again. Override that.
afterEvaluate {
(localDevices.names + listOf("connected")).forEach {
tasks.named("${it}DebugAndroidTest") {
outputs.upToDateWhen { false }
}
}
}
}
}
}
dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test:rules:1.5.0")
}
// Create some custom tasks to copy Python and its standard library from
// elsewhere in the repository.
androidComponents.onVariants { variant ->
val pyPlusVer = "python$pythonVersion"
generateTask(variant, variant.sources.assets!!) {
into("python") {
// Include files such as pyconfig.h are used by some of the tests.
into("include/$pyPlusVer") {
for (prefix in prefixes) {
from("$prefix/include/$pyPlusVer")
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
into("lib/$pyPlusVer") {
// To aid debugging, the source directory takes priority when
// running inside a CPython source tree.
if (inSourceTree) {
from("$PYTHON_DIR/Lib")
}
for (prefix in prefixes) {
from("$prefix/lib/$pyPlusVer")
}
into("site-packages") {
from("$projectDir/src/main/python")
val sitePackages = findProperty("python.sitePackages") as String?
if (!sitePackages.isNullOrEmpty()) {
if (!file(sitePackages).exists()) {
throw GradleException("$sitePackages does not exist")
}
from(sitePackages)
}
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
exclude("**/__pycache__")
}
into("cwd") {
val cwd = findProperty("python.cwd") as String?
if (!cwd.isNullOrEmpty()) {
if (!file(cwd).exists()) {
throw GradleException("$cwd does not exist")
}
from(cwd)
}
}
}
}
generateTask(variant, variant.sources.jniLibs!!) {
for ((prefix, abi) in abis.entries) {
into(abi) {
from("$prefix/lib")
include("libpython*.*.so")
include("lib*_python.so")
}
}
}
}
fun generateTask(
variant: ApplicationVariant, directories: SourceDirectories,
configure: GenerateTask.() -> Unit
) {
val taskName = "generate" +
listOf(variant.name, "Python", directories.name)
.map { it.replaceFirstChar(Char::uppercase) }
.joinToString("")
directories.addGeneratedSourceDirectory(
tasks.register<GenerateTask>(taskName) {
into(outputDir)
configure()
},
GenerateTask::outputDir)
}
// addGeneratedSourceDirectory requires the task to have a DirectoryProperty.
abstract class GenerateTask: Sync() {
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
}

View file

@ -0,0 +1,35 @@
package org.python.testbed
import androidx.test.annotation.UiThreadTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
@RunWith(AndroidJUnit4::class)
class PythonSuite {
@Test
@UiThreadTest
fun testPython() {
val start = System.currentTimeMillis()
try {
val status = PythonTestRunner(
InstrumentationRegistry.getInstrumentation().targetContext
).run(
InstrumentationRegistry.getArguments().getString("pythonArgs")!!,
)
assertEquals(0, status)
} finally {
// Make sure the process lives long enough for the test script to
// detect it (see `find_pid` in android.py).
val delay = 2000 - (System.currentTimeMillis() - start)
if (delay > 0) {
Thread.sleep(delay)
}
}
}
}

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Material3.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.4.1)
project(testbed)
# Resolve variables from the command line.
string(
REPLACE {{triplet}} ${CMAKE_LIBRARY_ARCHITECTURE}
PYTHON_PREFIX_DIR ${PYTHON_PREFIX_DIR}
)
include_directories(${PYTHON_PREFIX_DIR}/include/python${PYTHON_VERSION})
link_directories(${PYTHON_PREFIX_DIR}/lib)
link_libraries(log python${PYTHON_VERSION})
add_library(main_activity SHARED main_activity.c)

View file

@ -0,0 +1,202 @@
#include <android/log.h>
#include <errno.h>
#include <jni.h>
#include <pthread.h>
#include <Python.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static void throw_runtime_exception(JNIEnv *env, const char *message) {
(*env)->ThrowNew(
env,
(*env)->FindClass(env, "java/lang/RuntimeException"),
message);
}
static void throw_errno(JNIEnv *env, const char *error_prefix) {
char error_message[1024];
snprintf(error_message, sizeof(error_message),
"%s: %s", error_prefix, strerror(errno));
throw_runtime_exception(env, error_message);
}
// --- Stdio redirection ------------------------------------------------------
// Most apps won't need this, because the Python-level sys.stdout and sys.stderr
// are redirected to the Android logcat by Python itself. However, in the
// testbed it's useful to redirect the native streams as well, to debug problems
// in the Python startup or redirection process.
//
// Based on
// https://github.com/beeware/briefcase-android-gradle-template/blob/v0.3.11/%7B%7B%20cookiecutter.safe_formal_name%20%7D%7D/app/src/main/cpp/native-lib.cpp
typedef struct {
FILE *file;
int fd;
android_LogPriority priority;
char *tag;
int pipe[2];
} StreamInfo;
// The FILE member can't be initialized here because stdout and stderr are not
// compile-time constants. Instead, it's initialized immediately before the
// redirection.
static StreamInfo STREAMS[] = {
{NULL, STDOUT_FILENO, ANDROID_LOG_INFO, "native.stdout", {-1, -1}},
{NULL, STDERR_FILENO, ANDROID_LOG_WARN, "native.stderr", {-1, -1}},
{NULL, -1, ANDROID_LOG_UNKNOWN, NULL, {-1, -1}},
};
// The maximum length of a log message in bytes, including the level marker and
// tag, is defined as LOGGER_ENTRY_MAX_PAYLOAD in
// platform/system/logging/liblog/include/log/log.h. As of API level 30, messages
// longer than this will be be truncated by logcat. This limit has already been
// reduced at least once in the history of Android (from 4076 to 4068 between API
// level 23 and 26), so leave some headroom.
static const int MAX_BYTES_PER_WRITE = 4000;
static void *redirection_thread(void *arg) {
StreamInfo *si = (StreamInfo*)arg;
ssize_t read_size;
char buf[MAX_BYTES_PER_WRITE];
while ((read_size = read(si->pipe[0], buf, sizeof buf - 1)) > 0) {
buf[read_size] = '\0'; /* add null-terminator */
__android_log_write(si->priority, si->tag, buf);
}
return 0;
}
static char *redirect_stream(StreamInfo *si) {
/* make the FILE unbuffered, to ensure messages are never lost */
if (setvbuf(si->file, 0, _IONBF, 0)) {
return "setvbuf";
}
/* create the pipe and redirect the file descriptor */
if (pipe(si->pipe)) {
return "pipe";
}
if (dup2(si->pipe[1], si->fd) == -1) {
return "dup2";
}
/* start the logging thread */
pthread_t thr;
if ((errno = pthread_create(&thr, 0, redirection_thread, si))) {
return "pthread_create";
}
if ((errno = pthread_detach(thr))) {
return "pthread_detach";
}
return 0;
}
JNIEXPORT void JNICALL Java_org_python_testbed_PythonTestRunner_redirectStdioToLogcat(
JNIEnv *env, jobject obj
) {
STREAMS[0].file = stdout;
STREAMS[1].file = stderr;
for (StreamInfo *si = STREAMS; si->file; si++) {
char *error_prefix;
if ((error_prefix = redirect_stream(si))) {
throw_errno(env, error_prefix);
return;
}
}
}
// --- Python initialization ---------------------------------------------------
static char *init_signals() {
// Some tests use SIGUSR1, but that's blocked by default in an Android app in
// order to make it available to `sigwait` in the Signal Catcher thread.
// (https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:art/runtime/signal_catcher.cc).
// That thread's functionality is only useful for debugging the JVM, so disabling
// it should not weaken the tests.
//
// There's no safe way of stopping the thread completely (#123982), but simply
// unblocking SIGUSR1 is enough to fix most tests.
//
// However, in tests that generate multiple different signals in quick
// succession, it's possible for SIGUSR1 to arrive while the main thread is busy
// running the C-level handler for a different signal. In that case, the SIGUSR1
// may be sent to the Signal Catcher thread instead, which will generate a log
// message containing the text "reacting to signal".
//
// Such tests may need to be changed in one of the following ways:
// * Use a signal other than SIGUSR1 (e.g. test_stress_delivery_simultaneous in
// test_signal.py).
// * Send the signal to a specific thread rather than the whole process (e.g.
// test_signals in test_threadsignals.py.
sigset_t set;
if (sigemptyset(&set)) {
return "sigemptyset";
}
if (sigaddset(&set, SIGUSR1)) {
return "sigaddset";
}
if ((errno = pthread_sigmask(SIG_UNBLOCK, &set, NULL))) {
return "pthread_sigmask";
}
return NULL;
}
static void throw_status(JNIEnv *env, PyStatus status) {
throw_runtime_exception(env, status.err_msg ? status.err_msg : "");
}
JNIEXPORT int JNICALL Java_org_python_testbed_PythonTestRunner_runPython(
JNIEnv *env, jobject obj, jstring home, jarray args
) {
const char *home_utf8 = (*env)->GetStringUTFChars(env, home, NULL);
char cwd[PATH_MAX];
snprintf(cwd, sizeof(cwd), "%s/%s", home_utf8, "cwd");
if (chdir(cwd)) {
throw_errno(env, "chdir");
return 1;
}
char *error_prefix;
if ((error_prefix = init_signals())) {
throw_errno(env, error_prefix);
return 1;
}
PyConfig config;
PyStatus status;
PyConfig_InitPythonConfig(&config);
jsize argc = (*env)->GetArrayLength(env, args);
const char *argv[argc + 1];
for (int i = 0; i < argc; i++) {
jobject arg = (*env)->GetObjectArrayElement(env, args, i);
argv[i] = (*env)->GetStringUTFChars(env, arg, NULL);
}
argv[argc] = NULL;
// PyConfig_SetBytesArgv "must be called before other methods, since the
// preinitialization configuration depends on command line arguments"
if (PyStatus_Exception(status = PyConfig_SetBytesArgv(&config, argc, (char**)argv))) {
throw_status(env, status);
return 1;
}
status = PyConfig_SetBytesString(&config, &config.home, home_utf8);
if (PyStatus_Exception(status)) {
throw_status(env, status);
return 1;
}
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
throw_status(env, status);
return 1;
}
return Py_RunMain();
}

View file

@ -0,0 +1,92 @@
package org.python.testbed
import android.content.Context
import android.os.*
import android.system.Os
import android.widget.TextView
import androidx.appcompat.app.*
import org.json.JSONArray
import java.io.*
// Launching the tests from an activity is OK for a quick check, but for
// anything more complicated it'll be more convenient to use `android.py test`
// to launch the tests via PythonSuite.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val status = PythonTestRunner(this).run("""["-m", "test", "-W", "-uall"]""")
findViewById<TextView>(R.id.tvHello).text = "Exit status $status"
}
}
class PythonTestRunner(val context: Context) {
/** Run Python.
*
* @param args Python command-line, encoded as JSON.
* @return The Python exit status: zero on success, nonzero on failure. */
fun run(args: String) : Int {
// We leave argument 0 as an empty string, which is a placeholder for the
// executable name in embedded mode.
val argsJsonArray = JSONArray(args)
val argsStringArray = Array<String>(argsJsonArray.length() + 1) { it -> ""}
for (i in 0..<argsJsonArray.length()) {
argsStringArray[i + 1] = argsJsonArray.getString(i)
}
// Python needs this variable to help it find the temporary directory,
// but Android only sets it on API level 33 and later.
Os.setenv("TMPDIR", context.cacheDir.toString(), false)
val pythonHome = extractAssets()
System.loadLibrary("main_activity")
redirectStdioToLogcat()
return runPython(pythonHome.toString(), argsStringArray)
}
private fun extractAssets() : File {
val pythonHome = File(context.filesDir, "python")
if (pythonHome.exists() && !pythonHome.deleteRecursively()) {
throw RuntimeException("Failed to delete $pythonHome")
}
extractAssetDir("python", context.filesDir)
// Empty directories are lost in the asset packing/unpacking process.
val cwd = File(pythonHome, "cwd")
if (!cwd.exists()) {
cwd.mkdir()
}
return pythonHome
}
private fun extractAssetDir(path: String, targetDir: File) {
val names = context.assets.list(path)
?: throw RuntimeException("Failed to list $path")
val targetSubdir = File(targetDir, path)
if (!targetSubdir.mkdirs()) {
throw RuntimeException("Failed to create $targetSubdir")
}
for (name in names) {
val subPath = "$path/$name"
val input: InputStream
try {
input = context.assets.open(subPath)
} catch (e: FileNotFoundException) {
extractAssetDir(subPath, targetDir)
continue
}
input.use {
File(targetSubdir, name).outputStream().use { output ->
input.copyTo(output)
}
}
}
}
private external fun redirectStdioToLogcat()
private external fun runPython(home: String, args: Array<String>) : Int
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvHello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Python testbed</string>
</resources>

View file

@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.10.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
}

View file

@ -0,0 +1,28 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
# By default, the app will be uninstalled after the tests finish (apparently
# after 10 seconds in case of an unclean shutdown). We disable this, because
# when using android.py it can conflict with the installation of the next run.
android.injected.androidTest.leaveApksInstalledAfterRun=true

View file

@ -0,0 +1,6 @@
#Mon Feb 19 20:29:06 GMT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Python testbed"
include(":app")

1015
Apple/__main__.py Normal file

File diff suppressed because it is too large Load diff

328
Apple/iOS/README.md Normal file
View file

@ -0,0 +1,328 @@
# Python on iOS README
**iOS support is [tier 3](https://peps.python.org/pep-0011/#tier-3).**
This document provides a quick overview of some iOS specific features in the
Python distribution.
These instructions are only needed if you're planning to compile Python for iOS
yourself. Most users should *not* need to do this. If you're looking to
experiment with writing an iOS app in Python, tools such as [BeeWare's
Briefcase](https://briefcase.readthedocs.io) and [Kivy's
Buildozer](https://buildozer.readthedocs.io) will provide a much more
approachable user experience.
## Compilers for building on iOS
Building for iOS requires the use of Apple's Xcode tooling. It is strongly
recommended that you use the most recent stable release of Xcode. This will
require the use of the most (or second-most) recently released macOS version,
as Apple does not maintain Xcode for older macOS versions. The Xcode Command
Line Tools are not sufficient for iOS development; you need a *full* Xcode
install.
If you want to run your code on the iOS simulator, you'll also need to install
an iOS Simulator Platform. You should be prompted to select an iOS Simulator
Platform when you first run Xcode. Alternatively, you can add an iOS Simulator
Platform by selecting an open the Platforms tab of the Xcode Settings panel.
## Building Python on iOS
### ABIs and Architectures
iOS apps can be deployed on physical devices, and on the iOS simulator. Although
the API used on these devices is identical, the ABI is different - you need to
link against different libraries for an iOS device build (`iphoneos`) or an
iOS simulator build (`iphonesimulator`).
Apple uses the `XCframework` format to allow specifying a single dependency
that supports multiple ABIs. An `XCframework` is a wrapper around multiple
ABI-specific frameworks that share a common API.
iOS can also support different CPU architectures within each ABI. At present,
there is only a single supported architecture on physical devices - ARM64.
However, the *simulator* supports 2 architectures - ARM64 (for running on Apple
Silicon machines), and x86_64 (for running on older Intel-based machines).
To support multiple CPU architectures on a single platform, Apple uses a "fat
binary" format - a single physical file that contains support for multiple
architectures. It is possible to compile and use a "thin" single architecture
version of a binary for testing purposes; however, the "thin" binary will not be
portable to machines using other architectures.
### Building a multi-architecture iOS XCframework
The `Apple` subfolder of the Python repository acts as a build script that
can be used to coordinate the compilation of a complete iOS XCframework. To use
it, run::
python Apple build iOS
This will:
* Configure and compile a version of Python to run on the build machine
* Download pre-compiled binary dependencies for each platform
* Configure and build a `Python.framework` for each required architecture and
iOS SDK
* Merge the multiple `Python.framework` folders into a single `Python.xcframework`
* Produce a `.tar.gz` archive in the `cross-build/dist` folder containing
the `Python.xcframework`, plus a copy of the Testbed app pre-configured to
use the XCframework.
The `Apple` build script has other entry points that will perform the
individual parts of the overall `build` target, plus targets to test the
build, clean the `cross-build` folder of iOS build products, and perform a
complete "build and test" CI run. The `--clean` flag can also be used on
individual commands to ensure that a stale build product are removed before
building.
### Building a single-architecture framework
If you're using the `Apple` build script, you won't need to build
individual frameworks. However, if you do need to manually configure an iOS
Python build for a single framework, the following options are available.
#### iOS specific arguments to configure
* `--enable-framework[=DIR]`
This argument specifies the location where the Python.framework will be
installed. If `DIR` is not specified, the framework will be installed into
a subdirectory of the `iOS/Frameworks` folder.
This argument *must* be provided when configuring iOS builds. iOS does not
support non-framework builds.
* `--with-framework-name=NAME`
Specify the name for the Python framework; defaults to `Python`.
> [!NOTE]
> Unless you know what you're doing, changing the name of the Python
> framework on iOS is not advised. If you use this option, you won't be able
> to run the `Apple` build script without making significant manual
> alterations, and you won't be able to use any binary packages unless you
> compile them yourself using your own framework name.
#### Building Python for iOS
The Python build system will create a `Python.framework` that supports a
*single* ABI with a *single* architecture. Unlike macOS, iOS does not allow a
framework to contain non-library content, so the iOS build will produce a
`bin` and `lib` folder in the same output folder as `Python.framework`.
The `lib` folder will be needed at runtime to support the Python library.
If you want to use Python in a real iOS project, you need to produce multiple
`Python.framework` builds, one for each ABI and architecture. iOS builds of
Python *must* be constructed as framework builds. To support this, you must
provide the `--enable-framework` flag when configuring the build. The build
also requires the use of cross-compilation. The minimal commands for building
Python for the ARM64 iOS simulator will look something like:
```
export PATH="$(pwd)/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
./configure \
--enable-framework \
--host=arm64-apple-ios-simulator \
--build=arm64-apple-darwin \
--with-build-python=/path/to/python.exe
make
make install
```
In this invocation:
* `Apple/iOS/Resources/bin` has been added to the path, providing some shims for the
compilers and linkers needed by the build. Xcode requires the use of `xcrun`
to invoke compiler tooling. However, if `xcrun` is pre-evaluated and the
result passed to `configure`, these results can embed user- and
version-specific paths into the sysconfig data, which limits the portability
of the compiled Python. Alternatively, if `xcrun` is used *as* the compiler,
it requires that compiler variables like `CC` include spaces, which can
cause significant problems with many C configuration systems which assume that
`CC` will be a single executable.
To work around this problem, the `Apple/iOS/Resources/bin` folder contains some
wrapper scripts that present as simple compilers and linkers, but wrap
underlying calls to `xcrun`. This allows configure to use a `CC`
definition without spaces, and without user- or version-specific paths, while
retaining the ability to adapt to the local Xcode install. These scripts are
included in the `bin` directory of an iOS install.
These scripts will, by default, use the currently active Xcode installation.
If you want to use a different Xcode installation, you can use
`xcode-select` to set a new default Xcode globally, or you can use the
`DEVELOPER_DIR` environment variable to specify an Xcode install. The
scripts will use the default `iphoneos`/`iphonesimulator` SDK version for
the select Xcode install; if you want to use a different SDK, you can set the
`IOS_SDK_VERSION` environment variable. (e.g, setting
`IOS_SDK_VERSION=17.1` would cause the scripts to use the `iphoneos17.1`
and `iphonesimulator17.1` SDKs, regardless of the Xcode default.)
The path has also been cleared of any user customizations. A common source of
bugs is for tools like Homebrew to accidentally leak macOS binaries into an iOS
build. Resetting the path to a known "bare bones" value is the easiest way to
avoid these problems.
* `--host` is the architecture and ABI that you want to build, in GNU compiler
triple format. This will be one of:
- `arm64-apple-ios` for ARM64 iOS devices.
- `arm64-apple-ios-simulator` for the iOS simulator running on Apple
Silicon devices.
- `x86_64-apple-ios-simulator` for the iOS simulator running on Intel
devices.
* `--build` is the GNU compiler triple for the machine that will be running
the compiler. This is one of:
- `arm64-apple-darwin` for Apple Silicon devices.
- `x86_64-apple-darwin` for Intel devices.
* `/path/to/python.exe` is the path to a Python binary on the machine that
will be running the compiler. This is needed because the Python compilation
process involves running some Python code. On a normal desktop build of
Python, you can compile a python interpreter and then use that interpreter to
run Python code. However, the binaries produced for iOS won't run on macOS, so
you need to provide an external Python interpreter. This interpreter must be
the same version as the Python that is being compiled. To be completely safe,
this should be the *exact* same commit hash. However, the longer a Python
release has been stable, the more likely it is that this constraint can be
relaxed - the same micro version will often be sufficient.
* The `install` target for iOS builds is slightly different to other
platforms. On most platforms, `make install` will install the build into
the final runtime location. This won't be the case for iOS, as the final
runtime location will be on a physical device.
However, you still need to run the `install` target for iOS builds, as it
performs some final framework assembly steps. The location specified with
`--enable-framework` will be the location where `make install` will
assemble the complete iOS framework. This completed framework can then
be copied and relocated as required.
For a full CPython build, you also need to specify the paths to iOS builds of
the binary libraries that CPython depends on (such as XZ, LibFFI and OpenSSL).
This can be done by defining library specific environment variables (such as
`LIBLZMA_CFLAGS`, `LIBLZMA_LIBS`), and the `--with-openssl` configure
option. Versions of these libraries pre-compiled for iOS can be found in [this
repository](https://github.com/beeware/cpython-apple-source-deps/releases).
LibFFI is especially important, as many parts of the standard library
(including the `platform`, `sysconfig` and `webbrowser` modules) require
the use of the `ctypes` module at runtime.
By default, Python will be compiled with an iOS deployment target (i.e., the
minimum supported iOS version) of 13.0. To specify a different deployment
target, provide the version number as part of the `--host` argument - for
example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64
simulator build with a deployment target of 15.4.
## Testing Python on iOS
### Testing a multi-architecture framework
Once you have a built an XCframework, you can test that framework by running:
$ python Apple test iOS
### Testing a single-architecture framework
The `Apple/testbed` folder that contains an Xcode project that is able to run
the Python test suite on Apple platforms. This project converts the Python test
suite into a single test case in Xcode's XCTest framework. The single XCTest
passes if the test suite passes.
To run the test suite, configure a Python build for an iOS simulator (i.e.,
`--host=arm64-apple-ios-simulator` or `--host=x86_64-apple-ios-simulator`
), specifying a framework build (i.e. `--enable-framework`). Ensure that your
`PATH` has been configured to include the `Apple/iOS/Resources/bin` folder and
exclude any non-iOS tools, then run:
```
make all
make install
make testios
```
This will:
* Build an iOS framework for your chosen architecture;
* Finalize the single-platform framework;
* Make a clean copy of the testbed project;
* Install the Python iOS framework into the copy of the testbed project; and
* Run the test suite on an "entry-level device" simulator (i.e., an iPhone SE,
iPhone 16e, or a similar).
On success, the test suite will exit and report successful completion of the
test suite. On a 2022 M1 MacBook Pro, the test suite takes approximately 15
minutes to run; a couple of extra minutes is required to compile the testbed
project, and then boot and prepare the iOS simulator.
### Debugging test failures
Running `python Apple test iOS` generates a standalone version of the
`Apple/testbed` project, and runs the full test suite. It does this using
`Apple/testbed` itself - the folder is an executable module that can be used
to create and run a clone of the testbed project. The standalone version of the
testbed will be created in a directory named
`cross-build/iOS-testbed.<timestamp>`.
You can generate your own standalone testbed instance by running:
```
python cross-build/iOS/testbed clone my-testbed
```
In this invocation, `my-testbed` is the name of the folder for the new
testbed clone.
If you've built your own XCframework, or you only want to test a single architecture,
you can construct a standalone testbed instance by running:
```
python Apple/testbed clone --platform iOS --framework <path/to/framework> my-testbed
```
The framework path can be the path path to a `Python.xcframework`, or the
path to a folder that contains a single-platform `Python.framework`.
You can then use the `my-testbed` folder to run the Python test suite,
passing in any command line arguments you may require. For example, if you're
trying to diagnose a failure in the `os` module, you might run:
```
python my-testbed run -- test -W test_os
```
This is the equivalent of running `python -m test -W test_os` on a desktop
Python build. Any arguments after the `--` will be passed to testbed as if
they were arguments to `python -m` on a desktop machine.
### Testing in Xcode
You can also open the testbed project in Xcode by running:
```
open my-testbed/iOSTestbed.xcodeproj
```
This will allow you to use the full Xcode suite of tools for debugging.
The arguments used to run the test suite are defined as part of the test plan.
To modify the test plan, select the test plan node of the project tree (it
should be the first child of the root node), and select the "Configurations"
tab. Modify the "Arguments Passed On Launch" value to change the testing
arguments.
The test plan also disables parallel testing, and specifies the use of the
`Testbed.lldbinit` file for providing configuration of the debugger. The
default debugger configuration disables automatic breakpoints on the
`SIGINT`, `SIGUSR1`, `SIGUSR2`, and `SIGXFSZ` signals.
### Testing on an iOS device
To test on an iOS device, the app needs to be signed with known developer
credentials. To obtain these credentials, you must have an iOS Developer
account, and your Xcode install will need to be logged into your account (see
the Accounts tab of the Preferences dialog).
Once the project is open, and you're signed into your Apple Developer account,
select the root node of the project tree (labeled "iOSTestbed"), then the
"Signing & Capabilities" tab in the details page. Select a development team
(this will likely be your own name), and plug in a physical device to your
macOS machine with a USB cable. You should then be able to select your physical
device from the list of targets in the pulldown in the Xcode titlebar.

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@"

View file

@ -0,0 +1,2 @@
#!/bin/sh
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@"

View file

@ -19,7 +19,7 @@
<string>iPhoneOS</string> <string>iPhoneOS</string>
</array> </array>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>12.0</string> <string>13.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
</dict> </dict>

View file

@ -0,0 +1,150 @@
# Utility methods for use in an Xcode project.
#
# An iOS XCframework cannot include any content other than the library binary
# and relevant metadata. However, Python requires a standard library at runtime.
# Therefore, it is necessary to add a build step to an Xcode app target that
# processes the standard library and puts the content into the final app.
#
# In general, these tools will be invoked after bundle resources have been
# copied into the app, but before framework embedding (and signing).
#
# The following is an example script, assuming that:
# * Python.xcframework is in the root of the project
# * There is an `app` folder that contains the app code
# * There is an `app_packages` folder that contains installed Python packages.
# -----
# set -e
# source $PROJECT_DIR/Python.xcframework/build/build_utils.sh
# install_python Python.xcframework app app_packages
# -----
# Copy the standard library from the XCframework into the app bundle.
#
# Accepts one argument:
# 1. The path, relative to the root of the Xcode project, where the Python
# XCframework can be found.
install_stdlib() {
PYTHON_XCFRAMEWORK_PATH=$1
mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib"
if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then
echo "Installing Python modules for iOS Simulator"
if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/ios-arm64-simulator" ]; then
SLICE_FOLDER="ios-arm64-simulator"
else
SLICE_FOLDER="ios-arm64_x86_64-simulator"
fi
else
echo "Installing Python modules for iOS Device"
SLICE_FOLDER="ios-arm64"
fi
# If the XCframework has a shared lib folder, then it's a full framework.
# Copy both the common and slice-specific part of the lib directory.
# Otherwise, it's a single-arch framework; use the "full" lib folder.
if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib" ]; then
rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/"
rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib-$ARCHS/" "$CODESIGNING_FOLDER_PATH/python/lib/"
else
rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/"
fi
}
# Convert a single .so library into a framework that iOS can load.
#
# Accepts three arguments:
# 1. The path, relative to the root of the Xcode project, where the Python
# XCframework can be found.
# 2. The base path, relative to the installed location in the app bundle, that
# needs to be processed. Any .so file found in this path (or a subdirectory
# of it) will be processed.
# 2. The full path to a single .so file to process. This path should include
# the base path.
install_dylib () {
PYTHON_XCFRAMEWORK_PATH=$1
INSTALL_BASE=$2
FULL_EXT=$3
# The name of the extension file
EXT=$(basename "$FULL_EXT")
# The name and location of the module
MODULE_PATH=$(dirname "$FULL_EXT")
MODULE_NAME=$(echo $EXT | cut -d "." -f 1)
# The location of the extension file, relative to the bundle
RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/}
# The path to the extension file, relative to the install base
PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}
# The full dotted name of the extension module, constructed from the file path.
FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" ".");
# A bundle identifier; not actually used, but required by Xcode framework packaging
FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-")
# The name of the framework folder.
FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework"
# If the framework folder doesn't exist, create it.
if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then
echo "Creating framework for $RELATIVE_EXT"
mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER"
cp "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/build/$PLATFORM_FAMILY_NAME-dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
fi
echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
# Create a placeholder .fwork file where the .so was
echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork
# Create a back reference to the .so file location in the framework
echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin"
# If the framework provides an xcprivacy file, install it.
if [ -e "$MODULE_PATH/$MODULE_NAME.xcprivacy" ]; then
echo "Installing XCPrivacy file for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
XCPRIVACY_FILE="$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/PrivacyInfo.xcprivacy"
if [ -e "$XCPRIVACY_FILE" ]; then
rm -rf "$XCPRIVACY_FILE"
fi
mv "$MODULE_PATH/$MODULE_NAME.xcprivacy" "$XCPRIVACY_FILE"
fi
echo "Signing framework as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..."
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER"
}
# Process all the dynamic libraries in a path into Framework format.
#
# Accepts two arguments:
# 1. The path, relative to the root of the Xcode project, where the Python
# XCframework can be found.
# 2. The base path, relative to the installed location in the app bundle, that
# needs to be processed. Any .so file found in this path (or a subdirectory
# of it) will be processed.
process_dylibs () {
PYTHON_XCFRAMEWORK_PATH=$1
LIB_PATH=$2
find "$CODESIGNING_FOLDER_PATH/$LIB_PATH" -name "*.so" | while read FULL_EXT; do
install_dylib $PYTHON_XCFRAMEWORK_PATH "$LIB_PATH/" "$FULL_EXT"
done
}
# The entry point for post-processing a Python XCframework.
#
# Accepts 1 or more arguments:
# 1. The path, relative to the root of the Xcode project, where the Python
# XCframework can be found. If the XCframework is in the root of the project,
# 2+. The path of a package, relative to the root of the packaged app, that contains
# library content that should be processed for binary libraries.
install_python() {
PYTHON_XCFRAMEWORK_PATH=$1
shift
install_stdlib $PYTHON_XCFRAMEWORK_PATH
PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib")
echo "Install Python $PYTHON_VER standard library extension modules..."
process_dylibs $PYTHON_XCFRAMEWORK_PATH python/lib/$PYTHON_VER/lib-dynload
for package_path in $@; do
echo "Installing $package_path extension modules ..."
process_dylibs $PYTHON_XCFRAMEWORK_PATH $package_path
done
}

View file

@ -0,0 +1,4 @@
process handle SIGINT -n true -p true -s false
process handle SIGUSR1 -n true -p true -s false
process handle SIGUSR2 -n true -p true -s false
process handle SIGXFSZ -n true -p true -s false

View file

@ -0,0 +1,197 @@
#import <XCTest/XCTest.h>
#import <Python/Python.h>
@interface TestbedTests : XCTestCase
@end
@implementation TestbedTests
- (void)testPython {
const char **argv;
int exit_code;
int failed;
PyStatus status;
PyPreConfig preconfig;
PyConfig config;
PyObject *app_packages_path;
PyObject *method_args;
PyObject *result;
PyObject *site_module;
PyObject *site_addsitedir_attr;
PyObject *sys_module;
PyObject *sys_path_attr;
NSArray *test_args;
NSString *python_home;
NSString *path;
wchar_t *wtmp_str;
NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
// Set some other common environment indicators to disable color, as the
// Xcode log can't display color. Stdout will report that it is *not* a
// TTY.
setenv("NO_COLOR", "1", true);
setenv("PYTHON_COLORS", "0", true);
// Arguments to pass into the test suite runner.
// argv[0] must identify the process; any subsequent arg
// will be handled as if it were an argument to `python -m test`
// The processInfo arguments contain the binary that is running,
// followed by the arguments defined in the test plan. This means:
// run_module = test_args[1]
// argv = ["Testbed"] + test_args[2:]
test_args = [[NSProcessInfo processInfo] arguments];
if (test_args == NULL) {
NSLog(@"Unable to identify test arguments.");
}
NSLog(@"Test arguments: %@", test_args);
argv = malloc(sizeof(char *) * ([test_args count] - 1));
argv[0] = "Testbed";
for (int i = 1; i < [test_args count] - 1; i++) {
argv[i] = [[test_args objectAtIndex:i+1] UTF8String];
}
// Generate an isolated Python configuration.
NSLog(@"Configuring isolated Python...");
PyPreConfig_InitIsolatedConfig(&preconfig);
PyConfig_InitIsolatedConfig(&config);
// Configure the Python interpreter:
// Enforce UTF-8 encoding for stderr, stdout, file-system encoding and locale.
// See https://docs.python.org/3/library/os.html#python-utf-8-mode.
preconfig.utf8_mode = 1;
// Use the system logger for stdout/err
config.use_system_logger = 1;
// Don't buffer stdio. We want output to appears in the log immediately
config.buffered_stdio = 0;
// Don't write bytecode; we can't modify the app bundle
// after it has been signed.
config.write_bytecode = 0;
// Ensure that signal handlers are installed
config.install_signal_handlers = 1;
// Run the test module.
config.run_module = Py_DecodeLocale([[test_args objectAtIndex:1] UTF8String], NULL);
// For debugging - enable verbose mode.
// config.verbose = 1;
NSLog(@"Pre-initializing Python runtime...");
status = Py_PreInitialize(&preconfig);
if (PyStatus_Exception(status)) {
XCTFail(@"Unable to pre-initialize Python interpreter: %s", status.err_msg);
PyConfig_Clear(&config);
return;
}
// Set the home for the Python interpreter
python_home = [NSString stringWithFormat:@"%@/python", resourcePath, nil];
NSLog(@"PythonHome: %@", python_home);
wtmp_str = Py_DecodeLocale([python_home UTF8String], NULL);
status = PyConfig_SetString(&config, &config.home, wtmp_str);
if (PyStatus_Exception(status)) {
XCTFail(@"Unable to set PYTHONHOME: %s", status.err_msg);
PyConfig_Clear(&config);
return;
}
PyMem_RawFree(wtmp_str);
// Read the site config
status = PyConfig_Read(&config);
if (PyStatus_Exception(status)) {
XCTFail(@"Unable to read site config: %s", status.err_msg);
PyConfig_Clear(&config);
return;
}
NSLog(@"Configure argc/argv...");
status = PyConfig_SetBytesArgv(&config, [test_args count] - 1, (char**) argv);
if (PyStatus_Exception(status)) {
XCTFail(@"Unable to configure argc/argv: %s", status.err_msg);
PyConfig_Clear(&config);
return;
}
NSLog(@"Initializing Python runtime...");
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
XCTFail(@"Unable to initialize Python interpreter: %s", status.err_msg);
PyConfig_Clear(&config);
return;
}
// Add app_packages as a site directory. This both adds to sys.path,
// and ensures that any .pth files in that directory will be executed.
site_module = PyImport_ImportModule("site");
if (site_module == NULL) {
XCTFail(@"Could not import site module");
return;
}
site_addsitedir_attr = PyObject_GetAttrString(site_module, "addsitedir");
if (site_addsitedir_attr == NULL || !PyCallable_Check(site_addsitedir_attr)) {
XCTFail(@"Could not access site.addsitedir");
return;
}
path = [NSString stringWithFormat:@"%@/app_packages", resourcePath, nil];
NSLog(@"App packages path: %@", path);
wtmp_str = Py_DecodeLocale([path UTF8String], NULL);
app_packages_path = PyUnicode_FromWideChar(wtmp_str, wcslen(wtmp_str));
if (app_packages_path == NULL) {
XCTFail(@"Could not convert app_packages path to unicode");
return;
}
PyMem_RawFree(wtmp_str);
method_args = Py_BuildValue("(O)", app_packages_path);
if (method_args == NULL) {
XCTFail(@"Could not create arguments for site.addsitedir");
return;
}
result = PyObject_CallObject(site_addsitedir_attr, method_args);
if (result == NULL) {
XCTFail(@"Could not add app_packages directory using site.addsitedir");
return;
}
// Add test code to sys.path
sys_module = PyImport_ImportModule("sys");
if (sys_module == NULL) {
XCTFail(@"Could not import sys module");
return;
}
sys_path_attr = PyObject_GetAttrString(sys_module, "path");
if (sys_path_attr == NULL) {
XCTFail(@"Could not access sys.path");
return;
}
path = [NSString stringWithFormat:@"%@/app", resourcePath, nil];
NSLog(@"App path: %@", path);
wtmp_str = Py_DecodeLocale([path UTF8String], NULL);
failed = PyList_Insert(sys_path_attr, 0, PyUnicode_FromString([path UTF8String]));
if (failed) {
XCTFail(@"Unable to add app to sys.path");
return;
}
PyMem_RawFree(wtmp_str);
// Ensure the working directory is the app folder.
chdir([path UTF8String]);
// Start the test suite. Print a separator to differentiate Python startup logs from app logs
NSLog(@"---------------------------------------------------------------------------");
exit_code = Py_RunMain();
XCTAssertEqual(exit_code, 0, @"Test suite did not pass");
NSLog(@"---------------------------------------------------------------------------");
Py_Finalize();
}
@end

415
Apple/testbed/__main__.py Normal file
View file

@ -0,0 +1,415 @@
import argparse
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path
TEST_SLICES = {
"iOS": "ios-arm64_x86_64-simulator",
}
DECODE_ARGS = ("UTF-8", "backslashreplace")
# The system log prefixes each line:
# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ...
# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ...
LOG_PREFIX_REGEX = re.compile(
r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD
r"\s+\d+:\d{2}:\d{2}\.\d+\+\d{4}" # HH:MM:SS.ssssss+ZZZZ
r"\s+iOSTestbed\[\d+:\w+\]" # Process/thread ID
)
# Select a simulator device to use.
def select_simulator_device(platform):
# List the testing simulators, in JSON format
raw_json = subprocess.check_output(["xcrun", "simctl", "list", "-j"])
json_data = json.loads(raw_json)
if platform == "iOS":
# Any iOS device will do; we'll look for "SE" devices - but the name isn't
# consistent over time. Older Xcode versions will use "iPhone SE (Nth
# generation)"; As of 2025, they've started using "iPhone 16e".
#
# When Xcode is updated after a new release, new devices will be available
# and old ones will be dropped from the set available on the latest iOS
# version. Select the one with the highest minimum runtime version - this
# is an indicator of the "newest" released device, which should always be
# supported on the "most recent" iOS version.
se_simulators = sorted(
(devicetype["minRuntimeVersion"], devicetype["name"])
for devicetype in json_data["devicetypes"]
if devicetype["productFamily"] == "iPhone"
and (
(
"iPhone " in devicetype["name"]
and devicetype["name"].endswith("e")
)
or "iPhone SE " in devicetype["name"]
)
)
simulator = se_simulators[-1][1]
else:
raise ValueError(f"Unknown platform {platform}")
return simulator
def xcode_test(location: Path, platform: str, simulator: str, verbose: bool):
# Build and run the test suite on the named simulator.
args = [
"-project",
str(location / f"{platform}Testbed.xcodeproj"),
"-scheme",
f"{platform}Testbed",
"-destination",
f"platform={platform} Simulator,name={simulator}",
"-derivedDataPath",
str(location / "DerivedData"),
]
verbosity_args = [] if verbose else ["-quiet"]
print("Building test project...")
subprocess.run(
["xcodebuild", "build-for-testing"] + args + verbosity_args,
check=True,
)
print("Running test project...")
# Test execution *can't* be run -quiet; verbose mode
# is how we see the output of the test output.
process = subprocess.Popen(
["xcodebuild", "test-without-building"] + args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
while line := (process.stdout.readline()).decode(*DECODE_ARGS):
# Strip the timestamp/process prefix from each log line
line = LOG_PREFIX_REGEX.sub("", line)
sys.stdout.write(line)
sys.stdout.flush()
status = process.wait(timeout=5)
exit(status)
def copy(src, tgt):
"""An all-purpose copy.
If src is a file, it is copied. If src is a symlink, it is copied *as a
symlink*. If src is a directory, the full tree is duplicated, with symlinks
being preserved.
"""
if src.is_file() or src.is_symlink():
shutil.copyfile(src, tgt, follow_symlinks=False)
else:
shutil.copytree(src, tgt, symlinks=True)
def clone_testbed(
source: Path,
target: Path,
framework: Path,
platform: str,
apps: list[Path],
) -> None:
if target.exists():
print(f"{target} already exists; aborting without creating project.")
sys.exit(10)
if framework is None:
if not (
source / "Python.xcframework" / TEST_SLICES[platform] / "bin"
).is_dir():
print(
f"The testbed being cloned ({source}) does not contain "
"a framework with slices. Re-run with --framework"
)
sys.exit(11)
else:
if not framework.is_dir():
print(f"{framework} does not exist.")
sys.exit(12)
elif not (
framework.suffix == ".xcframework"
or (framework / "Python.framework").is_dir()
):
print(
f"{framework} is not an XCframework, "
f"or a simulator slice of a framework build."
)
sys.exit(13)
print("Cloning testbed project:")
print(f" Cloning {source}...", end="")
# Only copy the files for the platform being cloned plus the files common
# to all platforms. The XCframework will be copied later, if needed.
target.mkdir(parents=True)
for name in [
"__main__.py",
"TestbedTests",
"Testbed.lldbinit",
f"{platform}Testbed",
f"{platform}Testbed.xcodeproj",
f"{platform}Testbed.xctestplan",
]:
copy(source / name, target / name)
print(" done")
orig_xc_framework_path = source / "Python.xcframework"
xc_framework_path = target / "Python.xcframework"
test_framework_path = xc_framework_path / TEST_SLICES[platform]
if framework is not None:
if framework.suffix == ".xcframework":
print(" Installing XCFramework...", end="")
xc_framework_path.symlink_to(
framework.relative_to(xc_framework_path.parent, walk_up=True)
)
print(" done")
else:
print(" Installing simulator framework...", end="")
# We're only installing a slice of a framework; we need
# to do a full tree copy to make sure we don't damage
# symlinked content.
shutil.copytree(orig_xc_framework_path, xc_framework_path)
if test_framework_path.is_dir():
shutil.rmtree(test_framework_path)
else:
test_framework_path.unlink(missing_ok=True)
test_framework_path.symlink_to(
framework.relative_to(test_framework_path.parent, walk_up=True)
)
print(" done")
else:
copy(orig_xc_framework_path, xc_framework_path)
if (
xc_framework_path.is_symlink()
and not xc_framework_path.readlink().is_absolute()
):
# XCFramework is a relative symlink. Rewrite the symlink relative
# to the new location.
print(" Rewriting symlink to XCframework...", end="")
resolved_xc_framework_path = (
source / xc_framework_path.readlink()
).resolve()
xc_framework_path.unlink()
xc_framework_path.symlink_to(
resolved_xc_framework_path.relative_to(
xc_framework_path.parent, walk_up=True
)
)
print(" done")
elif (
test_framework_path.is_symlink()
and not test_framework_path.readlink().is_absolute()
):
print(" Rewriting symlink to simulator framework...", end="")
# Simulator framework is a relative symlink. Rewrite the symlink
# relative to the new location.
orig_test_framework_path = (
source / "Python.XCframework" / test_framework_path.readlink()
).resolve()
test_framework_path.unlink()
test_framework_path.symlink_to(
orig_test_framework_path.relative_to(
test_framework_path.parent, walk_up=True
)
)
print(" done")
else:
print(" Using pre-existing Python framework.")
for app_src in apps:
print(f" Installing app {app_src.name!r}...", end="")
app_target = target / f"Testbed/app/{app_src.name}"
if app_target.is_dir():
shutil.rmtree(app_target)
shutil.copytree(app_src, app_target)
print(" done")
print(f"Successfully cloned testbed: {target.resolve()}")
def update_test_plan(testbed_path, platform, args):
# Modify the test plan to use the requested test arguments.
test_plan_path = testbed_path / f"{platform}Testbed.xctestplan"
with test_plan_path.open("r", encoding="utf-8") as f:
test_plan = json.load(f)
test_plan["defaultOptions"]["commandLineArgumentEntries"] = [
{"argument": arg} for arg in args
]
with test_plan_path.open("w", encoding="utf-8") as f:
json.dump(test_plan, f, indent=2)
def run_testbed(
platform: str,
simulator: str | None,
args: list[str],
verbose: bool = False,
):
location = Path(__file__).parent
print("Updating test plan...", end="")
update_test_plan(location, platform, args)
print(" done.")
if simulator is None:
simulator = select_simulator_device(platform)
print(f"Running test on {simulator}")
xcode_test(
location,
platform=platform,
simulator=simulator,
verbose=verbose,
)
def main():
# Look for directories like `iOSTestbed` as an indicator of the platforms
# that the testbed folder supports. The original source testbed can support
# many platforms, but when cloned, only one platform is preserved.
available_platforms = [
platform
for platform in ["iOS"]
if (Path(__file__).parent / f"{platform}Testbed").is_dir()
]
parser = argparse.ArgumentParser(
description=(
"Manages the process of testing an Apple Python project through Xcode."
),
)
subcommands = parser.add_subparsers(dest="subcommand")
clone = subcommands.add_parser(
"clone",
description=(
"Clone the testbed project, copying in a Python framework and"
"any specified application code."
),
help="Clone a testbed project to a new location.",
)
clone.add_argument(
"--framework",
help=(
"The location of the XCFramework (or simulator-only slice of an "
"XCFramework) to use when running the testbed"
),
)
clone.add_argument(
"--platform",
dest="platform",
choices=available_platforms,
default=available_platforms[0],
help=f"The platform to target (default: {available_platforms[0]})",
)
clone.add_argument(
"--app",
dest="apps",
action="append",
default=[],
help="The location of any code to include in the testbed project",
)
clone.add_argument(
"location",
help="The path where the testbed will be cloned.",
)
run = subcommands.add_parser(
"run",
usage="%(prog)s [-h] [--simulator SIMULATOR] -- <test arg> [<test arg> ...]",
description=(
"Run a testbed project. The arguments provided after `--` will be "
"passed to the running iOS process as if they were arguments to "
"`python -m`."
),
help="Run a testbed project",
)
run.add_argument(
"--platform",
dest="platform",
choices=available_platforms,
default=available_platforms[0],
help=f"The platform to target (default: {available_platforms[0]})",
)
run.add_argument(
"--simulator",
help=(
"The name of the simulator to use (eg: 'iPhone 16e'). Defaults to "
"the most recently released 'entry level' iPhone device. Device "
"architecture and OS version can also be specified; e.g., "
"`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on "
"an ARM64 iPhone 16 Pro simulator running iOS 26.0."
),
)
run.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
)
try:
pos = sys.argv.index("--")
testbed_args = sys.argv[1:pos]
test_args = sys.argv[pos + 1 :]
except ValueError:
testbed_args = sys.argv[1:]
test_args = []
context = parser.parse_args(testbed_args)
if context.subcommand == "clone":
clone_testbed(
source=Path(__file__).parent.resolve(),
target=Path(context.location).resolve(),
framework=Path(context.framework).resolve()
if context.framework
else None,
platform=context.platform,
apps=[Path(app) for app in context.apps],
)
elif context.subcommand == "run":
if test_args:
if not (
Path(__file__).parent
/ "Python.xcframework"
/ TEST_SLICES[context.platform]
/ "bin"
).is_dir():
print(
f"Testbed does not contain a compiled Python framework. Use "
f"`python {sys.argv[0]} clone ...` to create a runnable "
f"clone of this testbed."
)
sys.exit(20)
run_testbed(
platform=context.platform,
simulator=context.simulator,
verbose=context.verbose,
args=test_args,
)
else:
print(
f"Must specify test arguments (e.g., {sys.argv[0]} run -- test)"
)
print()
parser.print_help(sys.stderr)
sys.exit(21)
else:
parser.print_help(sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -11,12 +11,13 @@
607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607A66212B0EFA390010BFC8 /* Assets.xcassets */; }; 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607A66212B0EFA390010BFC8 /* Assets.xcassets */; };
607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */; }; 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */; };
607A66282B0EFA390010BFC8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66272B0EFA390010BFC8 /* main.m */; }; 607A66282B0EFA390010BFC8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66272B0EFA390010BFC8 /* main.m */; };
607A66322B0EFA3A0010BFC8 /* iOSTestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.m */; }; 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */; };
607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; };
607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; };
607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
607A66582B0F079F0010BFC8 /* dylib-Info-template.plist in Resources */ = {isa = PBXBuildFile; fileRef = 607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */; }; 608619542CB77BA900F46182 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 608619532CB77BA900F46182 /* app_packages */; };
608619562CB7819B00F46182 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 608619552CB7819B00F46182 /* app */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -62,10 +63,12 @@
607A66242B0EFA390010BFC8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 607A66242B0EFA390010BFC8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
607A66272B0EFA390010BFC8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 607A66272B0EFA390010BFC8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iOSTestbedTests.m; sourceTree = "<group>"; }; 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestbedTests.m; sourceTree = "<group>"; };
607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = "<group>"; }; 607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = "<group>"; };
607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "dylib-Info-template.plist"; sourceTree = "<group>"; };
607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = "<group>"; }; 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = "<group>"; };
608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = "<group>"; };
608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = "<group>"; };
60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -91,9 +94,10 @@
607A66092B0EFA380010BFC8 = { 607A66092B0EFA380010BFC8 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */,
607A664A2B0EFB310010BFC8 /* Python.xcframework */, 607A664A2B0EFB310010BFC8 /* Python.xcframework */,
607A66142B0EFA380010BFC8 /* iOSTestbed */, 607A66142B0EFA380010BFC8 /* iOSTestbed */,
607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */, 607A66302B0EFA3A0010BFC8 /* TestbedTests */,
607A66132B0EFA380010BFC8 /* Products */, 607A66132B0EFA380010BFC8 /* Products */,
607A664F2B0EFFE00010BFC8 /* Frameworks */, 607A664F2B0EFFE00010BFC8 /* Frameworks */,
); );
@ -111,8 +115,9 @@
607A66142B0EFA380010BFC8 /* iOSTestbed */ = { 607A66142B0EFA380010BFC8 /* iOSTestbed */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
608619552CB7819B00F46182 /* app */,
608619532CB77BA900F46182 /* app_packages */,
607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */, 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */,
607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */,
607A66152B0EFA380010BFC8 /* AppDelegate.h */, 607A66152B0EFA380010BFC8 /* AppDelegate.h */,
607A66162B0EFA380010BFC8 /* AppDelegate.m */, 607A66162B0EFA380010BFC8 /* AppDelegate.m */,
607A66212B0EFA390010BFC8 /* Assets.xcassets */, 607A66212B0EFA390010BFC8 /* Assets.xcassets */,
@ -122,12 +127,12 @@
path = iOSTestbed; path = iOSTestbed;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */ = { 607A66302B0EFA3A0010BFC8 /* TestbedTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.m */, 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */,
); );
path = iOSTestbedTests; path = TestbedTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
607A664F2B0EFFE00010BFC8 /* Frameworks */ = { 607A664F2B0EFFE00010BFC8 /* Frameworks */ = {
@ -147,8 +152,7 @@
607A660E2B0EFA380010BFC8 /* Sources */, 607A660E2B0EFA380010BFC8 /* Sources */,
607A660F2B0EFA380010BFC8 /* Frameworks */, 607A660F2B0EFA380010BFC8 /* Frameworks */,
607A66102B0EFA380010BFC8 /* Resources */, 607A66102B0EFA380010BFC8 /* Resources */,
607A66552B0F061D0010BFC8 /* Install Target Specific Python Standard Library */, 607A66552B0F061D0010BFC8 /* Process Python libraries */,
607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */,
607A664E2B0EFC080010BFC8 /* Embed Frameworks */, 607A664E2B0EFC080010BFC8 /* Embed Frameworks */,
); );
buildRules = ( buildRules = (
@ -222,8 +226,9 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */, 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */,
607A66582B0F079F0010BFC8 /* dylib-Info-template.plist in Resources */, 608619562CB7819B00F46182 /* app in Resources */,
607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */, 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */,
608619542CB77BA900F46182 /* app_packages in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -237,7 +242,7 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
607A66552B0F061D0010BFC8 /* Install Target Specific Python Standard Library */ = { 607A66552B0F061D0010BFC8 /* Process Python libraries */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -247,33 +252,15 @@
); );
inputPaths = ( inputPaths = (
); );
name = "Install Target Specific Python Standard Library"; name = "Process Python libraries";
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "set -e\n\nmkdir -p \"$CODESIGNING_FOLDER_PATH/python/lib\"\nif [ \"$EFFECTIVE_PLATFORM_NAME\" = \"-iphonesimulator\" ]; then\n echo \"Installing Python modules for iOS Simulator\"\n rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nelse\n echo \"Installing Python modules for iOS Device\"\n rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nfi\n"; shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n";
}; showEnvVarsInLog = 0;
607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Prepare Python Binary Modules";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "set -e\n\ninstall_dylib () {\n INSTALL_BASE=$1\n FULL_EXT=$2\n\n # The name of the extension file\n EXT=$(basename \"$FULL_EXT\")\n # The location of the extension file, relative to the bundle\n RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} \n # The path to the extension file, relative to the install base\n PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}\n # The full dotted name of the extension module, constructed from the file path.\n FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d \".\" -f 1 | tr \"/\" \".\"); \n # A bundle identifier; not actually used, but required by Xcode framework packaging\n FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr \"_\" \"-\")\n # The name of the framework folder.\n FRAMEWORK_FOLDER=\"Frameworks/$FULL_MODULE_NAME.framework\"\n\n # If the framework folder doesn't exist, create it.\n if [ ! -d \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\" ]; then\n echo \"Creating framework for $RELATIVE_EXT\" \n mkdir -p \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\"\n cp \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n plutil -replace CFBundleExecutable -string \"$FULL_MODULE_NAME\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n plutil -replace CFBundleIdentifier -string \"$FRAMEWORK_BUNDLE_ID\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n fi\n \n echo \"Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" \n mv \"$FULL_EXT\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\"\n # Create a placeholder .fwork file where the .so was\n echo \"$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" > ${FULL_EXT%.so}.fwork\n # Create a back reference to the .so file location in the framework\n echo \"${RELATIVE_EXT%.so}.fwork\" > \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin\" \n}\n\nPYTHON_VER=$(ls -1 \"$CODESIGNING_FOLDER_PATH/python/lib\")\necho \"Install Python $PYTHON_VER standard library extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib python/lib/$PYTHON_VER/lib-dynload/ \"$FULL_EXT\"\ndone\n\n# Clean up dylib template \nrm -f \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\"\necho \"Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)...\"\nfind \"$CODESIGNING_FOLDER_PATH/Frameworks\" -name \"*.framework\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der \"{}\" \\; \n";
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
@ -291,7 +278,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
607A66322B0EFA3A0010BFC8 /* iOSTestbedTests.m in Sources */, 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -369,7 +356,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
@ -424,7 +411,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
@ -450,7 +437,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -481,7 +468,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -504,7 +491,7 @@
DEVELOPMENT_TEAM = 3HEZE76D99; DEVELOPMENT_TEAM = 3HEZE76D99;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -524,7 +511,7 @@
DEVELOPMENT_TEAM = 3HEZE76D99; DEVELOPMENT_TEAM = 3HEZE76D99;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607A66112B0EFA380010BFC8"
BuildableName = "iOSTestbed.app"
BlueprintName = "iOSTestbed"
ReferencedContainer = "container:iOSTestbed.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SOURCE_ROOT)/Testbed.lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:iOSTestbed.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607A662C2B0EFA3A0010BFC8"
BuildableName = "iOSTestbedTests.xctest"
BlueprintName = "iOSTestbedTests"
ReferencedContainer = "container:iOSTestbed.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607A66112B0EFA380010BFC8"
BuildableName = "iOSTestbed.app"
BlueprintName = "iOSTestbed"
ReferencedContainer = "container:iOSTestbed.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607A66112B0EFA380010BFC8"
BuildableName = "iOSTestbed.app"
BlueprintName = "iOSTestbed"
ReferencedContainer = "container:iOSTestbed.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,46 @@
{
"configurations" : [
{
"id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5",
"name" : "Test Scheme Action",
"options" : {
}
}
],
"defaultOptions" : {
"commandLineArgumentEntries" : [
{
"argument" : "test"
},
{
"argument" : "-uall"
},
{
"argument" : "--single-process"
},
{
"argument" : "--rerun"
},
{
"argument" : "-W"
}
],
"targetForVariableExpansion" : {
"containerPath" : "container:iOSTestbed.xcodeproj",
"identifier" : "607A66112B0EFA380010BFC8",
"name" : "iOSTestbed"
}
},
"testTargets" : [
{
"parallelizable" : false,
"target" : {
"containerPath" : "container:iOSTestbed.xcodeproj",
"identifier" : "607A662C2B0EFA3A0010BFC8",
"name" : "iOSTestbedTests"
}
}
],
"version" : 1
}

View file

@ -0,0 +1,7 @@
This folder can contain any Python application code.
During the build, any binary modules found in this folder will be processed into
Framework form.
When the test suite runs, this folder will be on the PYTHONPATH, and will be the
working directory for the test suite.

View file

@ -0,0 +1,7 @@
This folder can be a target for installing any Python dependencies needed by the
test suite.
During the build, any binary modules found in this folder will be processed into
Framework form.
When the test suite runs, this folder will be on the PYTHONPATH.

Some files were not shown because too many files have changed in this diff Show more