Skip to content

Commit

Permalink
[3.10] pythongh-114099: Additions to standard library to support iOS (p…
Browse files Browse the repository at this point in the history
…ythonGH-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>
  • Loading branch information
4 people committed Dec 13, 2024
1 parent c630a35 commit f62438d
Show file tree
Hide file tree
Showing 94 changed files with 1,028 additions and 1,508 deletions.
5 changes: 5 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,11 @@ process and user.
:func:`socket.gethostname` or even
``socket.gethostbyaddr(socket.gethostname())``.

On macOS, iOS and Android, this returns the *kernel* name and version (i.e.,
``'Darwin'`` on macOS and iOS; ``'Linux'`` on Android). :func:`platform.uname()`
can be used to get the user-facing operating system name and version on iOS and
Android.

.. availability:: recent flavors of Unix.

.. versionchanged:: 3.3
Expand Down
24 changes: 23 additions & 1 deletion Doc/library/platform.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ Cross Platform
Returns the system/OS name, such as ``'Linux'``, ``'Darwin'``, ``'Java'``,
``'Windows'``. An empty string is returned if the value cannot be determined.

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

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

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

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

.. function:: uname()

Expand Down Expand Up @@ -230,7 +235,6 @@ Windows Platform
macOS Platform
--------------


.. function:: mac_ver(release='', versioninfo=('','',''), machine='')

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

iOS Platform
------------

.. function:: ios_ver(system='', release='', model='', is_simulator=False)

Get iOS version information and return it as a
:func:`~collections.namedtuple` with the following attributes:

* ``system`` is the OS name; either ``'iOS'`` or ``'iPadOS'``.
* ``release`` is the iOS version number as a string (e.g., ``'17.2'``).
* ``model`` is the device model identifier; this will be a string like
``'iPhone13,2'`` for a physical device, or ``'iPhone'`` on a simulator.
* ``is_simulator`` is a boolean describing if the app is running on a
simulator or a physical device.

Entries which cannot be determined are set to the defaults given as
parameters.


Unix Platforms
--------------
Expand Down
18 changes: 17 additions & 1 deletion Doc/library/webbrowser.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ allow the remote browser to maintain its own windows on the display. If remote
browsers are not available on Unix, the controlling process will launch a new
browser and wait.

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

The script :program:`webbrowser` can be used as a command-line interface for the
module. It accepts a URL as the argument. It accepts the following optional
parameters: ``-n`` opens the URL in a new browser window, if possible;
Expand Down Expand Up @@ -155,6 +162,8 @@ for the controller classes, all defined in this module.
+------------------------+-----------------------------------------+-------+
| ``'chromium-browser'`` | :class:`Chromium('chromium-browser')` | |
+------------------------+-----------------------------------------+-------+
| ``'iosbrowser'`` | ``IOSBrowser`` | \(4) |
+------------------------+-----------------------------------------+-------+

Notes:

Expand All @@ -169,11 +178,18 @@ Notes:
Only on Windows platforms.

(3)
Only on macOS platform.
Only on macOS.

(4)
Only on iOS.


.. versionadded:: 3.3
Support for Chrome/Chromium has been added.

.. versionchanged:: 3.13
Support for iOS has been added.

Here are some simple examples::

url = 'https://docs.python.org/'
Expand Down
71 changes: 71 additions & 0 deletions Lib/_ios_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import sys
try:
from ctypes import cdll, c_void_p, c_char_p, util
except ImportError:
# ctypes is an optional module. If it's not present, we're limited in what
# we can tell about the system, but we don't want to prevent the module
# from working.
print("ctypes isn't available; iOS system calls will not be available")
objc = None
else:
# ctypes is available. Load the ObjC library, and wrap the objc_getClass,
# sel_registerName methods
lib = util.find_library("objc")
if lib is None:
# Failed to load the objc library
raise RuntimeError("ObjC runtime library couldn't be loaded")

objc = cdll.LoadLibrary(lib)
objc.objc_getClass.restype = c_void_p
objc.objc_getClass.argtypes = [c_char_p]
objc.sel_registerName.restype = c_void_p
objc.sel_registerName.argtypes = [c_char_p]


def get_platform_ios():
# Determine if this is a simulator using the multiarch value
is_simulator = sys.implementation._multiarch.endswith("simulator")

# We can't use ctypes; abort
if not objc:
return None

# Most of the methods return ObjC objects
objc.objc_msgSend.restype = c_void_p
# All the methods used have no arguments.
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]

# Equivalent of:
# device = [UIDevice currentDevice]
UIDevice = objc.objc_getClass(b"UIDevice")
SEL_currentDevice = objc.sel_registerName(b"currentDevice")
device = objc.objc_msgSend(UIDevice, SEL_currentDevice)

