Skip to content

Commit 06b3628

Browse files
freakboy3742hugovkmhsmithned-deily
authored andcommitted
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 6e44dd8 commit 06b3628

22 files changed

+474
-48
lines changed

Doc/library/os.rst

+5
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,11 @@ process and user.
784784
:func:`socket.gethostname` or even
785785
``socket.gethostbyaddr(socket.gethostname())``.
786786

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

789794
.. versionchanged:: 3.3

Doc/library/platform.rst

+23-1
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

@@ -238,7 +243,6 @@ Windows Platform
238243
macOS Platform
239244
--------------
240245

241-
242246
.. function:: mac_ver(release='', versioninfo=('','',''), machine='')
243247

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

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

252274
Unix Platforms
253275
--------------

Doc/library/webbrowser.rst

+16-1
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
The script :program:`webbrowser` can be used as a command-line interface for the
3744
module. It accepts a URL as the argument. It accepts the following optional
3845
parameters: ``-n`` opens the URL in a new browser window, if possible;
@@ -147,6 +154,8 @@ for the controller classes, all defined in this module.
147154
+------------------------+-----------------------------------------+-------+
148155
| ``'chromium-browser'`` | ``Chromium('chromium-browser')`` | |
149156
+------------------------+-----------------------------------------+-------+
157+
| ``'iosbrowser'`` | ``IOSBrowser`` | \(4) |
158+
+------------------------+-----------------------------------------+-------+
150159

151160
Notes:
152161

@@ -161,7 +170,10 @@ Notes:
161170
Only on Windows platforms.
162171

163172
(3)
164-
Only on macOS platform.
173+
Only on macOS.
174+
175+
(4)
176+
Only on iOS.
165177

166178
.. versionadded:: 3.2
167179
A new :class:`!MacOSXOSAScript` class has been added
@@ -176,6 +188,9 @@ Notes:
176188
Removed browsers include Grail, Mosaic, Netscape, Galeon,
177189
Skipstone, Iceape, and Firefox versions 35 and below.
178190

191+
.. versionchanged:: 3.13
192+
Support for iOS has been added.
193+
179194
Here are some simple examples::
180195

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

Lib/_ios_support.py

+71
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

+46-7
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,30 @@ def mac_ver(release='', versioninfo=('', '', ''), machine=''):
496496
# If that also doesn't work return the default values
497497
return release, versioninfo, machine
498498

499+
500+
# A namedtuple for iOS version information.
501+
IOSVersionInfo = collections.namedtuple(
502+
"IOSVersionInfo",
503+
["system", "release", "model", "is_simulator"]
504+
)
505+
506+
507+
def ios_ver(system="", release="", model="", is_simulator=False):
508+
"""Get iOS version information, and return it as a namedtuple:
509+
(system, release, model, is_simulator).
510+
511+
If values can't be determined, they are set to values provided as
512+
parameters.
513+
"""
514+
if sys.platform == "ios":
515+
import _ios_support
516+
result = _ios_support.get_platform_ios()
517+
if result is not None:
518+
return IOSVersionInfo(*result)
519+
520+
return IOSVersionInfo(system, release, model, is_simulator)
521+
522+
499523
def _java_getprop(name, default):
500524
"""This private helper is deprecated in 3.13 and will be removed in 3.15"""
501525
from java.lang import System
@@ -654,7 +678,7 @@ def _platform(*args):
654678
if cleaned == platform:
655679
break
656680
platform = cleaned
657-
while platform[-1] == '-':
681+
while platform and platform[-1] == '-':
658682
platform = platform[:-1]
659683

