Skip to content

Commit 9c28284

Browse files
freakboy3742hugovkmhsmithned-deily
committed
[3.12] pythongh-114099: Additions to standard library to support iOS (pythonGH-117052)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: Malcolm Smith <smith@chaquo.com> Co-authored-by: Ned Deily <nad@python.org>
1 parent 0af7e82 commit 9c28284

25 files changed

Lines changed: 489 additions & 53 deletions

Doc/library/os.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,11 @@ process and user.
785785
:func:`socket.gethostname` or even
786786
``socket.gethostbyaddr(socket.gethostname())``.
787787

788+
On macOS, iOS and Android, this returns the *kernel* name and version (i.e.,
789+
``'Darwin'`` on macOS and iOS; ``'Linux'`` on Android). :func:`platform.uname()`
790+
can be used to get the user-facing operating system name and version on iOS and
791+
Android.
792+
788793
.. availability:: Unix.
789794

790795
.. versionchanged:: 3.3

Doc/library/platform.rst

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ Cross Platform
148148
Returns the system/OS name, such as ``'Linux'``, ``'Darwin'``, ``'Java'``,
149149
``'Windows'``. An empty string is returned if the value cannot be determined.
150150

151+
On iOS and Android, this returns the user-facing OS name (i.e, ``'iOS``,
152+
``'iPadOS'`` or ``'Android'``). To obtain the kernel name (``'Darwin'`` or
153+
``'Linux'``), use :func:`os.uname()`.
151154

152155
.. function:: system_alias(system, release, version)
153156

@@ -161,6 +164,8 @@ Cross Platform
161164
Returns the system's release version, e.g. ``'#3 on degas'``. An empty string is
162165
returned if the value cannot be determined.
163166

167+
On iOS and Android, this is the user-facing OS version. To obtain the
168+
Darwin or Linux kernel version, use :func:`os.uname()`.
164169

165170
.. function:: uname()
166171

@@ -234,7 +239,6 @@ Windows Platform
234239
macOS Platform
235240
--------------
236241

237-
238242
.. function:: mac_ver(release='', versioninfo=('','',''), machine='')
239243