# Equivalent of:
# device_systemVersion = [device systemVersion]
SEL_systemVersion = objc.sel_registerName(b"systemVersion")
device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion)

# Equivalent of:
# device_systemName = [device systemName]
SEL_systemName = objc.sel_registerName(b"systemName")
device_systemName = objc.objc_msgSend(device, SEL_systemName)

# Equivalent of:
# device_model = [device model]
SEL_model = objc.sel_registerName(b"model")
device_model = objc.objc_msgSend(device, SEL_model)

# UTF8String returns a const char*;
SEL_UTF8String = objc.sel_registerName(b"UTF8String")
objc.objc_msgSend.restype = c_char_p

# Equivalent of:
# system = [device_systemName UTF8String]
# release = [device_systemVersion UTF8String]
# model = [device_model UTF8String]
system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode()
release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()

return system, release, model, is_simulator
3 changes: 3 additions & 0 deletions Lib/distutils/tests/test_cygwinccompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from io import BytesIO
from test.support import run_unittest

if sys.platform != 'win32':
raise unittest.SkipTest("Cygwin tests only needed on Windows")

from distutils import cygwinccompiler
from distutils.cygwinccompiler import (check_config_h,
CONFIG_H_OK, CONFIG_H_NOTOK,
Expand Down
5 changes: 4 additions & 1 deletion Lib/distutils/tests/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from distutils import sysconfig
from distutils.ccompiler import get_default_compiler
from distutils.tests import support
from test.support import run_unittest, swap_item
from test.support import is_apple_mobile, requires_subprocess, run_unittest, swap_item
from test.support.os_helper import TESTFN
from test.support.warnings_helper import check_warnings

Expand All @@ -32,6 +32,7 @@ def cleanup_testfn(self):
elif os.path.isdir(TESTFN):
shutil.rmtree(TESTFN)

@unittest.skipIf(is_apple_mobile, "Header files not distributed with Apple mobile")
def test_get_config_h_filename(self):
config_h = sysconfig.get_config_h_filename()
self.assertTrue(os.path.isfile(config_h), config_h)
Expand All @@ -48,6 +49,7 @@ def test_get_config_vars(self):
self.assertIsInstance(cvars, dict)
self.assertTrue(cvars)

@unittest.skipIf(is_apple_mobile, "Header files not distributed with Apple mobile")
def test_srcdir(self):
# See Issues #15322, #15364.
srcdir = sysconfig.get_config_var('srcdir')
Expand Down Expand Up @@ -247,6 +249,7 @@ def test_SO_in_vars(self):
self.assertIsNotNone(vars['SO'])
self.assertEqual(vars['SO'], vars['EXT_SUFFIX'])

@requires_subprocess()
def test_customize_compiler_before_get_config_vars(self):
# Issue #21923: test that a Distribution compiler
# instance can be called without an explicit call to
Expand Down
14 changes: 8 additions & 6 deletions Lib/distutils/unixccompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,9 @@ def find_library_file(self, dirs, lib, debug=0):
static_f = self.library_filename(lib, lib_type='static')

if sys.platform == 'darwin':
# On OSX users can specify an alternate SDK using
# '-isysroot', calculate the SDK root if it is specified
# (and use it further on)
# On macOS users can specify an alternate SDK using
# '-isysroot <path>' or --sysroot=<path>, calculate the SDK root
# if it is specified (and use it further on)
#
# Note that, as of Xcode 7, Apple SDKs may contain textual stub
# libraries with .tbd extensions rather than the normal .dylib
Expand All @@ -291,12 +291,14 @@ def find_library_file(self, dirs, lib, debug=0):
cflags = sysconfig.get_config_var('CFLAGS')
m = re.search(r'-isysroot\s*(\S+)', cflags)
if m is None:
sysroot = _osx_support._default_sysroot(sysconfig.get_config_var('CC'))
m = re.search(r'--sysroot=(\S+)', cflags)
if m is None:
sysroot = _osx_support._default_sysroot(sysconfig.get_config_var('CC'))
else:
sysroot = m.group(1)
else:
sysroot = m.group(1)



for dir in dirs:
shared = os.path.join(dir, shared_f)
dylib = os.path.join(dir, dylib_f)
Expand Down
25 changes: 20 additions & 5 deletions Lib/distutils/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,25 @@ def get_host_platform():
if m:
release = m.group()
elif osname[:6] == "darwin":
import _osx_support, distutils.sysconfig
osname, release, machine = _osx_support.get_platform_osx(
distutils.sysconfig.get_config_vars(),
osname, release, machine)
import distutils.sysconfig
config_vars = distutils.sysconfig.get_config_vars()
if sys.platform == "ios":
release = config_vars.get("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
osname = sys.platform
machine = sys.implementation._multiarch
elif sys.platform == "tvos":
release = config_vars.get("TVOS_DEPLOYMENT_TARGET", "9.0")
osname = sys.platform
machine = sys.implementation._multiarch
elif sys.platform == "watchos":
release = config_vars.get("WATCHOS_DEPLOYMENT_TARGET", "4.0")
osname = sys.platform
machine = sys.implementation._multiarch
else:
import _osx_support
osname, release, machine = _osx_support.get_platform_osx(
config_vars,
osname, release, machine)

return "%s-%s-%s" % (osname, release, machine)

Expand Down Expand Up @@ -170,7 +185,7 @@ def check_environ ():
if _environ_checked:
return

if os.name == 'posix' and 'HOME' not in os.environ:
if os.name == 'posix' and 'HOME' not in os.environ and sys.platform not in {"ios", "tvos", "watchos"}:
try:
import pwd
os.environ['HOME'] = pwd.getpwuid(os.getuid())[5]
Expand Down
1 change: 1 addition & 0 deletions Lib/lib2to3/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def test_load_grammar_from_pickle(self):
shutil.rmtree(tmpdir)

@unittest.skipIf(sys.executable is None, 'sys.executable required')
@test.support.requires_subprocess()
def test_load_grammar_from_subprocess(self):
tmpdir = tempfile.mkdtemp()
tmpsubdir = os.path.join(tmpdir, 'subdir')
Expand Down
53 changes: 46 additions & 7 deletions Lib/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,30 @@ def mac_ver(release='', versioninfo=('', '', ''), machine=''):
# If that also doesn't work return the default values
return release, versioninfo, machine


# A namedtuple for iOS version information.
IOSVersionInfo = collections.namedtuple(
"IOSVersionInfo",
["system", "release", "model", "is_simulator"]
)


def ios_ver(system="", release="", model="", is_simulator=False):
"""Get iOS version information, and return it as a namedtuple:
(system, release, model, is_simulator).
If values can't be determined, they are set to values provided as
parameters.
"""
if sys.platform == "ios":
import _ios_support
result = _ios_support.get_platform_ios()
if result is not None:
return IOSVersionInfo(*result)

return IOSVersionInfo(system, release, model, is_simulator)


def _java_getprop(name, default):

from java.lang import System
Expand Down Expand Up @@ -564,7 +588,7 @@ def _platform(*args):
if cleaned == platform:
break
platform = cleaned
while platform[-1] == '-':
while platform and platform[-1] == '-':
platform = platform[:-1]

return platform
Expand Down Expand Up @@ -605,7 +629,7 @@ def _syscmd_file(target, default=''):
default in case the command should fail.
"""
if sys.platform in ('dos', 'win32', 'win16'):
if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}:
# XXX Others too ?
return default

Expand Down Expand Up @@ -744,6 +768,14 @@ def get_OpenVMS():
csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0)
return 'Alpha' if cpu_number >= 128 else 'VAX'

# On the iOS simulator, os.uname returns the architecture as uname.machine.
# On device it returns the model name for some reason; but there's only one
# CPU architecture for iOS devices, so we know the right answer.
def get_ios():
if sys.implementation._multiarch.endswith("simulator"):
return os.uname().machine
return 'arm64'

def from_subprocess():
"""
Fall back to `uname -p`
Expand Down Expand Up @@ -893,6 +925,10 @@ def uname():
system = 'Windows'
release = 'Vista'

# Normalize responses on iOS
if sys.platform == 'ios':
system, release, _, _ = ios_ver()

vals = system, node, release, version, machine
# Replace 'unknown' values with the more portable ''
_uname_cache = uname_result(*map(_unknown_as_blank, vals))
Expand Down Expand Up @@ -1205,11 +1241,14 @@ def platform(aliased=0, terse=0):
system, release, version = system_alias(system, release, version)

if system == 'Darwin':
# macOS (darwin kernel)
macos_release = mac_ver()[0]
if macos_release:
system = 'macOS'
release = macos_release
# macOS and iOS both report as a "Darwin" kernel
if sys.platform == "ios":
system, release, _, _ = ios_ver()
else:
macos_release = mac_ver()[0]
if macos_release:
system = 'macOS'
release = macos_release

if system == 'Windows':
# MS platforms
Expand Down
Loading

0 comments on commit f62438d

Please sign in to comment.