660684
return platform
@@ -695,7 +719,7 @@ def _syscmd_file(target, default=''):
695719
default in case the command should fail.
696720
697721
"""
698-
if sys.platform in ('dos', 'win32', 'win16'):
722+
if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}:
699723
# XXX Others too ?
700724
return default
701725

@@ -859,6 +883,14 @@ def get_OpenVMS():
859883
csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0)
860884
return 'Alpha' if cpu_number >= 128 else 'VAX'
861885

886+
# On the iOS simulator, os.uname returns the architecture as uname.machine.
887+
# On device it returns the model name for some reason; but there's only one
888+
# CPU architecture for iOS devices, so we know the right answer.
889+
def get_ios():
890+
if sys.implementation._multiarch.endswith("simulator"):
891+
return os.uname().machine
892+
return 'arm64'
893+
862894
def from_subprocess():
863895
"""
864896
Fall back to `uname -p`
@@ -1018,6 +1050,10 @@ def uname():
10181050
system = 'Android'
10191051
release = android_ver().release
10201052

1053+
# Normalize responses on iOS
1054+
if sys.platform == 'ios':
1055+
system, release, _, _ = ios_ver()
1056+
10211057
vals = system, node, release, version, machine
10221058
# Replace 'unknown' values with the more portable ''
10231059
_uname_cache = uname_result(*map(_unknown_as_blank, vals))
@@ -1297,11 +1333,14 @@ def platform(aliased=False, terse=False):
12971333
system, release, version = system_alias(system, release, version)
12981334

12991335
if system == 'Darwin':
1300-
# macOS (darwin kernel)
1301-
macos_release = mac_ver()[0]
1302-
if macos_release:
1303-
system = 'macOS'
1304-
release = macos_release
1336+
# macOS and iOS both report as a "Darwin" kernel
1337+
if sys.platform == "ios":
1338+
system, release, _, _ = ios_ver()
1339+
else:
1340+
macos_release = mac_ver()[0]
1341+
if macos_release:
1342+
system = 'macOS'
1343+
release = macos_release
13051344

13061345
if system == 'Windows':
13071346
# MS platforms

Lib/site.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@ def _getuserbase():
280280
if env_base:
281281
return env_base
282282

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

287287
def joinuser(*args):

Lib/sysconfig/__init__.py

+14-6
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).
@@ -114,8 +116,8 @@ def _getuserbase():
114116
if env_base:
115117
return env_base
116118

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

121123
def joinuser(*args):
@@ -290,6 +292,7 @@ def _get_preferred_schemes():
290292
'home': 'posix_home',
291293
'user': 'osx_framework_user',
292294
}
295+
293296
return {
294297
'prefix': 'posix_prefix',
295298
'home': 'posix_home',
@@ -623,10 +626,15 @@ def get_platform():
623626
if m:
624627
release = m.group()
625628
elif osname[:6] == "darwin":
626-
import _osx_support
627-
osname, release, machine = _osx_support.get_platform_osx(
628-
get_config_vars(),
629-
osname, release, machine)
629+
if sys.platform == "ios":
630+
release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "12.0")
631+
osname = sys.platform
632+
machine = sys.implementation._multiarch
633+
else:
634+
import _osx_support
635+
osname, release, machine = _osx_support.get_platform_osx(
636+
get_config_vars(),
637+
osname, release, machine)
630638

631639
return f"{osname}-{release}-{machine}"
632640

Lib/test/pythoninfo.py

+1
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ def format_groups(groups):
290290
"HOMEDRIVE",
291291
"HOMEPATH",
292292
"IDLESTARTUP",
293+
"IPHONEOS_DEPLOYMENT_TARGET",
293294
"LANG",
294295
"LDFLAGS",
295296
"LDSHARED",

Lib/test/test_concurrent_futures/test_thread_pool.py

+1
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

+1
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,7 @@ def test_collect_garbage(self):
12231223
self.assertEqual(len(gc.garbage), 0)
12241224

12251225

1226+
@requires_subprocess()
12261227
@unittest.skipIf(BUILD_WITH_NDEBUG,
12271228
'built with -NDEBUG')
12281229
def test_refcount_errors(self):

0 commit comments

Comments
 (0)