240244
Get macOS version information and return it as tuple ``(release, versioninfo,
@@ -244,6 +248,24 @@ macOS Platform
244248
Entries which cannot be determined are set to ``''``. All tuple entries are
245249
strings.
246250

251+
iOS Platform
252+
------------
253+
254+
.. function:: ios_ver(system='', release='', model='', is_simulator=False)
255+
256+
Get iOS version information and return it as a
257+
:func:`~collections.namedtuple` with the following attributes:
258+
259+
* ``system`` is the OS name; either ``'iOS'`` or ``'iPadOS'``.
260+
* ``release`` is the iOS version number as a string (e.g., ``'17.2'``).
261+
* ``model`` is the device model identifier; this will be a string like
262+
``'iPhone13,2'`` for a physical device, or ``'iPhone'`` on a simulator.
263+
* ``is_simulator`` is a boolean describing if the app is running on a
264+
simulator or a physical device.
265+
266+
Entries which cannot be determined are set to the defaults given as
267+
parameters.
268+
247269

248270
Unix Platforms
249271
--------------

Doc/library/webbrowser.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ allow the remote browser to maintain its own windows on the display. If remote
3333
browsers are not available on Unix, the controlling process will launch a new
3434
browser and wait.
3535

36+
On iOS, the :envvar:`BROWSER` environment variable, as well as any arguments
37+
controlling autoraise, browser preference, and new tab/window creation will be
38+
ignored. Web pages will *always* be opened in the user's preferred browser, in
39+
a new tab, with the browser being brought to the foreground. The use of the
40+
:mod:`webbrowser` module on iOS requires the :mod:`ctypes` module. If
41+
:mod:`ctypes` isn't available, calls to :func:`.open` will fail.
42+
3643
.. program:: webbrowser
3744

3845
The script :program:`webbrowser` can be used as a command-line interface for the
@@ -166,6 +173,8 @@ for the controller classes, all defined in this module.
166173
+------------------------+-----------------------------------------+-------+
167174
| ``'chromium-browser'`` | :class:`Chromium('chromium-browser')` | |
168175
+------------------------+-----------------------------------------+-------+
176+
| ``'iosbrowser'`` | ``IOSBrowser`` | \(4) |
177+
+------------------------+-----------------------------------------+-------+
169178

170179
Notes:
171180

@@ -180,7 +189,11 @@ Notes:
180189
Only on Windows platforms.
181190

182191
(3)
183-
Only on macOS platform.
192+
Only on macOS.
193+
194+
(4)
195+
Only on iOS.
196+
184197

185198
.. versionadded:: 3.3
186199
Support for Chrome/Chromium has been added.
@@ -193,6 +206,9 @@ Notes:
193206
.. deprecated-removed:: 3.11 3.13
194207
:class:`MacOSX` is deprecated, use :class:`MacOSXOSAScript` instead.
195208

209+
.. versionchanged:: 3.13
210+
Support for iOS has been added.
211+
196212
Here are some simple examples::
197213

198214
url = 'https://docs.python.org/'

Lib/_ios_support.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import sys
2+
try:
3+
from ctypes import cdll, c_void_p, c_char_p, util
4+
except ImportError:
5+
# ctypes is an optional module. If it's not present, we're limited in what
6+
# we can tell about the system, but we don't want to prevent the module
7+
# from working.
8+
print("ctypes isn't available; iOS system calls will not be available")
9+
objc = None
10+
else:
11+
# ctypes is available. Load the ObjC library, and wrap the objc_getClass,
12+
# sel_registerName methods
13+
lib = util.find_library("objc")
14+
if lib is None:
15+
# Failed to load the objc library
16+
raise RuntimeError("ObjC runtime library couldn't be loaded")
17+
18+
objc = cdll.LoadLibrary(lib)
19+
objc.objc_getClass.restype = c_void_p
20+
objc.objc_getClass.argtypes = [c_char_p]
21+
objc.sel_registerName.restype = c_void_p
22+
objc.sel_registerName.argtypes = [c_char_p]
23+
24+
25+
def get_platform_ios():
26+
# Determine if this is a simulator using the multiarch value
27+
is_simulator = sys.implementation._multiarch.endswith("simulator")
28+
29+
# We can't use ctypes; abort
30+
if not objc:
31+
return None
32+
33+
# Most of the methods return ObjC objects
34+
objc.objc_msgSend.restype = c_void_p
35+
# All the methods used have no arguments.
36+
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
37+
38+
# Equivalent of:
39+
# device = [UIDevice currentDevice]
40+
UIDevice = objc.objc_getClass(b"UIDevice")
41+
SEL_currentDevice = objc.sel_registerName(b"currentDevice")
42+
device = objc.objc_msgSend(UIDevice, SEL_currentDevice)
43+
44+
# Equivalent of:
45+
# device_systemVersion = [device systemVersion]
46+
SEL_systemVersion = objc.sel_registerName(b"systemVersion")
47+
device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion)
48+
49+
# Equivalent of:
50+
# device_systemName = [device systemName]
51+
SEL_systemName = objc.sel_registerName(b"systemName")
52+
device_systemName = objc.objc_msgSend(device, SEL_systemName)
53+
54+
# Equivalent of:
55+
# device_model = [device model]
56+
SEL_model = objc.sel_registerName(b"model")
57+
device_model = objc.objc_msgSend(device, SEL_model)
58+
59+
# UTF8String returns a const char*;
60+
SEL_UTF8String = objc.sel_registerName(b"UTF8String")
61+
objc.objc_msgSend.restype = c_char_p
62+
63+
# Equivalent of:
64+
# system = [device_systemName UTF8String]
65+
# release = [device_systemVersion UTF8String]
66+
# model = [device_model UTF8String]
67+
system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode()
68+
release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
69+
model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()
70+
71+
return system, release, model, is_simulator

Lib/platform.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,30 @@ def mac_ver(release='', versioninfo=('', '', ''), machine=''):
498498
# If that also doesn't work return the default values
499499
return release, versioninfo, machine
500500

501+
502+
# A namedtuple for iOS version information.
503+
IOSVersionInfo = collections.namedtuple(
504+
"IOSVersionInfo",
505+
["system", "release", "model", "is_simulator"]
506+
)
507+
508+
509+
def ios_ver(system="", release="", model="", is_simulator=False):
510+
"""Get iOS version information, and return it as a namedtuple:
511+
(system, release, model, is_simulator).
512+
513+
If values can't be determined, they are set to values provided as
514+
parameters.
515+
"""
516+
if sys.platform == "ios":
517+
import _ios_support
518+
result = _ios_support.get_platform_ios()
519+
if result is not None:
520+
return IOSVersionInfo(*result)
521+
522+
return IOSVersionInfo(system, release, model, is_simulator)
523+
524+
501525
def _java_getprop(name, default):
502526

503527
from java.lang import System
@@ -613,7 +637,7 @@ def _platform(*args):
613637
if cleaned == platform:
614638
break
615639
platform = cleaned
616-
while platform[-1] == '-':
640+
while platform and platform[-1] == '-':
617641
platform = platform[:-1]
618642

