Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,4 @@ jobs:
run: xcodebuild -version

- name: Run iOS Simulator tests
run: |
xcodebuild \
-scheme astrolabe-runtime-ios \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest' \
test
run: scripts/run-ios-simulator-tests.sh
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.codegraph/
.swiftpm/
.DS_Store
Package.resolved
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/regulusleow/astrolabe-protocol.git",
exact: "1.0.0"
exact: "2.0.0"
)
],
targets: [
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Astrolabe Runtime for iOS

English | [简体中文](README.zh-CN.md)

Astrolabe Runtime for iOS exposes UIKit and Core Animation inspection data to
the Astrolabe Host during development. Runtime activation is compiled out of
Release builds.

Current package release: `1.0.0`.
Current package release: `2.0.0`.

## Requirements

Expand Down
121 changes: 121 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Astrolabe Runtime for iOS

[English](README.md) | 简体中文

Astrolabe Runtime for iOS 在开发期间向 Astrolabe Host 暴露 UIKit 和 Core Animation
检查数据。Release 构建不会编译 Runtime 激活逻辑。

当前 Package 版本:`2.0.0`。

## 环境要求

- iOS 15 或更高版本
- 支持 Swift 5.9 或更高版本的 Xcode
- Astrolabe Host 工具

## 安装

在 Xcode 中添加以下 Package:

```text
https://github.com/regulusleow/astrolabe-runtime-ios
```

将 `AstrolabeRuntime` Product 链接到 App Target。

## 接入

在当前 Scene 生命周期中启动和停止 Runtime。

Objective-C:

```objc
@import AstrolabeRuntime;

- (void)sceneDidBecomeActive:(UIScene *)scene {
#if DEBUG
[ASTRuntime start];
#endif
}

- (void)sceneDidEnterBackground:(UIScene *)scene {
#if DEBUG
[ASTRuntime stop];
#endif
}
```

Swift:

```swift
func sceneDidBecomeActive(_ scene: UIScene) {
#if DEBUG
ASTRuntime.start()
#endif
}

func sceneDidEnterBackground(_ scene: UIScene) {
#if DEBUG
ASTRuntime.stop()
#endif
}
```

Objective-C 代码需要处理启动失败时,可以使用 `startWithCompletion:`。需要自定义端口、请求限制或
检查授权时,可以使用 `UIKitRuntimeLifecycle`。

## 能力

- 发现模拟器和已配对 USB 设备上的 Runtime。
- 采集 UIKit 和 Core Animation 层级。
- 获取 Frame、可见性、无障碍、文本、字体排版、颜色、边框、阴影、控件状态、图片和 Auto Layout
元数据。
- 通过稳定的不透明标识符查询节点详情。
- 对受支持的文本、字体、颜色、Alpha、边框、圆角、阴影和已标识的 Auto Layout 约束值应用白名单内
的内存临时表现补丁。

补丁不会修改源代码、App 二进制文件、业务模型或持久化存储。补丁在 Host 重新连接后仍然有效,并在
Runtime 停止或 App 进程退出时失效。

## 架构

| Target | 职责 |
| --- | --- |
| `AstrolabeRuntimeCore` | iOS SDK 使用的协议服务、路由、Session、Transport、节点注册表和平台无关补丁协调。 |
| `AstrolabeRuntimeUIKit` | UIKit 和 Core Animation 采集、属性映射、Auto Layout、无障碍信息及白名单内的修改操作。 |
| `AstrolabeRuntime` | 公共 `ASTRuntime` 外观接口、SDK 元数据、生命周期和依赖组装。 |
| `AstrolabeRuntimeObjC` | 最小化的 Objective-C Runtime 元数据适配器。 |

该 Package 实现 `astrolabe-protocol` 定义的平台无关 Wire Protocol。iOS 特有的数据采集、映射、
生命周期和端口选择保留在本仓库中。

## 开发

运行兼容 macOS 的测试和 Release 构建:

```bash
npm ci
npm test
swift test --parallel
swift build -c release --product AstrolabeRuntime
```

在 iOS 模拟器中运行 UIKit 和集成测试:

```bash
xcodebuild \
-scheme astrolabe-runtime-ios \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest' \
test
```

## 安全性

Runtime 仅监听 Loopback TCP Endpoint,并且在 Release 构建中不可用。USB Transport 同样依赖
usbmux 强制执行的主机与设备配对机制。请勿分发包含敏感运行时数据的 Debug 构建。

临时修改仅限 Runtime 自身维护的补丁能力清单中声明的属性。Runtime 不会暴露任意 Selector、方法调用或业务操作。

## 许可证

Astrolabe Runtime for iOS 使用 [Apache License 2.0](LICENSE) 许可。
2 changes: 1 addition & 1 deletion Sources/AstrolabeRuntime/AstrolabeRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import AstrolabeProtocol

public enum AstrolabeRuntimeSDK {
/// Release version advertised by the embedded runtime.
public static let runtimeVersion = "1.0.0"
public static let runtimeVersion = "2.0.0"

public static var protocolVersion: RuntimeProtocolVersion {
.v2
Expand Down
2 changes: 1 addition & 1 deletion docs/ios-runtime-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ Completed runtime migration steps:

1. The platform-neutral wire contract, JSON Schemas, and fixtures live in the
independent `astrolabe-protocol` repository.
2. Runtime and Host both lock `AstrolabeProtocol` to package version `1.0.0`.
2. Runtime and Host both lock `AstrolabeProtocol` to package version `2.0.0`.
3. The Runtime package no longer publishes or maintains a duplicate protocol
target.
4. Handshake, App information, hierarchy, node detail, simulator TCP, and USB
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "astrolabe-runtime-ios",
"version": "1.0.0",
"version": "2.0.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
82 changes: 82 additions & 0 deletions scripts/run-ios-simulator-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash

set -euo pipefail

select_runtime() {
xcrun simctl list runtimes --json | python3 -c '
import json
import re
import sys

runtimes = [
runtime
for runtime in json.load(sys.stdin).get("runtimes", [])
if runtime.get("isAvailable")
and runtime.get("identifier", "").startswith(
"com.apple.CoreSimulator.SimRuntime.iOS-"
)
]
if runtimes:
latest = max(
runtimes,
key=lambda runtime: tuple(
int(component)
for component in re.findall(r"\d+", runtime.get("version", "0"))
),
)
print(latest["identifier"])
'
}

select_device_type() {
xcrun simctl list devicetypes --json | python3 -c '
import json
import sys

device_types = [
device_type
for device_type in json.load(sys.stdin).get("devicetypes", [])
if device_type.get("name", "").startswith("iPhone")
]
preferred = next(
(
device_type
for device_type in device_types
if device_type.get("name") == "iPhone 16 Pro"
),
None,
)
selected = preferred or (device_types[0] if device_types else None)
if selected:
print(selected["identifier"])
'
}

runtime_identifier="$(select_runtime)"
if [[ -z "$runtime_identifier" ]]; then
xcodebuild -downloadPlatform iOS
runtime_identifier="$(select_runtime)"
fi

device_type_identifier="$(select_device_type)"
if [[ -z "$runtime_identifier" || -z "$device_type_identifier" ]]; then
printf 'Unable to resolve an available iOS Simulator runtime and device type\n' >&2
exit 1
fi

device_udid="$(
xcrun simctl create \
Astrolabe-CI \
"$device_type_identifier" \
"$runtime_identifier"
)"

cleanup() {
xcrun simctl delete "$device_udid" >/dev/null 2>&1 || true
}
trap cleanup EXIT

xcodebuild \
-scheme astrolabe-runtime-ios \
-destination "platform=iOS Simulator,id=${device_udid}" \
test
32 changes: 30 additions & 2 deletions scripts/versioning.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ const runtimeMetadataPath = "Sources/AstrolabeRuntime/AstrolabeRuntime.swift";
export const versionedPaths = Object.freeze([
runtimeMetadataPath,
"README.md",
"README.zh-CN.md",
"package-lock.json",
"package.json"
]);

const releaseVersionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
const swiftVersionPattern = /static let runtimeVersion = "(\d+\.\d+\.\d+)"/g;
const documentationVersionPattern = /Current package release: `(\d+\.\d+\.\d+)`/g;
const localizedDocumentationVersionPattern =
/^(.*Package.*`)(\d+\.\d+\.\d+)(`.*)$/gm;

export function assertReleaseVersion(version) {
if (!releaseVersionPattern.test(version)) {
Expand Down Expand Up @@ -72,6 +75,11 @@ export function synchronizeRepositoryVersion(projectRoot, version) {
join(projectRoot, "README.md"),
documentationVersionPattern,
`Current package release: \`${version}\``
),
textVersionUpdate(
join(projectRoot, "README.zh-CN.md"),
localizedDocumentationVersionPattern,
`$1${version}$3`
)
];
updates.forEach(({ path, content }) => writeFileSync(path, content));
Expand Down Expand Up @@ -106,6 +114,14 @@ export function versionConsistencyIssues(projectRoot) {
expectedVersion,
issues
);
inspectTextVersion(
join(projectRoot, "README.zh-CN.md"),
"README.zh-CN.md",
localizedDocumentationVersionPattern,
expectedVersion,
issues,
2
);
return issues;
}

Expand Down Expand Up @@ -145,13 +161,25 @@ function inspectJSONVersion(path, displayPath, expectedVersion, issues) {
);
}

function inspectTextVersion(path, displayPath, pattern, expectedVersion, issues) {
function inspectTextVersion(
path,
displayPath,
pattern,
expectedVersion,
issues,
captureIndex = 1
) {
const matches = [...readFileSync(path, "utf8").matchAll(pattern)];
if (matches.length !== 1) {
issues.push(`${displayPath}: expected one version field, found ${matches.length}`);
return;
}
appendVersionIssue(displayPath, matches[0][1], expectedVersion, issues);
appendVersionIssue(
displayPath,
matches[0][captureIndex],
expectedVersion,
issues
);
}

function appendVersionIssue(path, actualVersion, expectedVersion, issues) {
Expand Down
17 changes: 17 additions & 0 deletions test/ci-workflow.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";

test("iOS Simulator CI provisions and targets an available runtime", async () => {
const [workflow, runner] = await Promise.all([
readFile(".github/workflows/ci.yml", "utf8"),
readFile("scripts/run-ios-simulator-tests.sh", "utf8")
]);

assert.match(workflow, /scripts\/run-ios-simulator-tests\.sh/);
assert.doesNotMatch(workflow, /name=iPhone/);
assert.match(runner, /xcodebuild -downloadPlatform iOS/);
assert.match(runner, /xcrun simctl create/);
assert.match(runner, /platform=iOS Simulator,id=\$\{device_udid\}/);
assert.match(runner, /xcrun simctl delete/);
});
Loading