mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 15:15:16 +00:00
756 lines
25 KiB
Python
Executable File
756 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright 2020 The Bazel Authors. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Invoked by `bazel run` to launch *_application targets in the simulator."""
|
|
|
|
# This script works in one of two modes.
|
|
#
|
|
# If either --ios_simulator_version or --ios_simulator_device were not
|
|
# passed to bazel:
|
|
#
|
|
# 1. Discovers a simulator compatible with the minimum_os of the
|
|
# *_application target, preferring already-booted simulators
|
|
# if possible
|
|
# 2. Boots the simulator if needed
|
|
# 3. Installs and launches the application
|
|
# 4. Displays the application's output on the console
|
|
#
|
|
# This mode does not kill running simulators or shutdown or delete the simulator
|
|
# after it completes.
|
|
#
|
|
# If --ios_simulator_version and --ios_simulator_device were both passed
|
|
# to bazel:
|
|
#
|
|
# 1. Creates a new temporary simulator by running "simctl create ..."
|
|
# 2. Boots the new temporary simulator
|
|
# 3. Installs and launches the application
|
|
# 4. Displays the application's output on the console
|
|
# 5. When done, shuts down and deletes the newly-created simulator
|
|
#
|
|
# All environment variables with names starting with "IOS_" are passed to the
|
|
# application, after stripping the prefix "IOS_".
|
|
|
|
import collections.abc
|
|
import contextlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import os.path
|
|
import pathlib
|
|
import platform
|
|
import plistlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from typing import Dict, Optional
|
|
import zipfile
|
|
|
|
|
|
# Custom type for methods yielding an Apple simulator UDID.
|
|
AppleSimulatorUDID = collections.abc.Generator[str, None, None]
|
|
|
|
|
|
logging.basicConfig(
|
|
format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
level=logging.INFO,
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
if platform.system() != "Darwin":
|
|
raise Exception(
|
|
"Cannot run Apple platform application targets on a non-mac machine."
|
|
)
|
|
|
|
|
|
class DeviceType(collections.abc.Mapping):
|
|
"""Wraps the `devicetype` dictionary from `simctl list -j`.
|
|
|
|
Provides an ordering so iPhones > iPads. In addition, maintains the
|
|
original order from `simctl list` as `simctl_list_index` to ensure
|
|
newer device types are sorted after older device types.
|
|
"""
|
|
|
|
def __init__(self, device_type, simctl_list_index):
|
|
self.device_type = device_type
|
|
self.simctl_list_index = simctl_list_index
|
|
|
|
def __getitem__(self, name):
|
|
return self.device_type[name]
|
|
|
|
def __iter__(self):
|
|
return iter(self.device_type)
|
|
|
|
def __len__(self):
|
|
return len(self.device_type)
|
|
|
|
def __repr__(self):
|
|
return self["name"] + " (" + self["identifier"] + ")"
|
|
|
|
def __lt__(self, other):
|
|
# Order iPhones ahead of (later in the list than) iPads.
|
|
if self.is_ipad() and other.is_iphone():
|
|
return True
|
|
elif self.is_iphone() and other.is_ipad():
|
|
return False
|
|
# Order device types from the same product family in the same order
|
|
# as `simctl list`.
|
|
return self.simctl_list_index < other.simctl_list_index
|
|
|
|
def supports_platform_type(self, platform_type: str) -> bool:
|
|
"""Returns boolean to indicate if device supports given Apple platform type."""
|
|
if platform_type == "ios":
|
|
return self.is_iphone() or self.is_ipad()
|
|
elif platform_type == "tvos":
|
|
return self.is_apple_tv()
|
|
elif platform_type == "watchos":
|
|
return self.is_apple_watch()
|
|
elif platform_type == "visionos":
|
|
return self.is_apple_vision()
|
|
else:
|
|
raise ValueError(
|
|
f"Apple platform type not supported for simulator: {platform_type}."
|
|
)
|
|
|
|
def is_apple_tv(self) -> bool:
|
|
return self.has_product_family_or_identifier("Apple TV")
|
|
|
|
def is_apple_watch(self) -> bool:
|
|
return self.has_product_family_or_identifier("Apple Watch")
|
|
|
|
def is_apple_vision(self) -> bool:
|
|
return self.has_product_family_or_identifier("Apple Vision")
|
|
|
|
def is_iphone(self) -> bool:
|
|
return self.has_product_family_or_identifier("iPhone")
|
|
|
|
def is_ipad(self) -> bool:
|
|
return self.has_product_family_or_identifier("iPad")
|
|
|
|
def has_product_family_or_identifier(self, device_type: str) -> bool:
|
|
product_family = self.get("productFamily")
|
|
if product_family:
|
|
return product_family == device_type
|
|
# Some older simulators are missing `productFamily`. Try to guess from the
|
|
# identifier.
|
|
return device_type in self["identifier"]
|
|
|
|
|
|
class Device(collections.abc.Mapping):
|
|
"""Wraps the `device` dictionary from `simctl list -j`.
|
|
|
|
Provides an ordering so booted devices > shutdown devices, delegating
|
|
to `DeviceType` order when both devices have the same state.
|
|
"""
|
|
|
|
def __init__(self, device, device_type):
|
|
self.device = device
|
|
self.device_type = device_type
|
|
|
|
def is_shutdown(self):
|
|
return self["state"] == "Shutdown"
|
|
|
|
def is_booted(self):
|
|
return self["state"] == "Booted"
|
|
|
|
def __getitem__(self, name):
|
|
return self.device[name]
|
|
|
|
def __iter__(self):
|
|
return iter(self.device)
|
|
|
|
def __len__(self):
|
|
return len(self.device)
|
|
|
|
def __repr__(self):
|
|
return self["name"] + "(" + self["udid"] + ")"
|
|
|
|
def __lt__(self, other):
|
|
if self.is_shutdown() and other.is_booted():
|
|
return True
|
|
elif self.is_booted() and other.is_shutdown():
|
|
return False
|
|
else:
|
|
return self.device_type < other.device_type
|
|
|
|
|
|
def minimum_os_to_simctl_runtime_version(minimum_os: str) -> int:
|
|
"""Converts a minimum OS string to a simctl RuntimeVersion integer.
|
|
|
|
Args:
|
|
minimum_os: A string in the form '12.2' or '13.2.3'.
|
|
|
|
Returns:
|
|
An integer in the form 0xAABBCC, where AA is the major version, BB is
|
|
the minor version, and CC is the micro version.
|
|
"""
|
|
# Pad the minimum OS version to major.minor.micro.
|
|
minimum_os_components = (minimum_os.split(".") + ["0"] * 3)[:3]
|
|
result = 0
|
|
for component in minimum_os_components:
|
|
result = (result << 8) | int(component)
|
|
return result
|
|
|
|
|
|
def discover_best_compatible_simulator(
|
|
*,
|
|
platform_type: str,
|
|
simctl_path: str,
|
|
minimum_os: str,
|
|
sim_device: str,
|
|
sim_os_version: str,
|
|
) -> (Optional[DeviceType], Optional[Device]):
|
|
"""Discovers the best compatible simulator device type and device.
|
|
|
|
Args:
|
|
platform_type: The Apple platform type for the given *_application() target.
|
|
simctl_path: The path to the `simctl` binary.
|
|
minimum_os: The minimum OS version required by the *_application() target.
|
|
sim_device: Optional name of the device (e.g. "iPhone 8 Plus").
|
|
sim_os_version: Optional version of the Apple platform runtime (e.g.
|
|
"13.2").
|
|
|
|
Returns:
|
|
A tuple (device_type, device) containing the DeviceType and Device
|
|
of the best compatible simulator (might be None if no match was found).
|
|
|
|
Raises:
|
|
subprocess.SubprocessError: if `simctl list` fails or times out.
|
|
"""
|
|
# The `simctl list` CLI provides only very basic case-insensitive description
|
|
# matching search term functionality.
|
|
#
|
|
# This code needs to enforce a numeric floor on `minimum_os`, so it directly
|
|
# parses the JSON output by `simctl list` instead of repeatedly invoking
|
|
# `simctl list` with search terms.
|
|
cmd = [simctl_path, "list", "-j"]
|
|
with subprocess.Popen(cmd, stdout=subprocess.PIPE) as process:
|
|
simctl_data = json.load(process.stdout)
|
|
if process.wait() != os.EX_OK:
|
|
raise subprocess.CalledProcessError(process.returncode, cmd)
|
|
compatible_device_types = []
|
|
minimum_runtime_version = minimum_os_to_simctl_runtime_version(minimum_os)
|
|
# Prepare the device name for case-insensitive matching.
|
|
sim_device = sim_device and sim_device.casefold()
|
|
# `simctl list` orders device types from oldest to newest. Remember
|
|
# the index of each device type to preserve that ordering when
|
|
# sorting device types.
|
|
for simctl_list_index, device_type in enumerate(simctl_data["devicetypes"]):
|
|
device_type = DeviceType(device_type, simctl_list_index)
|
|
if not device_type.supports_platform_type(platform_type):
|
|
continue
|
|
# Some older simulators are missing `maxRuntimeVersion`. Assume those
|
|
# simulators support all OSes (even though it's not true).
|
|
max_runtime_version = device_type.get("maxRuntimeVersion")
|
|
if max_runtime_version and max_runtime_version < minimum_runtime_version:
|
|
continue
|
|
if sim_device and device_type["name"].casefold() != sim_device:
|
|
continue
|
|
compatible_device_types.append(device_type)
|
|
compatible_device_types.sort()
|
|
# logger.info(
|
|
# "Found %d potentialcompatible device types.", len(compatible_device_types)
|
|
# )
|
|
compatible_runtime_identifiers = set()
|
|
for runtime in simctl_data["runtimes"]:
|
|
if not runtime["isAvailable"]:
|
|
continue
|
|
if sim_os_version and runtime["version"] != sim_os_version:
|
|
continue
|
|
compatible_runtime_identifiers.add(runtime["identifier"])
|
|
compatible_devices = []
|
|
for runtime_identifier, devices in simctl_data["devices"].items():
|
|
if runtime_identifier not in compatible_runtime_identifiers:
|
|
continue
|
|
for device in devices:
|
|
if not device["isAvailable"]:
|
|
continue
|
|
compatible_device = None
|
|
for device_type in compatible_device_types:
|
|
if device["deviceTypeIdentifier"] == device_type["identifier"]:
|
|
compatible_device = Device(device, device_type)
|
|
break
|
|
if not compatible_device:
|
|
continue
|
|
compatible_devices.append(compatible_device)
|
|
compatible_devices.sort()
|
|
logger.debug("Found %d compatible devices.", len(compatible_devices))
|
|
if compatible_device_types:
|
|
best_compatible_device_type = compatible_device_types[-1]
|
|
else:
|
|
best_compatible_device_type = None
|
|
if compatible_devices:
|
|
best_compatible_device = compatible_devices[-1]
|
|
else:
|
|
best_compatible_device = None
|
|
return (best_compatible_device_type, best_compatible_device)
|
|
|
|
|
|
def persistent_simulator(
|
|
*,
|
|
platform_type: str,
|
|
simctl_path: str,
|
|
minimum_os: str,
|
|
sim_device: str,
|
|
sim_os_version: str,
|
|
) -> str:
|
|
"""Finds or creates a persistent compatible Apple simulator.
|
|
|
|
Boots the simulator if needed. Does not shut down or delete the simulator when
|
|
done.
|
|
|
|
Args:
|
|
platform_type: The Apple platform type for the given *_application() target.
|
|
simctl_path: The path to the `simctl` binary.
|
|
minimum_os: The minimum OS version required by the *_application() target.
|
|
sim_device: Optional name of the device (e.g. "iPhone 8 Plus").
|
|
sim_os_version: Optional version of the Apple platform runtime (e.g.
|
|
"13.2").
|
|
|
|
Returns:
|
|
The UDID of the compatible Apple simulator.
|
|
|
|
Raises:
|
|
Exception: if a compatible simulator was not found.
|
|
"""
|
|
(best_compatible_device_type, best_compatible_device) = (
|
|
discover_best_compatible_simulator(
|
|
platform_type=platform_type,
|
|
simctl_path=simctl_path,
|
|
minimum_os=minimum_os,
|
|
sim_device=sim_device,
|
|
sim_os_version=sim_os_version,
|
|
)
|
|
)
|
|
if best_compatible_device:
|
|
udid = best_compatible_device["udid"]
|
|
if best_compatible_device.is_shutdown():
|
|
logger.debug("Booting compatible device: %s", best_compatible_device)
|
|
subprocess.run([simctl_path, "boot", udid], check=True)
|
|
else:
|
|
logger.debug("Using compatible device: %s", best_compatible_device)
|
|
return udid
|
|
if best_compatible_device_type:
|
|
device_name = best_compatible_device_type["name"]
|
|
device_id = best_compatible_device_type["identifier"]
|
|
# logger.info("Creating new %s simulator", device_name)
|
|
create_result = subprocess.run(
|
|
[simctl_path, "create", device_name, device_id],
|
|
encoding="utf-8",
|
|
stdout=subprocess.PIPE,
|
|
check=True,
|
|
)
|
|
udid = create_result.stdout.rstrip()
|
|
logger.debug("Created new simulator: %s", udid)
|
|
return udid
|
|
raise Exception(
|
|
f"Could not find or create a simulator for the {platform_type} platform"
|
|
f"compatible with minimum OS version {minimum_os} (device name "
|
|
f"{sim_device}, OS version {sim_os_version})"
|
|
)
|
|
|
|
|
|
def wait_for_sim_to_boot(simctl_path: str, udid: str) -> bool:
|
|
"""Blocks until the given simulator is booted.
|
|
|
|
Args:
|
|
simctl_path: The path to the `simctl` binary.
|
|
udid: The identifier of the simulator to wait for.
|
|
|
|
Returns:
|
|
True if the simulator boots within 60 seconds, False otherwise.
|
|
"""
|
|
logger.info("Waiting for simulator to boot...")
|
|
for _ in range(0, 60):
|
|
# The expected output of "simctl list" is like:
|
|
# -- iOS 8.4 --
|
|
# iPhone 5s (E946FA1C-26AB-465C-A7AC-24750D520BEA) (Shutdown)
|
|
# TestDevice (8491C4BC-B18E-4E2D-934A-54FA76365E48) (Booted)
|
|
# So if there's any booted simulator, $booted_device will not be empty.
|
|
#logger.info("will list devices with udid: %s", udid)
|
|
simctl_list_result = subprocess.run(
|
|
[simctl_path, "list", "devices"],
|
|
encoding="utf-8",
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
#logger.info(simctl_list_result.stdout)
|
|
for line in simctl_list_result.stdout.split("\n"):
|
|
if line.find(udid) != -1 and line.find("Booted") != -1:
|
|
logger.debug("Simulator is booted.")
|
|
# Simulator is booted.
|
|
return True
|
|
logger.debug("Simulator not booted, still waiting...")
|
|
time.sleep(1)
|
|
return False
|
|
|
|
|
|
def boot_simulator(*, developer_path: str, simctl_path: str, udid: str) -> None:
|
|
"""Launches the Apple simulator for the given identifier.
|
|
|
|
Ensures the Simulator process is in the foreground.
|
|
|
|
Args:
|
|
developer_path: The path to /Applications/Xcode.app/Contents/Developer.
|
|
simctl_path: The path to the `simctl` binary.
|
|
udid: The identifier of the simulator to wait for.
|
|
|
|
Raises:
|
|
Exception: if the simulator did not launch within 60 seconds.
|
|
"""
|
|
logger.info("Launching simulator with udid: %s", udid)
|
|
# Using subprocess.Popen() to launch Simulator.app and then
|
|
# `osascript -e "tell application \"Simulator\" to activate" is racy
|
|
# and can fail with:
|
|
#
|
|
# Simulator got an error: Connection is invalid. (-609)
|
|
#
|
|
# This is likely because the newly-spawned Simulator.app process
|
|
# hasn't had time to connect to the Apple Events system which
|
|
# `osascript` relies on.
|
|
simulator_path = os.path.join(developer_path, "Applications/Simulator.app")
|
|
subprocess.run(
|
|
["open", "-a", simulator_path, "--args", "-CurrentDeviceUDID", udid],
|
|
check=True,
|
|
)
|
|
logger.debug("Simulator launched.")
|
|
if not wait_for_sim_to_boot(simctl_path, udid):
|
|
raise Exception("Failed to launch simulator with UDID: " + udid)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def temporary_simulator(
|
|
*, platform_type: str, simctl_path: str, device: str, version: str
|
|
) -> AppleSimulatorUDID:
|
|
"""Creates a temporary Apple simulator, cleaned up automatically upon close.
|
|
|
|
Args:
|
|
platform_type: The Apple platform type for the given *_application() target.
|
|
simctl_path: The path to the `simctl` binary.
|
|
device: The name of the device (e.g. "iPhone 8 Plus").
|
|
version: The version of the Apple platform runtime (e.g. "13.2").
|
|
|
|
Yields:
|
|
The UDID of the newly-created Apple simulator.
|
|
"""
|
|
runtime_version_name = version.replace(".", "-")
|
|
# capitalizes 'os' from Apple platform type string (e.g. watchos -> watchOS)
|
|
runtime_platform = platform_type[0:-2].lower() + platform_type[-2:].upper()
|
|
# logger.info("Creating simulator, device=%s, version=%s", device, version)
|
|
simctl_create_result = subprocess.run(
|
|
[
|
|
simctl_path,
|
|
"create",
|
|
"TestDevice",
|
|
device,
|
|
"{prefix}.{runtime_platform}-{runtime_version_name}".format(
|
|
prefix="com.apple.CoreSimulator.SimRuntime",
|
|
runtime_platform=runtime_platform,
|
|
runtime_version_name=runtime_version_name,
|
|
),
|
|
],
|
|
encoding="utf-8",
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
udid = simctl_create_result.stdout.rstrip()
|
|
try:
|
|
logger.info("Killing all running simulators...")
|
|
subprocess.run(
|
|
["pkill", "Simulator"], stderr=subprocess.DEVNULL, check=False
|
|
)
|
|
yield udid
|
|
finally:
|
|
logger.info("Shutting down simulator with udid: %s", udid)
|
|
subprocess.run(
|
|
[simctl_path, "shutdown", udid], stderr=subprocess.DEVNULL, check=False
|
|
)
|
|
logger.info("Deleting simulator with udid: %s", udid)
|
|
subprocess.run([simctl_path, "delete", udid], check=True)
|
|
|
|
|
|
def register_dsyms(dsyms_dir: str):
|
|
"""Adds all dSYMs in `dsyms_dir` to the symbolscache.
|
|
|
|
Args:
|
|
dsyms_dir: Path to directory potentially containing dSYMs
|
|
"""
|
|
symbolscache_command = [
|
|
"/usr/bin/symbolscache",
|
|
"delete",
|
|
"--tag",
|
|
"Bazel",
|
|
"compact",
|
|
"add",
|
|
"--tag",
|
|
"Bazel",
|
|
] + [
|
|
a
|
|
for a in pathlib.Path(dsyms_dir).glob(
|
|
"**/*.dSYM/Contents/Resources/DWARF/*"
|
|
)
|
|
]
|
|
logger.debug("Running command: %s", symbolscache_command)
|
|
result = subprocess.run(
|
|
symbolscache_command,
|
|
capture_output=True,
|
|
check=True,
|
|
encoding="utf-8",
|
|
text=True,
|
|
)
|
|
logger.debug("symbolscache output: %s", result.stdout)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def extracted_app(
|
|
application_output_path: str, app_name: str
|
|
) -> AppleSimulatorUDID:
|
|
"""Extracts Foo.app from *_application() output and makes it writable.
|
|
|
|
Args:
|
|
application_output_path: Path to the output of an `*_application()`. If the
|
|
path is a directory, copies it to a temporary directory and makes the
|
|
contents writable, as `simctl install` fails to install an `.app` that is
|
|
read-only. If the path is an .ipa archive, unzips it to a temporary
|
|
directory.
|
|
app_name: The name of the application (e.g. "Foo" for "Foo.app").
|
|
|
|
Yields:
|
|
Path to Foo.app in temporary directory (re-used if already present).
|
|
"""
|
|
if os.path.isdir(application_output_path):
|
|
# Re-use the same path for each run and rsync to it (reducing
|
|
# copies). Ensure the result is writable, or `simctl install` will
|
|
# fail with `Unhandled error domain NSPOSIXErrorDomain, code 13`.
|
|
dst_dir = os.path.join(tempfile.gettempdir(), "bazel_temp_" + app_name)
|
|
os.makedirs(dst_dir, exist_ok=True)
|
|
|
|
# NOTE: use `which` to find the path to `rsync`.
|
|
# In macOS 15.4, the system `rsync` is using `openrsync` which contains some permission issues.
|
|
# This allows users to workaround the issue by overriding the system `rsync` with a working version.
|
|
# Remove this once we no longer support macOS versions with broken `rsync`.
|
|
rsync_path = shutil.which("rsync")
|
|
|
|
rsync_command = [
|
|
rsync_path,
|
|
"--archive",
|
|
"--delete",
|
|
"--checksum",
|
|
"--chmod=u+w",
|
|
"--verbose",
|
|
# The output path might itself be a symlink; resolve to the
|
|
# real path so rsync doesn't just copy the symlink.
|
|
os.path.realpath(application_output_path),
|
|
dst_dir,
|
|
]
|
|
logger.debug(
|
|
"Found app directory: %s, running command: %s",
|
|
application_output_path,
|
|
rsync_command,
|
|
)
|
|
result = subprocess.run(
|
|
rsync_command,
|
|
capture_output=True,
|
|
check=True,
|
|
encoding="utf-8",
|
|
text=True,
|
|
)
|
|
logger.debug("rsync output: %s", result.stdout)
|
|
yield os.path.join(dst_dir, app_name + ".app")
|
|
else:
|
|
# Create a new temporary directory for each run, deleting it
|
|
# afterwards (there's no efficient way to "sync" an unzip, so this
|
|
# can't re-use the output directory).
|
|
with tempfile.TemporaryDirectory(prefix="bazel_temp") as temp_dir:
|
|
logger.debug(
|
|
"Unzipping IPA from %s to %s", application_output_path, temp_dir
|
|
)
|
|
with zipfile.ZipFile(application_output_path) as ipa_zipfile:
|
|
ipa_zipfile.extractall(temp_dir)
|
|
yield os.path.join(temp_dir, "Payload", app_name + ".app")
|
|
|
|
|
|
def bundle_id(bundle_path: str) -> str:
|
|
"""Returns the bundle ID given a bundle directory path."""
|
|
info_plist_path = os.path.join(bundle_path, "Info.plist")
|
|
with open(info_plist_path, mode="rb") as plist_file:
|
|
plist = plistlib.load(plist_file)
|
|
return plist["CFBundleIdentifier"]
|
|
|
|
|
|
def simctl_launch_environ() -> Dict[str, str]:
|
|
"""Calculates an environment dictionary for running `simctl launch`."""
|
|
# Pass environment variables prefixed with "IOS_" to the simulator, replace
|
|
# the prefix with "SIMCTL_CHILD_". bazel adds "IOS_" to the env vars which
|
|
# will be passed to the app as prefix to differentiate from other env vars. We
|
|
# replace the prefix "IOS_" with "SIMCTL_CHILD_" here, because "simctl" only
|
|
# pass the env vars prefixed with "SIMCTL_CHILD_" to the app.
|
|
result = {}
|
|
for k, v in os.environ.items():
|
|
if not k.startswith("IOS_"):
|
|
continue
|
|
new_key = k.replace("IOS_", "SIMCTL_CHILD_", 1)
|
|
result[new_key] = v
|
|
if "IDE_DISABLED_OS_ACTIVITY_DT_MODE" not in os.environ:
|
|
# Ensure os_log() mirrors writes to stderr. (lldb and Xcode set this
|
|
# environment variable as well.)
|
|
result["SIMCTL_CHILD_OS_ACTIVITY_DT_MODE"] = "enable"
|
|
return result
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def apple_simulator(
|
|
*,
|
|
platform_type: str,
|
|
simctl_path: str,
|
|
minimum_os: str,
|
|
sim_device: str,
|
|
sim_os_version: str,
|
|
) -> AppleSimulatorUDID:
|
|
"""Finds either a temporary or persistent Apple simulator based on args.
|
|
|
|
Args:
|
|
platform_type: The Apple platform type for the given *_application() target.
|
|
simctl_path: The path to the `simctl` binary.
|
|
minimum_os: The minimum OS version required by the *_application() target.
|
|
sim_device: Optional name of the device (e.g. "iPhone 8 Plus").
|
|
sim_os_version: Optional version of the Apple platform runtime (e.g.
|
|
"13.2").
|
|
|
|
Yields:
|
|
The UDID of the simulator.
|
|
"""
|
|
yield persistent_simulator(
|
|
platform_type=platform_type,
|
|
simctl_path=simctl_path,
|
|
minimum_os=minimum_os,
|
|
sim_device=sim_device,
|
|
sim_os_version=sim_os_version,
|
|
)
|
|
|
|
|
|
def run_app_in_simulator(
|
|
*,
|
|
simulator_udid: str,
|
|
developer_path: str,
|
|
simctl_path: str,
|
|
application_output_path: str,
|
|
app_name: str,
|
|
) -> None:
|
|
"""Installs and runs an app in the specified simulator.
|
|
|
|
Args:
|
|
simulator_udid: The UDID of the simulator in which to run the app.
|
|
developer_path: The path to /Applications/Xcode.app/Contents/Developer.
|
|
simctl_path: The path to the `simctl` binary.
|
|
application_output_path: Path to the output of an `*_application()`.
|
|
app_name: The name of the application (e.g. "Foo" for "Foo.app").
|
|
"""
|
|
boot_simulator(
|
|
developer_path=developer_path,
|
|
simctl_path=simctl_path,
|
|
udid=simulator_udid,
|
|
)
|
|
root_dir = os.path.dirname(application_output_path)
|
|
register_dsyms(root_dir)
|
|
with extracted_app(application_output_path, app_name) as app_path:
|
|
logger.info("Will use simulator: %s", simulator_udid)
|
|
logger.info(
|
|
"Terminating any existing instances of the app in that simulator..."
|
|
)
|
|
# First, quietly kill any existing instances of the app
|
|
app_bundle_id = bundle_id(app_path)
|
|
subprocess.run(
|
|
[simctl_path, "terminate", simulator_udid, app_bundle_id],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
logger.info("Installing...")
|
|
subprocess.run(
|
|
[simctl_path, "install", simulator_udid, app_path], check=True
|
|
)
|
|
logger.info(
|
|
"Launching..."
|
|
)
|
|
args = [
|
|
simctl_path,
|
|
"launch",
|
|
]
|
|
# Append optional launch arguments.
|
|
args.extend(sys.argv[1:])
|
|
args.extend([
|
|
simulator_udid,
|
|
app_bundle_id,
|
|
])
|
|
subprocess.run(args, env=simctl_launch_environ(), check=True)
|
|
|
|
|
|
def main(
|
|
*,
|
|
app_name: str,
|
|
application_output_path: str,
|
|
minimum_os: str,
|
|
platform_type: str,
|
|
sim_device: str,
|
|
sim_os_version: str,
|
|
):
|
|
"""Main entry point to `bazel run` for *_application() targets.
|
|
|
|
Args:
|
|
app_name: The name of the application (e.g. "Foo" for "Foo.app").
|
|
application_output_path: Path to the output of an *_application().
|
|
minimum_os: The minimum OS version required by the *_application() target.
|
|
platform_type: The Apple platform type for the given *_application() target.
|
|
sim_device: The name of the device (e.g. "iPhone 8 Plus").
|
|
sim_os_version: The version of the Apple platform runtime (e.g. "13.2").
|
|
"""
|
|
xcode_select_result = subprocess.run(
|
|
["xcode-select", "-p"],
|
|
encoding="utf-8",
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
developer_path = xcode_select_result.stdout.rstrip()
|
|
simctl_path = os.path.join(developer_path, "usr", "bin", "simctl")
|
|
|
|
with apple_simulator(
|
|
platform_type=platform_type,
|
|
simctl_path=simctl_path,
|
|
minimum_os=minimum_os,
|
|
sim_device=sim_device,
|
|
sim_os_version=sim_os_version,
|
|
) as simulator_udid:
|
|
run_app_in_simulator(
|
|
simulator_udid=simulator_udid,
|
|
developer_path=developer_path,
|
|
simctl_path=simctl_path,
|
|
application_output_path=application_output_path,
|
|
app_name=app_name,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(
|
|
app_name="Telegram",
|
|
application_output_path="Telegram/Telegram.ipa",
|
|
minimum_os="13.0",
|
|
platform_type="ios",
|
|
sim_device="iPhone 16 Pro",
|
|
sim_os_version="26.0",
|
|
)
|