619643
return platform
@@ -654,7 +678,7 @@ def _syscmd_file(target, default=''):
654678
default in case the command should fail.
655679
656680
"""
657-
if sys.platform in ('dos', 'win32', 'win16'):
681+
if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}:
658682
# XXX Others too ?
659683
return default
660684

@@ -816,6 +840,14 @@ def get_OpenVMS():
816840
csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0)
817841
return 'Alpha' if cpu_number >= 128 else 'VAX'
818842

843+
# On the iOS simulator, os.uname returns the architecture as uname.machine.
844+
# On device it returns the model name for some reason; but there's only one
845+
# CPU architecture for iOS devices, so we know the right answer.
846+
def get_ios():
847+
if sys.implementation._multiarch.endswith("simulator"):
848+
return os.uname().machine
849+
return 'arm64'
850+
819851
def from_subprocess():
820852
"""
821853
Fall back to `uname -p`
@@ -970,6 +1002,10 @@ def uname():
9701002
system = 'Windows'
9711003
release = 'Vista'
9721004

1005+
# Normalize responses on iOS
1006+
if sys.platform == 'ios':
1007+
system, release, _, _ = ios_ver()
1008+
9731009
vals = system, node, release, version, machine
9741010
# Replace 'unknown' values with the more portable ''
9751011
_uname_cache = uname_result(*map(_unknown_as_blank, vals))
@@ -1249,11 +1285,14 @@ def platform(aliased=False, terse=False):
12491285
system, release, version = system_alias(system, release, version)
12501286

12511287
if system == 'Darwin':
1252-
# macOS (darwin kernel)
1253-
macos_release = mac_ver()[0]
1254-
if macos_release:
1255-
system = 'macOS'
1256-
release = macos_release
1288+
# macOS and iOS both report as a "Darwin" kernel
1289+
if sys.platform == "ios":
1290+
system, release, _, _ = ios_ver()
1291+
else:
1292+
macos_release = mac_ver()[0]
1293+
if macos_release:
1294+
system = 'macOS'
1295+
release = macos_release
12571296

12581297
if system == 'Windows':
12591298
# MS platforms

Lib/site.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ def _getuserbase():
287287
if env_base:
288288
return env_base
289289

290-
# Emscripten, VxWorks, and WASI have no home directories
291-
if sys.platform in {"emscripten", "vxworks", "wasi"}:
290+
# Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
291+
if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
292292
return None
293293

294294
def joinuser(*args):

Lib/sysconfig.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
# Keys for get_config_var() that are never converted to Python integers.
2323
_ALWAYS_STR = {
24+
'IPHONEOS_DEPLOYMENT_TARGET',
2425
'MACOSX_DEPLOYMENT_TARGET',
2526
}
2627

@@ -57,6 +58,7 @@
5758
'scripts': '{base}/Scripts',
5859
'data': '{base}',
5960
},
61+
6062
# Downstream distributors can overwrite the default install scheme.
6163
# This is done to support downstream modifications where distributors change
6264
# the installation layout (eg. different site-packages directory).
@@ -112,8 +114,8 @@ def _getuserbase():
112114
if env_base:
113115
return env_base
114116

115-
# Emscripten, VxWorks, and WASI have no home directories
116-
if sys.platform in {"emscripten", "vxworks", "wasi"}:
117+
# Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
118+
if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
117119
return None
118120

119121
def joinuser(*args):
@@ -299,6 +301,7 @@ def _get_preferred_schemes():
299301
'home': 'posix_home',
300302
'user': 'osx_framework_user',
301303
}
304+
302305
return {
303306
'prefix': 'posix_prefix',
304307
'home': 'posix_home',
@@ -831,10 +834,15 @@ def get_platform():
831834
if m:
832835
release = m.group()
833836
elif osname[:6] == "darwin":
834-
import _osx_support
835-
osname, release, machine = _osx_support.get_platform_osx(
836-
get_config_vars(),
837-
osname, release, machine)
837+
if sys.platform == "ios":
838+
release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "12.0")
839+
osname = sys.platform
840+
machine = sys.implementation._multiarch
841+
else:
842+
import _osx_support
843+
osname, release, machine = _osx_support.get_platform_osx(
844+
get_config_vars(),
845+
osname, release, machine)
838846

839847
return f"{osname}-{release}-{machine}"
840848

Lib/test/pythoninfo.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def format_groups(groups):
287287
"HOMEDRIVE",
288288
"HOMEPATH",
289289
"IDLESTARTUP",
290+
"IPHONEOS_DEPLOYMENT_TARGET",
290291
"LANG",
291292
"LDFLAGS",
292293
"LDSHARED",

Lib/test/test_concurrent_futures/test_thread_pool.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def test_idle_thread_reuse(self):
4949
self.assertEqual(len(executor._threads), 1)
5050
executor.shutdown(wait=True)
5151

52+
@support.requires_fork()
5253
@unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork')
5354
@support.requires_resource('cpu')
5455
def test_hang_global_shutdown_lock(self):

Lib/test/test_gc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,7 @@ def test_collect_garbage(self):
11881188
self.assertEqual(len(gc.garbage), 0)
11891189

11901190

1191+
@requires_subprocess()
11911192
@unittest.skipIf(BUILD_WITH_NDEBUG,
11921193
'built with -NDEBUG')
11931194
def test_refcount_errors(self):

0 commit comments

Comments
 (0)