mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[fix] improve iOS privacy and App Store distribution compliance
This commit is contained in:
@@ -375,6 +375,16 @@ jobs:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Select corresponding source tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
|
||||
echo "CROSSDESK_SOURCE_TAG=${GITHUB_REF_NAME}" >> "${GITHUB_ENV}"
|
||||
echo "CROSSDESK_RELEASE_BUILD=YES" >> "${GITHUB_ENV}"
|
||||
fi
|
||||
|
||||
- name: Read independent iOS version
|
||||
id: ios_version
|
||||
@@ -403,9 +413,9 @@ jobs:
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/ios/.xmake/packages
|
||||
key: "${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-xcode26.6-xrepo1211093-min16-${{ github.run_id }}"
|
||||
key: "${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-source-v1-xcode26.6-xrepo1211093-min16-${{ github.run_id }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-xcode26.6-xrepo1211093-min16-
|
||||
${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-source-v1-xcode26.6-xrepo1211093-min16-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -477,6 +487,11 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERIFY_FLAGS=()
|
||||
if [[ "${CROSSDESK_RELEASE_BUILD:-NO}" == "YES" ]]; then
|
||||
VERIFY_FLAGS+=(--require-release)
|
||||
fi
|
||||
python3 apps/ios/scripts/verify_app_bundle.py "${APP_PATH}" "${VERIFY_FLAGS[@]}"
|
||||
xcrun lipo -info "${EXECUTABLE_PATH}"
|
||||
echo "Minimum iOS version: ${MINIMUM_OS}"
|
||||
|
||||
@@ -489,6 +504,9 @@ jobs:
|
||||
|
||||
mkdir -p "${PACKAGE_DIR}"
|
||||
cp -R "${APP_PATH}" "${PACKAGE_DIR}/"
|
||||
cp apps/ios/licenses/sources.json "${PACKAGE_DIR}/ThirdPartySources.json"
|
||||
cp apps/ios/licenses/OPEN_SOURCE_NOTICE.txt "${PACKAGE_DIR}/OPEN_SOURCE_NOTICE.txt"
|
||||
cp "${APP_PATH}/SourceMetadata.json" "${PACKAGE_DIR}/SourceMetadata.json"
|
||||
|
||||
DSYM_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app.dSYM"
|
||||
if [[ -d "${DSYM_PATH}" ]]; then
|
||||
@@ -793,8 +811,13 @@ jobs:
|
||||
|
||||
- name: Prepare release notes
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/release-notes.md" << 'EOF'
|
||||
Binary release only. Source code is not included.
|
||||
cat > "$RUNNER_TEMP/release-notes.md" << EOF
|
||||
## Corresponding source
|
||||
|
||||
Source for these binaries: [${GITHUB_REF_NAME}](https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_SHA}).
|
||||
Clone that version with recursive submodules. The repository includes the build scripts and dependency patches.
|
||||
[iOS source, modification and rebuild instructions](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/apps/ios/README.md).
|
||||
The iOS archive also contains the exact source revision and third-party source catalog with archive checksums.
|
||||
|
||||
## Code signing policy
|
||||
|
||||
|
||||
+25
-7
@@ -1,6 +1,6 @@
|
||||
# CrossDesk Privacy Policy
|
||||
|
||||
Last updated: August 8, 2026
|
||||
Last updated: September 27, 2026
|
||||
|
||||
[中文](#中文) | [English](#english)
|
||||
|
||||
@@ -8,14 +8,14 @@ Last updated: August 8, 2026
|
||||
|
||||
### 适用范围
|
||||
|
||||
本隐私政策适用于 CrossDesk 桌面客户端及项目提供的默认网络服务。用户自行部署或指定的服务器由相应服务器运营者负责,其数据处理方式不在本政策控制范围内。
|
||||
本隐私政策适用于 CrossDesk 桌面客户端、iOS 客户端及项目提供的默认网络服务。用户自行部署或指定的服务器由相应服务器运营者负责,其数据处理方式不在本政策控制范围内。
|
||||
|
||||
### 网络通信
|
||||
|
||||
CrossDesk 是远程桌面软件,需要通过网络完成设备发现、连接协商及远程会话。客户端可能进行以下网络通信:
|
||||
|
||||
- **默认服务:** 未启用自托管时,客户端会连接预配置的 CrossDesk 服务(`api.crossdesk.cn`),用于设备注册、信令、网络地址转换(NAT)穿透,并在无法建立直接连接时提供中继。
|
||||
- **版本检查:** 客户端启动时会从 CrossDesk 版本服务(`version.crossdesk.cn`)获取最新版本信息。当前即使启用自托管,版本检查仍会访问 CrossDesk 版本服务。
|
||||
- **版本检查:** 桌面客户端启动时会从 CrossDesk 版本服务(`version.crossdesk.cn`)获取最新版本信息。桌面端当前即使启用自托管,版本检查仍会访问 CrossDesk 版本服务。iOS 客户端不执行这项版本检查。
|
||||
- **远程会话:** 用户主动发起或接受远程连接后,CrossDesk 会根据使用的功能在设备之间传输屏幕画面、音频、键盘和鼠标输入、剪贴板文本以及用户选择传输的文件。剪贴板文本可能在远程会话期间自动同步。这些数据可能通过直接连接传输,也可能在必要时通过配置的中继服务器转发。
|
||||
- **自托管:** 用户可以在设置中指定自己的信令及中继服务器。自托管服务器的日志、数据保存和访问规则由其运营者决定。
|
||||
|
||||
@@ -33,7 +33,7 @@ CrossDesk 是远程桌面软件,需要通过网络完成设备发现、连接
|
||||
- 设备标识、连接设置及用户偏好会保存在本地设备上,以便后续使用。
|
||||
- 通过官方信令和中继服务处理的信息仅用于建立、维持和转发 CrossDesk 连接,以及服务运行、安全防护和故障排查。
|
||||
- 中继传输的远程会话内容仅用于实时转发,不用于广告或用户画像。
|
||||
- CrossDesk 桌面客户端不包含广告功能,也不会主动上传诊断日志、崩溃报告或用户行为分析数据。
|
||||
- CrossDesk 桌面及 iOS 客户端不包含广告功能,也不会主动上传诊断日志、崩溃报告或用户行为分析数据。
|
||||
- 自托管服务器可能保存的日志和数据由其运营者控制,请向相应运营者了解其隐私与保存政策。
|
||||
|
||||
### 用户选择
|
||||
@@ -46,6 +46,15 @@ CrossDesk 是远程桌面软件,需要通过网络完成设备发现、连接
|
||||
- 结束远程会话,以停止会话数据传输;
|
||||
- 删除 CrossDesk 的本地配置、缓存及日志文件。
|
||||
|
||||
### iOS 客户端的本地数据与权限
|
||||
|
||||
- iOS 客户端作为控制端接收远程电脑的画面和音频,不采集本机屏幕、摄像头或麦克风。
|
||||
- 局域网权限用于发现和连接局域网中的设备。客户端也可通过信令及中继服务连接远程设备。
|
||||
- 服务器配置及最近连接记录保存在应用本地;最近连接记录可以包含远程桌面的预览缩略图。本机信令登录凭据和选择记住的远程连接密码保存在系统钥匙串中,不通过 iCloud 钥匙串同步。升级时会将旧版本保存在应用偏好中的本机登录凭据迁入钥匙串,保存成功后删除旧值。删除相应最近连接记录时,也会删除其缩略图和已保存的远程连接密码。
|
||||
- 当前 iOS 界面不读取或发送本机剪贴板文本;会话收到的远程剪贴板文本可以写入本机剪贴板。
|
||||
- 发送文件需通过系统文件选择器选择文件;接收的文件保存在应用的 `Documents/Received` 目录,可通过系统“文件”应用或共享功能管理。
|
||||
- 应用及通信库使用本地文件元数据、应用偏好和计时 API 提供文件传输、配置保存及网络计时功能,不使用这些信息制作设备指纹或进行广告跟踪。
|
||||
|
||||
### 政策更新与联系方式
|
||||
|
||||
本政策可能随着 CrossDesk 功能或网络服务的变化而更新。更新内容将在本文件中发布。如有隐私相关问题,请联系 [junkun.di@hotmail.com](mailto:junkun.di@hotmail.com)。
|
||||
@@ -56,14 +65,14 @@ CrossDesk 是远程桌面软件,需要通过网络完成设备发现、连接
|
||||
|
||||
### Scope
|
||||
|
||||
This Privacy Policy applies to the CrossDesk desktop client and the default network services provided by the project. Servers deployed or selected by users are operated by their respective administrators, and their data-processing practices are outside the control of this policy.
|
||||
This Privacy Policy applies to the CrossDesk desktop and iOS clients and the default network services provided by the project. Servers deployed or selected by users are operated by their respective administrators, and their data-processing practices are outside the control of this policy.
|
||||
|
||||
### Network communications
|
||||
|
||||
CrossDesk is remote desktop software and requires network communications for device discovery, connection negotiation, and remote sessions. The client may perform the following communications:
|
||||
|
||||
- **Default services:** Unless self-hosting is enabled, the client connects to preconfigured CrossDesk services (`api.crossdesk.cn`) for device registration, signaling, network address translation (NAT) traversal, and relay when a direct connection cannot be established.
|
||||
- **Update checks:** On startup, the client retrieves current release information from the CrossDesk version service (`version.crossdesk.cn`). Update checks currently continue to use the CrossDesk version service even when self-hosting is enabled.
|
||||
- **Update checks:** On startup, the desktop client retrieves current release information from the CrossDesk version service (`version.crossdesk.cn`). Desktop update checks currently continue to use the CrossDesk version service even when self-hosting is enabled. The iOS client does not perform this update check.
|
||||
- **Remote sessions:** After a user initiates or accepts a remote connection, CrossDesk transmits the data required by enabled features between devices. This may include screen content, audio, keyboard and mouse input, clipboard text, and files selected by the user. Clipboard text may be synchronized automatically during a remote session. Data may travel over a direct connection or through the configured relay server when necessary.
|
||||
- **Self-hosting:** Users may configure their own signaling and relay servers. Logging, retention, and access policies for a self-hosted server are determined by its operator.
|
||||
|
||||
@@ -81,7 +90,7 @@ To provide these functions, CrossDesk may process:
|
||||
- Device identifiers, connection settings, and user preferences are stored locally for subsequent use.
|
||||
- Information processed by the official signaling and relay services is used only to establish, maintain, and relay CrossDesk connections and for service operation, security, and troubleshooting.
|
||||
- Remote-session content passing through a relay is used only for real-time forwarding and is not used for advertising or user profiling.
|
||||
- The CrossDesk desktop client does not contain advertising functionality and does not intentionally upload diagnostic logs, crash reports, or behavioral analytics.
|
||||
- The CrossDesk desktop and iOS clients do not contain advertising functionality and does not intentionally upload diagnostic logs, crash reports, or behavioral analytics.
|
||||
- Logs and data that may be retained by self-hosted servers are controlled by their operators. Contact the relevant operator for its privacy and retention practices.
|
||||
|
||||
### User choices
|
||||
@@ -94,6 +103,15 @@ Users may:
|
||||
- end a remote session to stop session-data transmission; and
|
||||
- remove local CrossDesk configuration, cache, and log files.
|
||||
|
||||
### iOS local data and permissions
|
||||
|
||||
- The iOS client controls remote computers and receives their screen content and audio. It does not capture the iPhone or iPad screen, camera or microphone.
|
||||
- Local network permission supports discovering and connecting to local devices. Remote connections may also use signaling and relay services.
|
||||
- Server settings and recent connections are stored locally. Recent connections may include remote desktop preview thumbnails. Local signaling credentials and remembered remote connection passwords are stored in the system Keychain without iCloud Keychain synchronization. On upgrade, legacy local login credentials stored in app preferences are migrated to the Keychain; the old values are removed only after saving succeeds. Removing a recent connection also removes its thumbnail and saved remote connection password.
|
||||
- The current iOS interface does not read or send local clipboard text. Clipboard text received during a remote session may be written to the local clipboard.
|
||||
- Sending a file requires selecting it using the system document picker. Received files are stored in the application's `Documents/Received` directory and can be managed through Files or the share action.
|
||||
- The application and its communication libraries use local file metadata, app preferences and timing APIs for file transfer, settings and network timing. This information is not used for device fingerprinting or advertising tracking.
|
||||
|
||||
### Changes and contact
|
||||
|
||||
This policy may be updated as CrossDesk features or network services change. Updates will be published in this file. For privacy-related questions, contact [junkun.di@hotmail.com](mailto:junkun.di@hotmail.com).
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
100000000000000000000006 /* NativeMetalVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000009 /* NativeMetalVideoView.swift */; };
|
||||
100000000000000000000007 /* RemoteTouchInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000A /* RemoteTouchInputView.swift */; };
|
||||
100000000000000000000008 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000D /* Assets.xcassets */; };
|
||||
100000000000000000000009 /* ThirdPartyLicensesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000E /* ThirdPartyLicensesView.swift */; };
|
||||
10000000000000000000000A /* ThirdPartyLicenses.json in Resources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000F /* ThirdPartyLicenses.json */; };
|
||||
10000000000000000000000B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000010 /* PrivacyInfo.xcprivacy */; };
|
||||
10000000000000000000000C /* OpenSSL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 200000000000000000000011 /* OpenSSL.framework */; };
|
||||
10000000000000000000000D /* OpenSSL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 200000000000000000000011 /* OpenSSL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -31,13 +36,30 @@
|
||||
20000000000000000000000B /* CrossDeskMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CrossDeskMobile.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
20000000000000000000000C /* build_minirtc_ios.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build_minirtc_ios.sh; sourceTree = "<group>"; };
|
||||
20000000000000000000000D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
20000000000000000000000E /* ThirdPartyLicensesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyLicensesView.swift; sourceTree = "<group>"; };
|
||||
20000000000000000000000F /* ThirdPartyLicenses.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Resources/ThirdPartyLicenses.json; sourceTree = "<group>"; };
|
||||
200000000000000000000010 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
200000000000000000000011 /* OpenSSL.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OpenSSL.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
300000000000000000000002 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (10000000000000000000000D /* OpenSSL.framework in Embed Frameworks */, );
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
300000000000000000000001 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
10000000000000000000000C /* OpenSSL.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -59,6 +81,9 @@
|
||||
200000000000000000000001 /* CrossDeskMobileApp.swift */,
|
||||
200000000000000000000002 /* RemoteSessionModel.swift */,
|
||||
200000000000000000000003 /* ContentView.swift */,
|
||||
20000000000000000000000E /* ThirdPartyLicensesView.swift */,
|
||||
20000000000000000000000F /* ThirdPartyLicenses.json */,
|
||||
200000000000000000000010 /* PrivacyInfo.xcprivacy */,
|
||||
200000000000000000000004 /* RemoteSessionView.swift */,
|
||||
400000000000000000000003 /* Bridge */,
|
||||
400000000000000000000004 /* Rendering */,
|
||||
@@ -107,6 +132,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
20000000000000000000000B /* CrossDeskMobile.app */,
|
||||
200000000000000000000011 /* OpenSSL.framework */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -122,6 +148,7 @@
|
||||
600000000000000000000001 /* Sources */,
|
||||
300000000000000000000001 /* Frameworks */,
|
||||
600000000000000000000002 /* Resources */,
|
||||
300000000000000000000002 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -172,6 +199,8 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
100000000000000000000008 /* Assets.xcassets in Resources */,
|
||||
10000000000000000000000A /* ThirdPartyLicenses.json in Resources */,
|
||||
10000000000000000000000B /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -193,10 +222,16 @@
|
||||
);
|
||||
outputPaths = (
|
||||
"$(PROJECT_DIR)/Vendor/iphoneos/$(CONFIGURATION)/libCrossDeskMiniRTC.a",
|
||||
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/OpenSSL",
|
||||
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/Info.plist",
|
||||
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/PrivacyInfo.xcprivacy",
|
||||
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/LICENSE.txt",
|
||||
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/BuildMetadata.json",
|
||||
"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/SourceMetadata.json",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/zsh;
|
||||
shellScript = "\"${PROJECT_DIR}/scripts/build_minirtc_ios.sh\"\n";
|
||||
shellScript = "set -e\npython3 \"${PROJECT_DIR}/scripts/prepare_source_metadata.py\" --output \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SourceMetadata.json\"\n\"${PROJECT_DIR}/scripts/build_minirtc_ios.sh\"\npython3 \"${PROJECT_DIR}/scripts/generate_third_party_licenses.py\" --inputs \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/libCrossDeskMiniRTC.a.inputs.sha256\" --target-info \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/minirtc-target-info.txt\" --xmake-version \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/xmake-version.txt\" --xmake-repository \"$(cat \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/xmake-repository.txt\")\"\npython3 \"${PROJECT_DIR}/scripts/package_openssl_ios.py\" --inputs \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/libCrossDeskMiniRTC.a.inputs.sha256\" --output \"${BUILT_PRODUCTS_DIR}/OpenSSL.framework\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
@@ -208,6 +243,7 @@
|
||||
100000000000000000000001 /* CrossDeskMobileApp.swift in Sources */,
|
||||
100000000000000000000002 /* RemoteSessionModel.swift in Sources */,
|
||||
100000000000000000000003 /* ContentView.swift in Sources */,
|
||||
100000000000000000000009 /* ThirdPartyLicensesView.swift in Sources */,
|
||||
100000000000000000000004 /* RemoteSessionView.swift in Sources */,
|
||||
100000000000000000000005 /* CrossDeskRTCBridge.mm in Sources */,
|
||||
100000000000000000000006 /* NativeMetalVideoView.swift in Sources */,
|
||||
@@ -270,6 +306,7 @@
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = ("$(inherited)", "$(BUILT_PRODUCTS_DIR)", );
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../../deps/submodules/minirtc/src/api",
|
||||
@@ -319,6 +356,7 @@
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = ("$(inherited)", "$(BUILT_PRODUCTS_DIR)", );
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../../deps/submodules/minirtc/src/api",
|
||||
|
||||
@@ -11,6 +11,7 @@ typedef NS_ENUM(NSInteger, CrossDeskSignalState) {
|
||||
CrossDeskSignalStateReconnecting,
|
||||
CrossDeskSignalStateServerClosed,
|
||||
CrossDeskSignalStateTLSCertificateError,
|
||||
CrossDeskSignalStateCredentialUnavailable,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CrossDeskConnectionState) {
|
||||
@@ -47,6 +48,8 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
|
||||
@optional
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didChangeSignalState:(CrossDeskSignalState)state;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didChangeIdentityStorageError:(BOOL)hasError;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didChangeConnectionState:(CrossDeskConnectionState)state
|
||||
remoteID:(NSString *)remoteID;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#import "CrossDeskRTCBridge.h"
|
||||
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <Security/Security.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -101,6 +102,113 @@ bool IsReusableIdentity(const std::string &identity) {
|
||||
separator + 1 < identity.size();
|
||||
}
|
||||
|
||||
std::string ProvisionedIdentityCredential(const std::string &received,
|
||||
const std::string ¤t) {
|
||||
// A successful re-login can acknowledge the ID without echoing its password.
|
||||
// Retain the credential used to authenticate that same ID.
|
||||
if (!IsReusableIdentity(received) && IsReusableIdentity(current) &&
|
||||
current.substr(0, current.find('@')) == received) return current;
|
||||
return received;
|
||||
}
|
||||
|
||||
NSString *IdentityCredentialAccount(NSString *host, NSInteger port) {
|
||||
// Preserve the old server-scoped key so upgrades retain the same identity.
|
||||
return [NSString stringWithFormat:@"CrossDeskIdentity.%@.%ld", host, (long)port];
|
||||
}
|
||||
|
||||
NSMutableDictionary *IdentityCredentialQuery(NSString *account) {
|
||||
return [@{
|
||||
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: @"cn.crossdesk.mobile.signaling-identity",
|
||||
(__bridge id)kSecAttrAccount: account,
|
||||
(__bridge id)kSecAttrSynchronizable: @NO,
|
||||
} mutableCopy];
|
||||
}
|
||||
|
||||
bool IsReusableIdentityValue(NSString *value) {
|
||||
return [value isKindOfClass:NSString.class] && value.UTF8String &&
|
||||
IsReusableIdentity(std::string(value.UTF8String));
|
||||
}
|
||||
|
||||
OSStatus SaveIdentityCredential(NSString *account, NSString *value) {
|
||||
if (!IsReusableIdentityValue(value)) return errSecParam;
|
||||
NSMutableDictionary *query = IdentityCredentialQuery(account);
|
||||
NSDictionary *attributes = @{
|
||||
(__bridge id)kSecValueData: [value dataUsingEncoding:NSUTF8StringEncoding],
|
||||
(__bridge id)kSecAttrAccessible:
|
||||
(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
};
|
||||
// Update in place: deleting first could lose a valid credential on failure.
|
||||
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query,
|
||||
(__bridge CFDictionaryRef)attributes);
|
||||
if (status == errSecItemNotFound) {
|
||||
NSMutableDictionary *item = [query mutableCopy];
|
||||
[item addEntriesFromDictionary:attributes];
|
||||
status = SecItemAdd((__bridge CFDictionaryRef)item, nullptr);
|
||||
if (status == errSecDuplicateItem) {
|
||||
status = SecItemUpdate((__bridge CFDictionaryRef)query,
|
||||
(__bridge CFDictionaryRef)attributes);
|
||||
}
|
||||
}
|
||||
if (status == errSecSuccess) {
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:account];
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
OSStatus LoadIdentityCredential(NSString *account, NSString **value) {
|
||||
*value = nil;
|
||||
NSMutableDictionary *query = IdentityCredentialQuery(account);
|
||||
query[(__bridge id)kSecReturnData] = @YES;
|
||||
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
CFTypeRef result = nullptr;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
||||
id object = CFBridgingRelease(result);
|
||||
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
|
||||
if (status == errSecSuccess) {
|
||||
NSString *stored = [object isKindOfClass:NSData.class]
|
||||
? [[NSString alloc] initWithData:object encoding:NSUTF8StringEncoding] : nil;
|
||||
if (!IsReusableIdentityValue(stored)) return errSecDecode;
|
||||
*value = stored;
|
||||
// A Keychain value wins over a stale copy left by an interrupted migration.
|
||||
[defaults removeObjectForKey:account];
|
||||
return errSecSuccess;
|
||||
}
|
||||
if (status != errSecItemNotFound) return status;
|
||||
|
||||
NSString *legacy = [defaults objectForKey:account];
|
||||
if (!legacy) return errSecItemNotFound;
|
||||
if (!IsReusableIdentityValue(legacy)) {
|
||||
// Older passwordless IDs cannot authenticate on a subsequent app launch.
|
||||
[defaults removeObjectForKey:account];
|
||||
return errSecItemNotFound;
|
||||
}
|
||||
status = SaveIdentityCredential(account, legacy);
|
||||
if (status == errSecSuccess) *value = legacy;
|
||||
return status;
|
||||
}
|
||||
|
||||
OSStatus MigrateLegacyIdentityCredentials() {
|
||||
OSStatus first_error = errSecSuccess;
|
||||
for (NSString *key in NSUserDefaults.standardUserDefaults.dictionaryRepresentation) {
|
||||
if (![key hasPrefix:@"CrossDeskIdentity."]) continue;
|
||||
NSString *unused = nil;
|
||||
OSStatus status = LoadIdentityCredential(key, &unused);
|
||||
if (status != errSecSuccess && status != errSecItemNotFound &&
|
||||
first_error == errSecSuccess) first_error = status;
|
||||
}
|
||||
return first_error;
|
||||
}
|
||||
|
||||
OSStatus RemoveIdentityCredential(NSString *account) {
|
||||
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)IdentityCredentialQuery(account));
|
||||
if (status == errSecSuccess || status == errSecItemNotFound) {
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:account];
|
||||
return errSecSuccess;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void CopyCString(char *destination, size_t capacity,
|
||||
const std::string &source) {
|
||||
if (!destination || capacity == 0) return;
|
||||
@@ -230,6 +338,7 @@ void DispatchMain(dispatch_block_t block) {
|
||||
- (void)createIdentityPeer;
|
||||
- (void)createControllerPeer;
|
||||
- (void)destroyIdentityPeer;
|
||||
- (void)reportIdentityStorageStatus:(OSStatus)status;
|
||||
- (void)destroyControllerPeer;
|
||||
@end
|
||||
|
||||
@@ -455,7 +564,16 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
const bool unchanged = self->_state->signal_host == host_value &&
|
||||
self->_state->signal_port == signalPort &&
|
||||
self->_state->identity_peer != nullptr;
|
||||
if (unchanged) return;
|
||||
const OSStatus migration_status = MigrateLegacyIdentityCredentials();
|
||||
[self reportIdentityStorageStatus:migration_status];
|
||||
if (unchanged) {
|
||||
if (IsReusableIdentity(self->_state->identity_with_password)) {
|
||||
NSString *value = [NSString stringWithUTF8String:self->_state->identity_with_password.c_str()];
|
||||
[self reportIdentityStorageStatus:SaveIdentityCredential(
|
||||
IdentityCredentialAccount(trimmed, signalPort), value)];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
[self destroyControllerPeer];
|
||||
[self destroyIdentityPeer];
|
||||
@@ -467,29 +585,26 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
self->_state->identity_with_password.clear();
|
||||
self->_state->identity_base.clear();
|
||||
|
||||
NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%ld",
|
||||
trimmed, (long)signalPort];
|
||||
NSString *cached = [[NSUserDefaults standardUserDefaults]
|
||||
stringForKey:key];
|
||||
if (cached.length > 0) {
|
||||
const std::string cached_identity = cached.UTF8String ?: "";
|
||||
if (IsReusableIdentity(cached_identity)) {
|
||||
self->_state->identity_with_password = cached_identity;
|
||||
self->_state->identity_base = BaseIdentity(cached_identity);
|
||||
DispatchMain(^{
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:
|
||||
@selector(rtcBridge:didProvisionIdentity:)]) {
|
||||
[delegate rtcBridge:self didProvisionIdentity:cached];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// A bare ID cannot authenticate after its first signaling session.
|
||||
// Older iOS builds stored it anyway, causing every later launch to be
|
||||
// rejected as "Incorrect password". Drop it and request a fresh
|
||||
// server-issued ID/password pair.
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
|
||||
}
|
||||
NSString *cached = nil;
|
||||
OSStatus load_status = LoadIdentityCredential(
|
||||
IdentityCredentialAccount(trimmed, signalPort), &cached);
|
||||
if (load_status == errSecSuccess) {
|
||||
self->_state->identity_with_password = cached.UTF8String;
|
||||
self->_state->identity_base = BaseIdentity(self->_state->identity_with_password);
|
||||
NSString *identifier = [NSString stringWithUTF8String:self->_state->identity_base.c_str()];
|
||||
DispatchMain(^{
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(rtcBridge:didProvisionIdentity:)]) {
|
||||
[delegate rtcBridge:self didProvisionIdentity:identifier];
|
||||
}
|
||||
});
|
||||
} else if (load_status != errSecItemNotFound) {
|
||||
[self reportIdentityStorageStatus:load_status];
|
||||
// Locked/unavailable Keychain is not the same as a missing identity.
|
||||
[self handleSignalState:CrossDeskSignalStateCredentialUnavailable
|
||||
role:PeerRole::Identity
|
||||
generation:self->_identityGeneration.load()];
|
||||
return;
|
||||
}
|
||||
[self createIdentityPeer];
|
||||
});
|
||||
@@ -842,6 +957,20 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
});
|
||||
}
|
||||
|
||||
- (void)reportIdentityStorageStatus:(OSStatus)status {
|
||||
const BOOL has_error = status != errSecSuccess;
|
||||
if (has_error) {
|
||||
// Log only the OSStatus, never an account, credential or Keychain payload.
|
||||
NSLog(@"CrossDesk identity Keychain operation failed (%d)", (int)status);
|
||||
}
|
||||
DispatchMain(^{
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(rtcBridge:didChangeIdentityStorageError:)]) {
|
||||
[delegate rtcBridge:self didChangeIdentityStorageError:has_error];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)createIdentityPeer {
|
||||
if (_state->identity_peer || _state->signal_host.empty()) return;
|
||||
_state->identity_context.generation = _identityGeneration.fetch_add(1) + 1;
|
||||
@@ -962,10 +1091,16 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
self->_state->identity_recovery_attempted = true;
|
||||
NSString *host = [NSString stringWithUTF8String:
|
||||
self->_state->signal_host.c_str()];
|
||||
NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%d",
|
||||
host,
|
||||
self->_state->signal_port];
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
|
||||
OSStatus status = RemoveIdentityCredential(
|
||||
IdentityCredentialAccount(host, self->_state->signal_port));
|
||||
[self reportIdentityStorageStatus:status];
|
||||
if (status != errSecSuccess) {
|
||||
[self destroyIdentityPeer];
|
||||
[self handleSignalState:CrossDeskSignalStateCredentialUnavailable
|
||||
role:PeerRole::Identity
|
||||
generation:self->_identityGeneration.load()];
|
||||
return;
|
||||
}
|
||||
[self destroyIdentityPeer];
|
||||
self->_state->identity_with_password.clear();
|
||||
self->_state->identity_base.clear();
|
||||
@@ -1078,25 +1213,25 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
const std::string identity_copy = identity;
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (generation != self->_identityGeneration.load()) return;
|
||||
self->_state->identity_with_password = identity_copy;
|
||||
self->_state->identity_base = BaseIdentity(identity_copy);
|
||||
const std::string credential = ProvisionedIdentityCredential(
|
||||
identity_copy, self->_state->identity_with_password);
|
||||
self->_state->identity_with_password = credential;
|
||||
self->_state->identity_base = BaseIdentity(credential);
|
||||
NSString *host = [NSString stringWithUTF8String:
|
||||
self->_state->signal_host.c_str()];
|
||||
NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%d",
|
||||
host, self->_state->signal_port];
|
||||
NSString *value = [NSString stringWithUTF8String:identity_copy.c_str()];
|
||||
if (IsReusableIdentity(identity_copy)) {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:value forKey:key];
|
||||
} else {
|
||||
// Keep a passwordless identity only for this process. It is valid for
|
||||
// the current login but is not safe to reuse on the next app launch.
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
|
||||
}
|
||||
NSString *account = IdentityCredentialAccount(host, self->_state->signal_port);
|
||||
NSString *value = [NSString stringWithUTF8String:credential.c_str()];
|
||||
OSStatus status = IsReusableIdentity(credential)
|
||||
? SaveIdentityCredential(account, value)
|
||||
: RemoveIdentityCredential(account);
|
||||
[self reportIdentityStorageStatus:status];
|
||||
// Only the public device ID crosses into UI state; keep its password here.
|
||||
NSString *identifier = [NSString stringWithUTF8String:self->_state->identity_base.c_str()];
|
||||
DispatchMain(^{
|
||||
if (generation != self->_identityGeneration.load()) return;
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(rtcBridge:didProvisionIdentity:)]) {
|
||||
[delegate rtcBridge:self didProvisionIdentity:value];
|
||||
[delegate rtcBridge:self didProvisionIdentity:identifier];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -711,6 +711,10 @@ private struct ServerSettingsView: View {
|
||||
} header: {
|
||||
Text("服务器")
|
||||
} footer: {
|
||||
if let warning = session.identityStorageWarning {
|
||||
Text(warning)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
Text(session.localIdentity.isEmpty
|
||||
? "正在获取本机 ID…"
|
||||
: "本机 ID \(session.localIdentity)")
|
||||
@@ -720,6 +724,20 @@ private struct ServerSettingsView: View {
|
||||
.padding(.top, 16)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
Section {
|
||||
if let source = SourceMetadata.bundled {
|
||||
Link(destination: source.privacyPolicyURL) {
|
||||
Label("隐私政策", systemImage: "hand.raised")
|
||||
}
|
||||
}
|
||||
NavigationLink {
|
||||
ThirdPartyLicensesView()
|
||||
} label: {
|
||||
Label("关于与开源许可", systemImage: "doc.text")
|
||||
}
|
||||
} footer: {
|
||||
Text("本应用使用 OpenFEC,遵循 CeCILL-C 许可。版权声明、许可全文和源码入口见“关于与开源许可”。")
|
||||
}
|
||||
}
|
||||
.navigationTitle("设置")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
<string>3B52.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
<string>8FFB.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -332,6 +332,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
@Published private(set) var signalStatus = "正在连接信令服务"
|
||||
@Published private(set) var connectionStatus = "未连接"
|
||||
@Published private(set) var localIdentity = ""
|
||||
@Published private(set) var identityStorageWarning: String?
|
||||
@Published private(set) var isConnecting = false
|
||||
@Published private(set) var isConnected = false
|
||||
@Published private(set) var sessionVisible = false
|
||||
@@ -828,6 +829,12 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
bridge.sendWindowsKeyCode(keyCode, isDown: isDown)
|
||||
}
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge, didChangeIdentityStorageError hasError: Bool) {
|
||||
identityStorageWarning = hasError
|
||||
? "本机登录凭据暂时无法安全存取。请解锁设备后,在设置中点击“应用”重试。"
|
||||
: nil
|
||||
}
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge,
|
||||
didChange state: CrossDeskSignalState) {
|
||||
signalConnected = state.rawValue == 1
|
||||
@@ -840,6 +847,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
case 4: signalStatus = "信令服务重连中"
|
||||
case 5: signalStatus = "信令服务器已关闭连接"
|
||||
case 6: signalStatus = "TLS 证书校验失败"
|
||||
case 7: signalStatus = "无法访问本机登录凭据,请解锁后重试"
|
||||
default: signalStatus = "未知信令状态"
|
||||
}
|
||||
if signalConnected {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,185 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
private struct LicenseDocument: Decodable, Identifiable {
|
||||
let title: String
|
||||
let text: String
|
||||
var id: String { title }
|
||||
}
|
||||
|
||||
private struct LicensedComponent: Decodable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let version: String
|
||||
let license: String
|
||||
let sourceURL: URL
|
||||
let buildSourceURL: URL?
|
||||
let documents: [LicenseDocument]
|
||||
|
||||
var displayVersion: String {
|
||||
if id == "crossdesk" {
|
||||
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
|
||||
}
|
||||
return version.count == 40 ? String(version.prefix(12)) : version
|
||||
}
|
||||
}
|
||||
|
||||
private struct LicenseCatalog: Decodable {
|
||||
let schemaVersion: Int
|
||||
let components: [LicensedComponent]
|
||||
|
||||
static let bundled: Result<LicenseCatalog, Error> = Result {
|
||||
guard let url = Bundle.main.url(forResource: "ThirdPartyLicenses", withExtension: "json") else {
|
||||
throw CocoaError(.fileNoSuchFile)
|
||||
}
|
||||
let catalog = try JSONDecoder().decode(LicenseCatalog.self, from: Data(contentsOf: url))
|
||||
guard catalog.schemaVersion == 1, catalog.components.contains(where: { $0.id == "openfec" }) else {
|
||||
throw CocoaError(.fileReadCorruptFile)
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceMetadata: Decodable {
|
||||
let revision: String
|
||||
let tag: String?
|
||||
let isModified: Bool
|
||||
let sourceURL: URL
|
||||
let buildInstructionsURL: URL
|
||||
let privacyPolicyURL: URL
|
||||
|
||||
static let bundled: SourceMetadata? = {
|
||||
guard let url = Bundle.main.url(forResource: "SourceMetadata", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: url) else { return nil }
|
||||
return try? JSONDecoder().decode(SourceMetadata.self, from: data)
|
||||
}()
|
||||
}
|
||||
|
||||
struct ThirdPartyLicensesView: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
switch LicenseCatalog.bundled {
|
||||
case .success(let catalog):
|
||||
List {
|
||||
Section("本版本源码与构建说明") {
|
||||
if let source = SourceMetadata.bundled {
|
||||
Text(source.tag ?? String(source.revision.prefix(12)))
|
||||
.textSelection(.enabled)
|
||||
if source.isModified {
|
||||
Text("开发版本包含尚未发布的修改,以下链接为基础版本源码。")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Link("CrossDesk 源码", destination: source.sourceURL)
|
||||
Link("获取源码、修改与重新构建", destination: source.buildInstructionsURL)
|
||||
Link("隐私政策", destination: source.privacyPolicyURL)
|
||||
}
|
||||
Text("各组件的使用、复制、修改和再分发权利以相应开源许可证为准。普通使用条款不限制这些许可证授予的权利。")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Section("OpenFEC 告知") {
|
||||
Text("本应用使用 OpenFEC \(catalog.components.first(where: { $0.id == "openfec" })?.displayVersion ?? "") 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。")
|
||||
Text("(c) Copyright 2009 - 2012 INRIA - All rights reserved")
|
||||
.font(.footnote)
|
||||
Text("OpenFEC 按其许可提供有限保证,作者、权利人和后续许可人的责任亦受限制。完整声明、许可条款及源码见下方。")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if let openFEC = catalog.components.first(where: { $0.id == "openfec" }) {
|
||||
NavigationLink("OpenFEC 许可与源码") {
|
||||
ComponentLicenseView(component: openFEC)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section {
|
||||
ForEach(catalog.components) { component in
|
||||
NavigationLink {
|
||||
ComponentLicenseView(component: component)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(component.name)
|
||||
Text(component.displayVersion + " · " + component.license)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("开源组件")
|
||||
} footer: {
|
||||
Text("许可全文和版权声明可离线阅读;查看源码需要网络连接。")
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "doc.text.magnifyingglass")
|
||||
.font(.largeTitle)
|
||||
Text("无法读取开源许可")
|
||||
.font(.headline)
|
||||
Text("应用中的许可文件缺失或损坏,请重新安装应用。")
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationTitle("关于与开源许可")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ComponentLicenseView: View {
|
||||
let component: LicensedComponent
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
Text(component.name).font(.headline)
|
||||
Text(component.displayVersion).textSelection(.enabled)
|
||||
Text(component.license).foregroundStyle(.secondary)
|
||||
Link("查看源码", destination: component.id == "crossdesk"
|
||||
? SourceMetadata.bundled?.sourceURL ?? component.sourceURL
|
||||
: component.sourceURL)
|
||||
if let buildSourceURL = component.buildSourceURL {
|
||||
Link("构建配方与项目补丁", destination: buildSourceURL)
|
||||
}
|
||||
}
|
||||
Section("许可与版权声明") {
|
||||
ForEach(component.documents) { document in
|
||||
NavigationLink {
|
||||
LicenseTextView(text: document.text)
|
||||
.navigationTitle(document.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
Text(document.title == "Source copyright and license notices"
|
||||
? "源码版权与许可声明" : document.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(component.name)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
// UITextView keeps long license documents selectable and efficient to scroll.
|
||||
private struct LicenseTextView: UIViewRepresentable {
|
||||
let text: String
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let view = UITextView()
|
||||
view.isEditable = false
|
||||
view.isSelectable = true
|
||||
view.dataDetectorTypes = [.link]
|
||||
view.adjustsFontForContentSizeCategory = true
|
||||
view.font = .preferredFont(forTextStyle: .body)
|
||||
view.textColor = .label
|
||||
view.backgroundColor = .systemBackground
|
||||
view.textContainerInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ view: UITextView, context: Context) {
|
||||
if view.text != text { view.text = text }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<false/>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
+174
-2
@@ -7,7 +7,7 @@ There is no `WKWebView` or browser runtime.
|
||||
## Requirements
|
||||
|
||||
- Xcode 16 or newer
|
||||
- Xmake available on `PATH`
|
||||
- Xmake 3.1.1 available on `PATH` (or selected with `XMAKE_BIN`)
|
||||
- A physical arm64 iPhone or iPad running iOS 16 or newer
|
||||
|
||||
## Build
|
||||
@@ -22,6 +22,28 @@ cd crossdesk
|
||||
|
||||
For an existing checkout, run `git submodule update --init --recursive` from
|
||||
the repository root. All remaining commands below use that root directory.
|
||||
For a released app, select the tag shown in **Settings → About and open-source
|
||||
licenses** when cloning: `git clone --branch <source-tag> --recurse-submodules
|
||||
https://github.com/kunkundi/crossdesk.git`. The iOS marketing version is independent
|
||||
of the repository tag; use the source tag or commit recorded in the app.
|
||||
|
||||
Install [Xmake 3.1.1](https://github.com/xmake-io/xmake/releases/tag/v3.1.1),
|
||||
CMake, and a current Python 3 with setuptools/wheel. Pin the package repository
|
||||
in a separate Xmake global directory before building:
|
||||
|
||||
```sh
|
||||
export XMAKE_GLOBALDIR="$HOME/.cache/crossdesk/xmake-ios"
|
||||
xmake repo --update
|
||||
git -C "$XMAKE_GLOBALDIR/.xmake/repositories/xmake-repo" fetch origin 121109352da272cd1a3d4ed52f2dcda34f839955
|
||||
git -C "$XMAKE_GLOBALDIR/.xmake/repositories/xmake-repo" checkout --detach 121109352da272cd1a3d4ed52f2dcda34f839955
|
||||
```
|
||||
|
||||
Keep `XMAKE_GLOBALDIR` set for command-line Xcode builds. For builds launched
|
||||
from the Xcode app, set it as a user-defined build setting on the target, or
|
||||
pin the default `~/.xmake/repositories/xmake-repo` instead. The build checks
|
||||
the tool version, package repository commit and reviewed recipe hashes.
|
||||
Application dependencies are built from source, with Xmake precompiled
|
||||
package downloads disabled.
|
||||
|
||||
1. Open `apps/ios/CrossDeskMobile.xcodeproj` and select the `CrossDeskMobile`
|
||||
target and scheme.
|
||||
@@ -34,7 +56,8 @@ the repository root. All remaining commands below use that root directory.
|
||||
|
||||
The first build phase builds MiniRTC and the CrossDesk wire library, downloads
|
||||
and builds their dependencies, then merges the iPhoneOS archives into a local library under
|
||||
`Vendor/`. This build path targets physical devices; simulator builds are
|
||||
`Vendor/`. OpenSSL is kept separate and packaged as a static framework with SDK
|
||||
privacy resources in the Xcode products directory. This build path targets physical devices; simulator builds are
|
||||
not supported. The exact tool versions used by CI are recorded in the
|
||||
[workflow](../../.github/workflows/build.yml).
|
||||
|
||||
@@ -67,6 +90,74 @@ decoding is currently unsupported. Desktop builds retain OpenH264.
|
||||
The native build script checks the merged archive for OpenH264 entry points
|
||||
and rejects a build if they are present, including in cached archives.
|
||||
|
||||
## Third-party licenses
|
||||
|
||||
**设置 → 关于与开源许可** provides offline license texts, copyright notices,
|
||||
and source links. OpenFEC is acknowledged both on the settings page and at the
|
||||
top of the notices page, with its INRIA copyright, CeCILL-C terms and limited
|
||||
warranty/liability notice. Its embedded BSD and CC-BY-SA notices are preserved
|
||||
in the source-notices document as well.
|
||||
|
||||
`licenses/sources.json` pins the reviewed source archives (SHA-256), Git
|
||||
revisions, selected legal files and source-notice extraction rules.
|
||||
`CrossDeskMobile/Resources/ThirdPartyLicenses.json` is the generated resource
|
||||
bundled by Xcode. The catalog covers 34 components for the default iOS build,
|
||||
including header-only dependencies, GLib's proxy-libintl, spdlog's bundled fmt,
|
||||
WebRTC-derived code, inih, and the applicable patent notices. Host build tools,
|
||||
OpenH264, desktop-only libraries, and the disabled libpsl built-in suffix list
|
||||
are excluded.
|
||||
|
||||
Regenerate from the pinned source distributions with Python 3.9 or newer:
|
||||
|
||||
```sh
|
||||
python3 apps/ios/scripts/generate_third_party_licenses.py --generate
|
||||
```
|
||||
|
||||
The collector uses original checksum-verified archives, including GLib's
|
||||
`COPYING` file even when Xmake excludes it from an extracted build directory.
|
||||
It reuses matching Xmake downloads or caches downloads outside the repository
|
||||
in `~/.cache/crossdesk/licenses/`. License text is preserved, with explicit
|
||||
character decoding for the legacy OpenFEC files. Copyright comment blocks are
|
||||
collected from the reviewed runtime source directories; the catalog is an
|
||||
attribution superset, not a claim that every source file survives linker
|
||||
dead stripping.
|
||||
|
||||
Normal Xcode builds verify the checked-in resource offline, compare all resolved
|
||||
dependency versions (including header-only packages) with the catalog, check
|
||||
the merged archive's input hashes and local package recipes, and check the
|
||||
MiniRTC revision. A missing, stale or altered catalog fails the build. When
|
||||
dependencies or MiniRTC change, review the corresponding source notices and
|
||||
update the lock before regenerating; regeneration alone does not select a new
|
||||
dependency version. `MINIRTC_ENABLE_AOM=true` also requires a reviewed AOM entry.
|
||||
|
||||
The source screen links to the exact application tag/commit, MiniRTC commit,
|
||||
and custom dependency recipes/patches. `SourceMetadata.json` is generated in
|
||||
the app bundle, not in the checkout. Development builds with local edits are
|
||||
explicitly identified as modified. Version-tag CI builds reject an unclean
|
||||
checkout or a mismatched tag. The release archive also carries
|
||||
`ThirdPartySources.json`, `SourceMetadata.json` and the open-source rights
|
||||
notice; release notes provide the corresponding source and build links.
|
||||
Keep the source tags, submodule commits and referenced dependency source
|
||||
archives available for recipients of that release.
|
||||
|
||||
To change a library, edit MiniRTC in its submodule, or modify the relevant
|
||||
package recipe/patch under `deps/submodules/minirtc/thirdparty`. If a package
|
||||
recipe changes, change its package configuration revision (or rebuild that
|
||||
package) so Xmake does not reuse a previous binary. Review affected notices,
|
||||
update recipe hashes, commit MiniRTC, update its revision in `sources.json`,
|
||||
then regenerate the catalog. Build the application again to relink it with
|
||||
the modified library. Use your own team and bundle identifier to install the
|
||||
modified build; the maintainer's signing credentials are not required.
|
||||
|
||||
These materials implement source identification and attribution. They do not
|
||||
by themselves resolve GPL/LGPL distribution-term compatibility or grant
|
||||
additional permissions from other copyright holders. Applicable modification,
|
||||
reverse-engineering and installation-information rights must also be respected.
|
||||
See [LGPLv3 section 4](https://www.gnu.org/licenses/lgpl-3.0.html) and
|
||||
[GPLv3 section 6](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## Optional codecs
|
||||
|
||||
The unused libaom backends are excluded by default. To include them for development,
|
||||
set the `MINIRTC_ENABLE_AOM` environment variable (or Xcode user-defined build setting)
|
||||
to `true`. This includes libaom in the merged native archive without changing the
|
||||
@@ -106,3 +197,84 @@ If decoded frames do not reach the bridge, check that the selected stream is
|
||||
named `Display1`, `Display2`, and so on. If no frames decode, check the selected
|
||||
codec and the desktop capture/encoding logs, then reconnect or switch displays
|
||||
to request a new key frame.
|
||||
|
||||
## App Store archive and bundle validation
|
||||
|
||||
OpenSSL is on Apple's
|
||||
[required SDK privacy-manifest list](https://developer.apple.com/support/third-party-SDK-requirements/).
|
||||
The iOS build compiles OpenSSL from source, packages it separately as
|
||||
`OpenSSL.framework`, then links and embeds it using Xcode's
|
||||
[static framework support](https://developer.apple.com/documentation/xcode/creating-a-static-framework).
|
||||
Xcode removes the static archive from the embedded framework (it may insert a
|
||||
small resource-framework stub) and preserves `PrivacyInfo.xcprivacy`, source
|
||||
metadata and license texts. The application does not load a dynamic OpenSSL copy.
|
||||
|
||||
The iOS-specific OpenSSL recipe disables POSIX metadata calls, automatic
|
||||
`openssl.cnf` loading, and the unused file-URI store provider. It selects the
|
||||
upstream `getrandom` entropy backend, which uses Darwin's `getentropy` on the
|
||||
supported iOS versions; the random-device fallback is not compiled. Existing
|
||||
in-memory certificate/key handling remains available, while `OSSL_STORE` file
|
||||
URLs are intentionally unsupported. Signaling trust and hostname validation
|
||||
use Apple Security rather than OpenSSL's build-host certificate paths.
|
||||
Desktop OpenSSL builds are unchanged.
|
||||
|
||||
`package_openssl_ios.py` checks the actual OpenSSL archives for file metadata,
|
||||
disk-space and boot-time API imports before producing the framework. Its
|
||||
`sysctlbyname` calls are limited to the reviewed `armcap.c` CPU implementation
|
||||
selection, not fingerprinting. The SDK privacy manifest declares no tracking,
|
||||
SDK data collection or required-reason API use for this specific configuration.
|
||||
This is not a declaration about generic OpenSSL builds or the application's
|
||||
signaling/relay data processing. Source or configuration changes require another
|
||||
review; the packager rejects known forbidden imports even when reusing a cache.
|
||||
|
||||
`PrivacyInfo.xcprivacy` declares app preferences (`CA92.1`), sandbox and
|
||||
user-selected file metadata (`C617.1`, `3B52.1`), and elapsed/event timing
|
||||
(`35F9.1`, `8FFB.1`). MiniRTC uses timing for pacing, timeouts, RTT and media
|
||||
packet timestamps. The iOS GLib recipe disables unused filesystem-capacity
|
||||
and system-volume discovery. These APIs must not be repurposed for fingerprinting.
|
||||
The app-level manifest does not assert that the service collects no data: App Store
|
||||
Connect privacy answers also depend on actual signaling/relay-server practices.
|
||||
The application links to the [privacy policy](../../PRIVACY.md), including iOS
|
||||
clipboard, file, local thumbnail and Keychain behavior.
|
||||
|
||||
Validate a built bundle using:
|
||||
|
||||
```sh
|
||||
python3 apps/ios/scripts/verify_app_bundle.py /path/to/CrossDeskMobile.app
|
||||
```
|
||||
|
||||
For a release, first commit all application and submodule changes, regenerate
|
||||
notices for the committed dependency revision, and check out the intended
|
||||
version tag. Publish the tag and its referenced MiniRTC commit to the public repositories
|
||||
before distributing the app. Local tag verification cannot prove that recipients
|
||||
can fetch an unpublished commit. With an App Store-capable development team
|
||||
configured in Xcode:
|
||||
|
||||
```sh
|
||||
xcodebuild -project apps/ios/CrossDeskMobile.xcodeproj \
|
||||
-scheme CrossDeskMobile -configuration Release \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-archivePath /tmp/CrossDeskMobile.xcarchive \
|
||||
CROSSDESK_RELEASE_BUILD=YES archive
|
||||
|
||||
python3 apps/ios/scripts/verify_app_bundle.py \
|
||||
/tmp/CrossDeskMobile.xcarchive/Products/Applications/CrossDeskMobile.app \
|
||||
--require-release
|
||||
|
||||
xcodebuild -exportArchive -archivePath /tmp/CrossDeskMobile.xcarchive \
|
||||
-exportOptionsPlist apps/ios/ExportOptions-AppStore.plist \
|
||||
-exportPath /tmp/CrossDeskMobile-AppStore
|
||||
```
|
||||
|
||||
Set a new `CURRENT_PROJECT_VERSION` for each upload. The export configuration
|
||||
creates a local App Store export; it does not submit the app. Distribution
|
||||
signing requires the appropriate Apple Developer Program membership and
|
||||
provisioning. The unsigned GitHub artifact is for development, not an App Store
|
||||
submission. Organizer/App Store Connect validation remains necessary.
|
||||
|
||||
The app embeds OpenSSL and libsrtp. Do not declare that encryption is limited
|
||||
to Apple system APIs or set `ITSAppUsesNonExemptEncryption=NO` without completing
|
||||
the applicable [export-compliance determination](https://developer.apple.com/help/app-store-connect/manage-app-information/overview-of-export-compliance).
|
||||
After that determination, supply the appropriate declaration and any required
|
||||
approval code in the distribution build. The repository deliberately does not
|
||||
preselect an exemption.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
CrossDesk 开源软件权利说明 / Open-source software rights
|
||||
|
||||
CrossDesk 按 GNU GPL 第 3 版发布,MiniRTC 按 GNU LGPL 第 3 版发布。
|
||||
其他组件适用各自列明的许可证。使用、复制、修改及再分发这些组件的权利,
|
||||
以相应许可证为准;本应用的普通使用条款不限制开源许可证授予你的权利。
|
||||
许可证中的保证、责任限制及其他条件仍然适用。
|
||||
|
||||
“本版本源码与构建说明”提供与发行版本对应的源码入口、子模块信息、
|
||||
依赖配方、补丁及构建说明。可以修改相关库后重新构建和链接应用。
|
||||
在自己的 iPhone 或 iPad 上运行修改版时,需要使用自己的 Apple 开发签名,
|
||||
并按构建文档设置开发团队和 Bundle Identifier。
|
||||
|
||||
CrossDesk is distributed under GNU GPL version 3; MiniRTC is distributed
|
||||
under GNU LGPL version 3. Other components are governed by their listed
|
||||
licenses. Your rights to use, copy, modify and redistribute those components
|
||||
are governed by the applicable licenses. The application's ordinary terms
|
||||
of use do not restrict rights granted by those open-source licenses.
|
||||
The licenses' warranty, liability and other provisions continue to apply.
|
||||
|
||||
The version-specific source and build links identify the application source,
|
||||
submodules, dependency recipes, patches and build instructions. You can
|
||||
modify the libraries and rebuild and relink the application. Running a
|
||||
modified build on your own iPhone or iPad requires your own Apple development
|
||||
signing setup, team and bundle identifier as described in the build guide.
|
||||
@@ -0,0 +1,906 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"platform": "iphoneos-arm64",
|
||||
"components": [
|
||||
{
|
||||
"id": "asio",
|
||||
"name": "Asio",
|
||||
"version": "1.32.0",
|
||||
"license": "BSL-1.0",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/chriskohlhoff/asio/archive/refs/tags/asio-1-32-0.tar.gz",
|
||||
"sha256": "f1b94b80eeb00bb63a3c8cef5047d4e409df4d8a3fe502305976965827d95672"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "asio/COPYING",
|
||||
"sha256": "5cc69ca9afd3417ed8e58bcd9c0bb730375e9e3e92b5aaf6483b93a2837e0cac"
|
||||
},
|
||||
{
|
||||
"path": "asio/LICENSE_1_0.txt",
|
||||
"sha256": "c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "52bddc12cf0837117e935ea5d71aa5c35e7fd6ae802aa7401c6dba598cae0989",
|
||||
"recipe_path": "xmake-repo/packages/a/asio/xmake.lua",
|
||||
"package": "asio",
|
||||
"notice_prefixes": [
|
||||
"asio/include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "concurrentqueue",
|
||||
"name": "ConcurrentQueue",
|
||||
"version": "v1.0.4",
|
||||
"license": "(BSD-2-Clause OR BSL-1.0) / Zlib (semaphore)",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/cameron314/concurrentqueue/archive/refs/tags/v1.0.4.tar.gz",
|
||||
"sha256": "87fbc9884d60d0d4bf3462c18f4c0ee0a9311d0519341cac7cbd361c885e5281"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE.md",
|
||||
"sha256": "22331571e5e3dbe4c79ad715e23127bdf3beabd75052c953ddabf191ef799e14"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "23c69dc09488527b0d6adf1d25f795a9f0afdc7a1d594c44603c2dd07162faa4",
|
||||
"recipe_path": "xmake-repo/packages/c/concurrentqueue/xmake.lua",
|
||||
"package": "concurrentqueue",
|
||||
"notice_prefixes": [
|
||||
"concurrentqueue.h",
|
||||
"blockingconcurrentqueue.h",
|
||||
"lightweightsemaphore.h"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "crossdesk",
|
||||
"name": "CrossDesk / crossdesk_wire",
|
||||
"version": "application source",
|
||||
"license": "GPL-3.0-only",
|
||||
"source": {
|
||||
"kind": "repository",
|
||||
"url": "https://github.com/kunkundi/crossdesk",
|
||||
"revision": null
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"
|
||||
},
|
||||
{
|
||||
"path": "apps/ios/licenses/OPEN_SOURCE_NOTICE.txt",
|
||||
"sha256": "34720d126e6373603d0c1802a8c3276802b93f2a8fbbf2b61837c6fdefea8877"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dav1d",
|
||||
"name": "dav1d",
|
||||
"version": "1.4.3",
|
||||
"license": "BSD-2-Clause and patent grant",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://code.videolan.org/videolan/dav1d/-/archive/e9986de7f4264940af6fa1df1498fd2754077de4/dav1d-e9986de7f4264940af6fa1df1498fd2754077de4.tar.gz",
|
||||
"sha256": "ba147ff83d6c73f578a12f8eece636226e9c1f40cd55a11841a3bcb2c88cd46a",
|
||||
"revision": "e9986de7f4264940af6fa1df1498fd2754077de4"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "b327887de263238deaa80c34cdd2ff3e0ba1d35db585ce14a37ce3e74ee389e9"
|
||||
},
|
||||
{
|
||||
"path": "doc/PATENTS",
|
||||
"sha256": "335eca574598bf4ca181b12f708d6669e5a5e78c8e1513e5b35fa1f03901484b"
|
||||
}
|
||||
],
|
||||
"package": "dav1d",
|
||||
"notice_prefixes": [
|
||||
"src/",
|
||||
"include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "glib",
|
||||
"name": "GLib / GObject / GIO / GModule / GThread / GVDB",
|
||||
"version": "2.84.1",
|
||||
"license": "LGPL-2.1-or-later and embedded notices",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://download.gnome.org/sources/glib/2.84/glib-2.84.1.tar.xz",
|
||||
"sha256": "2b4bc2ec49611a5fc35f86aca855f2ed0196e69e53092bab6bb73396bf30789a"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "fa6f36630bb1e0c571d34b2bbdf188d08495c9dbf58f28cac112f303fc1f58fb"
|
||||
},
|
||||
{
|
||||
"path": "LICENSES/Apache-2.0.txt",
|
||||
"sha256": "074e6e32c86a4c0ef8b3ed25b721ca23aca83df277cd88106ef7177c354615ff"
|
||||
},
|
||||
{
|
||||
"path": "LICENSES/CC0-1.0.txt",
|
||||
"sha256": "a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499"
|
||||
},
|
||||
{
|
||||
"path": "LICENSES/LGPL-2.1-only.txt",
|
||||
"sha256": "fa6f36630bb1e0c571d34b2bbdf188d08495c9dbf58f28cac112f303fc1f58fb"
|
||||
},
|
||||
{
|
||||
"path": "LICENSES/LLVM-exception.txt",
|
||||
"sha256": "e34c58338bd89d43e709e226610d8f32b3e3c47f4ad9a99a8dc1d4ac7842488e"
|
||||
},
|
||||
{
|
||||
"path": "LICENSES/MIT.txt",
|
||||
"sha256": "b85dcd3e453d05982552c52b5fc9e0bdd6d23c6f8e844b984a88af32570b0cc0"
|
||||
},
|
||||
{
|
||||
"path": "LICENSES/MPL-1.1.txt",
|
||||
"sha256": "6214f8b1300bb9f37b16ebc146f8f61af0187e0025042ff95c8b3030744a8795"
|
||||
},
|
||||
{
|
||||
"path": "subprojects/gvdb/COPYING",
|
||||
"sha256": "bdbcdaa7858872393e9d6b216bbdc8a23e99d98dd6cdb7cda19458025b9e19fc"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "773cac2e1a5aec087abc9347c7278f3fca40e36212b33a5f01012875cfbf7e6c",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/glib/xmake.lua",
|
||||
"package": "glib",
|
||||
"notice_prefixes": [
|
||||
"glib/",
|
||||
"gobject/",
|
||||
"gio/",
|
||||
"gmodule/",
|
||||
"subprojects/gvdb/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gssdp",
|
||||
"name": "GSSDP",
|
||||
"version": "1.6.4",
|
||||
"license": "LGPL-2.1-or-later",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://download.gnome.org/sources/gssdp/1.6/gssdp-1.6.4.tar.xz",
|
||||
"sha256": "ff97fdfb7f561d3e6813b4f6a2145259e7c2eff43cc0e63f3fd031d0b6266032"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551"
|
||||
},
|
||||
{
|
||||
"path": "AUTHORS",
|
||||
"sha256": "66b5509fcdd21acaab5d7a3856e699bada9e9ebf0485312abbe987711bc7bd27"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
|
||||
"package": "gssdp",
|
||||
"notice_prefixes": [
|
||||
"libgssdp/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gupnp",
|
||||
"name": "GUPnP",
|
||||
"version": "1.6.9",
|
||||
"license": "LGPL-2.1-or-later",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://download.gnome.org/sources/gupnp/1.6/gupnp-1.6.9.tar.xz",
|
||||
"sha256": "2edb6ee3613558e62f538735368aee27151b7e09d4e2e2c51606833da801869b"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551"
|
||||
},
|
||||
{
|
||||
"path": "AUTHORS",
|
||||
"sha256": "fadd2ab40439bc89d2e9540c02aef1f23edea96983af9cceaef345a9423d3c70"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
|
||||
"package": "gupnp",
|
||||
"notice_prefixes": [
|
||||
"libgupnp/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gupnp-igd",
|
||||
"name": "GUPnP IGD",
|
||||
"version": "1.6.0",
|
||||
"license": "LGPL-2.1-or-later",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://download.gnome.org/sources/gupnp-igd/1.6/gupnp-igd-1.6.0.tar.xz",
|
||||
"sha256": "4099978339ab22126d4968f2a332b6d094fc44c78797860781f1fc2f11771b74"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "6095e9ffa777dd22839f7801aa845b31c9ed07f3d6bf8a26dc5d2dec8ccc0ef3"
|
||||
},
|
||||
{
|
||||
"path": "AUTHORS",
|
||||
"sha256": "77a943ea347578cfcffc032616707d43e5f5e03a12f864c0d421c3d632fc1582"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
|
||||
"package": "gupnp-igd",
|
||||
"notice_prefixes": [
|
||||
"libgupnp-igd/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inih",
|
||||
"name": "inih / INIReader",
|
||||
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
|
||||
"license": "BSD-3-Clause",
|
||||
"source": {
|
||||
"kind": "repository",
|
||||
"url": "https://github.com/kunkundi/minirtc",
|
||||
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "deps/submodules/minirtc/src/inih/LICENSE",
|
||||
"sha256": "bd2a675a5a26fbff77f171f3a15afb1a9c7855be1a01c7e3fd16b8d2617acdda"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "nlohmann_json",
|
||||
"name": "JSON for Modern C++ / Hedley",
|
||||
"version": "v3.11.3",
|
||||
"license": "MIT",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/nlohmann/json/archive/refs/tags/v3.11.3.tar.gz",
|
||||
"sha256": "0d8ef5af7f9794e3263480193c491549b2ba6cc74bb018906202ada498a79406"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE.MIT",
|
||||
"sha256": "86b998c792894ccb911a1cb7994f7a9652894e7a094c0b5e45be2f553f45cf14"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "eadd211f0521a18207e9544c0c5a08c41dca578fce99df5c2577f6fcf2a0fdce",
|
||||
"recipe_path": "xmake-repo/packages/n/nlohmann_json/xmake.lua",
|
||||
"package": "nlohmann_json",
|
||||
"notice_prefixes": [
|
||||
"include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "kcp",
|
||||
"name": "KCP",
|
||||
"version": "1.7",
|
||||
"license": "MIT",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/skywind3000/kcp/archive/refs/tags/1.7.tar.gz",
|
||||
"sha256": "b4d26994d95599ab0c44e1f93002f9fda275094a879d66c192d79d596529199e"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "6587fd02a924b97dbd6758b0573ddec056fb494a84f3709c5aaa33ce1f7f029c"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "f86cac2c4b884f3347a39a59f3dd8de566c257c7fd481c6e49b05f4730f12c4f",
|
||||
"recipe_path": "xmake-repo/packages/k/kcp/xmake.lua",
|
||||
"package": "kcp",
|
||||
"notice_prefixes": [
|
||||
"ikcp.c",
|
||||
"ikcp.h"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "libdatachannel",
|
||||
"name": "libdatachannel",
|
||||
"version": "v0.24.5",
|
||||
"license": "MPL-2.0",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/paullouisageneau/libdatachannel/archive/refs/tags/v0.24.5.tar.gz",
|
||||
"sha256": "454537c3cd526bed935d847bb2dff4046f266eef84d43b2a5f2f2f293c0026f4"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "b3ec15652a47d65e0a1f85f62d02c098c5cf5b8d8c3cf7b03ff099efa7016326",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/libdatachannel/xmake.lua",
|
||||
"package": "libdatachannel",
|
||||
"notice_prefixes": [
|
||||
"include/",
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "libffi",
|
||||
"name": "libffi",
|
||||
"version": "3.4.8",
|
||||
"license": "MIT",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/libffi/libffi/releases/download/v3.4.8/libffi-3.4.8.tar.gz",
|
||||
"sha256": "bc9842a18898bfacb0ed1252c4febcc7e78fa139fd27fdc7a3e30d9d9356119b"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "67894089811f93fca47a76f85e017da6f8582d4ba0905963c6e0f1ad6df7a195"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "de19ac4e3e91ef5715ac582c75e0d10404dc14ec4e33b2e764dc2637a78d24e9",
|
||||
"recipe_path": "xmake-repo/packages/l/libffi/xmake.lua",
|
||||
"package": "libffi",
|
||||
"notice_prefixes": [
|
||||
"src/",
|
||||
"include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "libnice",
|
||||
"name": "libnice",
|
||||
"version": "0.1.24",
|
||||
"license": "MPL-1.1 OR LGPL-2.1-or-later",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://gitlab.freedesktop.org/libnice/libnice/-/archive/0.1.24/libnice-0.1.24.tar.gz",
|
||||
"sha256": "1da5ac13ed5d4e175e0d2d46ad3748c6635244f8f3eb2b8e31578ef59aa2ddce"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "e5504e5ffe894870ea1a652df957a120ead9c3f9a5e9d314ca41ba06f2d72024"
|
||||
},
|
||||
{
|
||||
"path": "COPYING.LGPL",
|
||||
"sha256": "a9bdde5616ecdd1e980b44f360600ee8783b1f99b8cc83a2beb163a0a390e861"
|
||||
},
|
||||
{
|
||||
"path": "COPYING.MPL",
|
||||
"sha256": "3b3c75ae6c663c4f93987647eb5463d33dc1602a1aded3ac479773f2c3928e3a"
|
||||
},
|
||||
{
|
||||
"path": "AUTHORS",
|
||||
"sha256": "d72eb6fa9d5e93abb11d13b6c6f04fda2ba183d0705e703887cda243f6f1eead"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "7d7c30d7d63245d0190e64b050184f6ff6933ef9133bff5329d62a7b9b685441",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/libnice/xmake.lua",
|
||||
"package": "libnice",
|
||||
"notice_prefixes": [
|
||||
"agent/",
|
||||
"nice/",
|
||||
"random/",
|
||||
"socket/",
|
||||
"stun/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "upnp-libpsl",
|
||||
"name": "libpsl",
|
||||
"version": "0.21.5",
|
||||
"license": "MIT / BSD-3-Clause",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/rockdaboot/libpsl/releases/download/0.21.5/libpsl-0.21.5.tar.gz",
|
||||
"sha256": "1dcc9ceae8b128f3c0b3f654decd0e1e891afc6ff81098f227ef260449dae208"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "322392e6001b2b3b0fca705dc9f11f26e12441188ecde50eb1d22909980b2925"
|
||||
},
|
||||
{
|
||||
"path": "src/LICENSE.chromium",
|
||||
"sha256": "1d7f52747a9169751cdf2641a8299c0098e9ecdf9429296ffd55bdb14c9ed5b3"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
|
||||
"package": "upnp-libpsl",
|
||||
"notice_prefixes": [
|
||||
"src/",
|
||||
"include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "libsoup",
|
||||
"name": "libsoup",
|
||||
"version": "3.6.5",
|
||||
"license": "LGPL-2.0-or-later",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://download.gnome.org/sources/libsoup/3.6/libsoup-3.6.5.tar.xz",
|
||||
"sha256": "6891765aac3e949017945c3eaebd8cc8216df772456dc9f460976fbdb7ada234"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "b7993225104d90ddd8024fd838faf300bea5e83d91203eab98e29512acebd69c"
|
||||
},
|
||||
{
|
||||
"path": "AUTHORS",
|
||||
"sha256": "f8776285b4556368878a1b903a1f0e6d6cee342284961562a7716a2dacc28321"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
|
||||
"package": "libsoup",
|
||||
"notice_prefixes": [
|
||||
"libsoup/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "libsrtp",
|
||||
"name": "libsrtp",
|
||||
"version": "v2.7.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/cisco/libsrtp/archive/refs/tags/v2.7.0.tar.gz",
|
||||
"sha256": "54facb1727a557c2a76b91194dcb2d0a453aaf8e2d0cbbf1e3c2848c323e28ad"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "8e19d42a1eec9561f3f347253ddf2e385c55f392f025bb0fd41b88dbf38db5ae"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "9629edb8d395ea3017af1702cafea57173ca0a6a1ea305a6174b1936598faec6",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/libsrtp/xmake.lua",
|
||||
"package": "libsrtp",
|
||||
"notice_prefixes": [
|
||||
"srtp/",
|
||||
"crypto/",
|
||||
"include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "upnp-libxml2",
|
||||
"name": "libxml2",
|
||||
"version": "2.14.6",
|
||||
"license": "MIT and embedded notices",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://download.gnome.org/sources/libxml2/2.14/libxml2-2.14.6.tar.xz",
|
||||
"sha256": "7ce458a0affeb83f0b55f1f4f9e0e55735dbfc1a9de124ee86fb4a66b597203a"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "Copyright",
|
||||
"sha256": "5d4873884a890122a4b9b20ad56ac6f7da1d796a5bfcf04a427970ac96217626"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
|
||||
"package": "upnp-libxml2",
|
||||
"notice_prefixes": [
|
||||
"include/"
|
||||
],
|
||||
"notice_root_files": true
|
||||
},
|
||||
{
|
||||
"id": "libyuv",
|
||||
"name": "libyuv",
|
||||
"version": "2025.8.14",
|
||||
"license": "BSD-3-Clause and patent grant",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://codeload.github.com/kunkundi/libyuv/tar.gz/ec10b61c58ee4b8fbe648d2744d9dad9ccba6430",
|
||||
"sha256": "e6e83acefcb9322a498a04c2c417491ceab18cf9750d306110ab5ab27d84377b",
|
||||
"revision": "ec10b61c58ee4b8fbe648d2744d9dad9ccba6430"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "2b2cc1180c7e6988328ad2033b04b80117419db9c4c584918bbb3cfec7e9364f"
|
||||
},
|
||||
{
|
||||
"path": "PATENTS",
|
||||
"sha256": "4da84211f947c6f9f7dd8fbce513e1522e01833407ae732b8c9a2bbb75fed495"
|
||||
},
|
||||
{
|
||||
"path": "AUTHORS",
|
||||
"sha256": "0c4a51c72a06b3431bb2b750e3b04dbe3694b22e2fcae6e370216424eec56d52"
|
||||
}
|
||||
],
|
||||
"package": "libyuv",
|
||||
"notice_prefixes": [
|
||||
"source/",
|
||||
"include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "minirtc",
|
||||
"name": "MiniRTC",
|
||||
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
|
||||
"license": "LGPL-3.0-only",
|
||||
"source": {
|
||||
"kind": "repository",
|
||||
"url": "https://github.com/kunkundi/minirtc",
|
||||
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "deps/submodules/minirtc/LICENSE",
|
||||
"sha256": "ea8af5e789cb2d4e9b10bce3874982ade163b749b6bfbdb32e2df21c4d106de1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "nghttp2",
|
||||
"name": "nghttp2",
|
||||
"version": "1.68.0",
|
||||
"license": "MIT",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/nghttp2/nghttp2/releases/download/v1.68.0/nghttp2-1.68.0.tar.gz",
|
||||
"sha256": "2c16ffc588ad3f9e2613c3fad72db48ecb5ce15bc362fcc85b342e48daf51013"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "6b94f3abc1aabd0c72a7c7d92a77f79dda7c8a0cb3df839a97890b4116a2de2a"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "eec10214a906c663f74e3e6bf8952cddec39fec3cac710c10d8b9ea669c9ab79",
|
||||
"recipe_path": "xmake-repo/packages/n/nghttp2/xmake.lua",
|
||||
"package": "nghttp2",
|
||||
"notice_prefixes": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "openfec",
|
||||
"name": "OpenFEC",
|
||||
"version": "1.4.2",
|
||||
"license": "CeCILL-C 1.0 / BSD / CC-BY-SA-3.0",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/kunkundi/openfec/archive/refs/tags/1.4.2.tar.gz",
|
||||
"sha256": "5aa19dc56038f4b19927efb53ca28e896fb05f068a4715de512f0709420f03be"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENCE_CeCILL-C_V1-en.txt",
|
||||
"sha256": "b48f5dc16f144cd79ddaadce3cc8ef183778a0b4f75ab4362e134b347ca20f3d",
|
||||
"encoding": "iso-8859-1"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "8644c6306ad6ba4c974dd949d56178c761b72d503fe7c4a577a23817effa6243",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/openfec/xmake.lua",
|
||||
"package": "openfec",
|
||||
"notice_prefixes": [
|
||||
"src/lib_common/",
|
||||
"src/lib_stable/"
|
||||
],
|
||||
"notice_encodings": {
|
||||
"src/lib_common/linear_binary_codes_utils/binary_matrix/of_hamming_weight.c": "gb18030",
|
||||
"src/lib_common/linear_binary_codes_utils/binary_matrix/of_hamming_weight.h": "gb18030"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "openssl3",
|
||||
"name": "OpenSSL",
|
||||
"version": "3.3.2",
|
||||
"license": "Apache-2.0 and embedded notices",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/openssl/openssl/archive/refs/tags/openssl-3.3.2.zip",
|
||||
"sha256": "4cda357946f9dd5541b565dba35348d614288e88aeb499045018970c789c9d61"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE.txt",
|
||||
"sha256": "7d5450cb2d142651b8afa315b5f238efc805dad827d91ba367d8516bc9d49e7a"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "864c308edf59fd802babd45d064d8575635ecd577e829df2b2c7ac7a4d06d4d7",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/openssl/xmake.lua",
|
||||
"package": "openssl3",
|
||||
"notice_prefixes": [
|
||||
"crypto/",
|
||||
"ssl/",
|
||||
"include/",
|
||||
"providers/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "libopus",
|
||||
"name": "Opus",
|
||||
"version": "1.5.1",
|
||||
"license": "BSD-3-Clause and patent grants",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://downloads.xiph.org/releases/opus/opus-1.5.1.tar.gz",
|
||||
"sha256": "b84610959b8d417b611aa12a22565e0a3732097c6389d19098d844543e340f85"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "01e1167d54a096d123cf6dfbbeb19587278845c6481d2d66d545669846079551"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "c965643eb4e093858c27c402e6cde544620460f2169c89d8ff2bab01111d022b",
|
||||
"recipe_path": "xmake-repo/packages/l/libopus/xmake.lua",
|
||||
"package": "libopus",
|
||||
"notice_prefixes": [
|
||||
"src/",
|
||||
"include/",
|
||||
"celt/",
|
||||
"silk/",
|
||||
"dnn/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pcre2",
|
||||
"name": "PCRE2 / SLJIT",
|
||||
"version": "10.47",
|
||||
"license": "BSD-3-Clause / BSD-2-Clause",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.47/pcre2-10.47.tar.gz",
|
||||
"sha256": "c08ae2388ef333e8403e670ad70c0a11f1eed021fd88308d7e02f596fcd9dc16"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENCE.md",
|
||||
"sha256": "197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811"
|
||||
},
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "99272c55f3dcfa07a8a7e15a5c1a33096e4727de74241d65fa049fccfdd59507"
|
||||
},
|
||||
{
|
||||
"path": "deps/sljit/LICENSE",
|
||||
"sha256": "5f216505c0f6ea3273caec89e766eef93cdeb7bbb0c429f9360116d7c938feeb"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "821f1fbc91434f4324be88eeea754db09c5d7be31446242764861ca2aee136b7",
|
||||
"recipe_path": "xmake-repo/packages/p/pcre2/xmake.lua",
|
||||
"package": "pcre2",
|
||||
"notice_prefixes": [
|
||||
"src/",
|
||||
"deps/sljit/sljitLir.c",
|
||||
"deps/sljit/sljitLir.h"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "plog",
|
||||
"name": "plog",
|
||||
"version": "1.1.11",
|
||||
"license": "MIT",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/SergiusTheBest/plog/archive/refs/tags/1.1.11.tar.gz",
|
||||
"sha256": "d60b8b35f56c7c852b7f00f58cbe9c1c2e9e59566c5b200512d0cdbb6309a7c2"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "e4d01796524cbc13b1571a1f0914823c6a23b316ba039e7014f47a4eef7fd4c3"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "deb525d09b5b79351eeb524ab3bc6a3009e434974e585e5e8852c491d7fa07c8",
|
||||
"recipe_path": "xmake-repo/packages/p/plog/xmake.lua",
|
||||
"package": "plog",
|
||||
"notice_prefixes": [
|
||||
"include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "proxy-libintl",
|
||||
"name": "proxy-libintl (GLib libintl)",
|
||||
"version": "33934de09af6a6627eb44e310a8079df009abdbb",
|
||||
"license": "LGPL-2.0-or-later",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://codeload.github.com/frida/proxy-libintl/tar.gz/33934de09af6a6627eb44e310a8079df009abdbb",
|
||||
"sha256": "4ab5af85ec6124cb526a15c58bbc0877d241807e30855779808dbf9292d861aa",
|
||||
"revision": "33934de09af6a6627eb44e310a8079df009abdbb"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "d245807f90032872d1438d741ed21e2490e1175dc8aa3afa5ddb6c8e529b58e5"
|
||||
}
|
||||
],
|
||||
"bundled_by": "glib",
|
||||
"notice_prefixes": [
|
||||
"src/",
|
||||
"libintl.c",
|
||||
"libintl.h"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "spdlog",
|
||||
"name": "spdlog / fmt 10.2.1",
|
||||
"version": "v1.14.1",
|
||||
"license": "MIT",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/gabime/spdlog/archive/refs/tags/v1.14.1.zip",
|
||||
"sha256": "429dfdf3afc1984feb59e414353c21c110bc79609f6d7899d52f6aa388646f6d"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "4ccecab18d1ff0b61174fe3d6c430541625d3ddb865b0d5887db296f883c76e7"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "5b299effb366420b4920c5b89e6535b76173136d91ee01f8f7d3423486a93549",
|
||||
"recipe_path": "xmake-repo/packages/s/spdlog/xmake.lua",
|
||||
"package": "spdlog",
|
||||
"notice_prefixes": [
|
||||
"include/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sqlite3",
|
||||
"name": "SQLite",
|
||||
"version": "3.50.0+400",
|
||||
"license": "Public domain",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://sqlite.org/2025/sqlite-autoconf-3500400.tar.gz",
|
||||
"sha256": "a3db587a1b92ee5ddac2f66b3edb41b26f9c867275782d46c3a088977d6a5b18"
|
||||
},
|
||||
"documents": [],
|
||||
"recipe_sha256": "a752522259dbd722ea79beaa01e5287429ccb37643ccffbecf5fa30ec4df30fd",
|
||||
"recipe_path": "xmake-repo/packages/s/sqlite3/xmake.lua",
|
||||
"package": "sqlite3",
|
||||
"notice_prefixes": [
|
||||
"sqlite3.h"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "svt-av1",
|
||||
"name": "SVT-AV1",
|
||||
"version": "v3.0.2",
|
||||
"license": "BSD-3-Clause-Clear / BSD-2-Clause, AOMedia patent license and embedded notices",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://gitlab.com/AOMediaCodec/SVT-AV1/-/archive/efc905a7c2ed155b3654d7968173622734eeb0c0/SVT-AV1-efc905a7c2ed155b3654d7968173622734eeb0c0.tar.gz",
|
||||
"sha256": "50a897d58470b06f99b386dc691ab63e802962421e6abdb75e4d234a3e73cd6a",
|
||||
"revision": "efc905a7c2ed155b3654d7968173622734eeb0c0"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE.md",
|
||||
"sha256": "0acc2fcb27472bdc9aaf8b71f37055bbdac4f54671b7d922f241bd7fcd0dd3e6"
|
||||
},
|
||||
{
|
||||
"path": "LICENSE-BSD2.md",
|
||||
"sha256": "c582fcbeb9cf972a60d298bfaf41dc5adff2dd14208946edcb9bb56a281e8438"
|
||||
},
|
||||
{
|
||||
"path": "PATENTS.md",
|
||||
"sha256": "20678ab10402659106dc4c147c97b2a6e94b5c0695415e15a8f195ebc3547922"
|
||||
},
|
||||
{
|
||||
"path": "third_party/fastfeat/LICENSE",
|
||||
"sha256": "043dcfd059386f9facd376351b2bd79325778744aa442177390cdfcca54babed"
|
||||
},
|
||||
{
|
||||
"path": "third_party/safestringlib/LICENSE",
|
||||
"sha256": "c0f99a208799ca20c9ccf31a30533d49334eb458dca5a3d14f9afe3e884e5d7d"
|
||||
}
|
||||
],
|
||||
"package": "svt-av1",
|
||||
"notice_prefixes": [
|
||||
"Source/",
|
||||
"third_party/fastfeat/",
|
||||
"third_party/safestringlib/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "usrsctp",
|
||||
"name": "usrsctp",
|
||||
"version": "0.9.5+0",
|
||||
"license": "BSD-3-Clause and embedded notices",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/sctplab/usrsctp/archive/refs/tags/0.9.5.0.tar.gz",
|
||||
"sha256": "260107caf318650a57a8caa593550e39bca6943e93f970c80d6c17e59d62cd92"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE.md",
|
||||
"sha256": "fa53711b25af4b9a9b8dadfea3cb38166ec4b96760c8d62b284055554537d9ef"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "e3dac7d1db8e5bc79d99851e2486371af19962a4b8fca6eaac3156c00eb73b92",
|
||||
"recipe_path": "xmake-repo/packages/u/usrsctp/xmake.lua",
|
||||
"package": "usrsctp",
|
||||
"notice_prefixes": [
|
||||
"usrsctplib/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "webrtc",
|
||||
"name": "WebRTC-derived code in MiniRTC",
|
||||
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
|
||||
"license": "BSD-3-Clause and patent grant",
|
||||
"source": {
|
||||
"kind": "repository",
|
||||
"url": "https://github.com/kunkundi/minirtc",
|
||||
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "deps/submodules/minirtc/thirdparty/webrtc/LICENSE",
|
||||
"sha256": "ab00a482b6a3902e40211b43c5d0441962ea99b6cc7c25c0f243fa270b78d482"
|
||||
},
|
||||
{
|
||||
"path": "deps/submodules/minirtc/thirdparty/webrtc/PATENTS",
|
||||
"sha256": "01462e2068d1a04c2274f3389773014c14ed9bc3446b28303543bd3e3c064145"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "websocketpp",
|
||||
"name": "WebSocket++",
|
||||
"version": "0.8.2",
|
||||
"license": "BSD-3-Clause and embedded notices",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/zaphoyd/websocketpp/archive/refs/tags/0.8.2.tar.gz",
|
||||
"sha256": "6ce889d85ecdc2d8fa07408d6787e7352510750daa66b5ad44aacb47bea76755"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "COPYING",
|
||||
"sha256": "b31e597bc777d884a39663c11d0557a0af40780e1f451de11ea5103924a69667"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "beac245a717c2bc3bc8d17265d9fc9b54f699f3833b11513f8723c4dbaffd539",
|
||||
"recipe_path": "deps/submodules/minirtc/thirdparty/websocketpp/xmake.lua",
|
||||
"package": "websocketpp",
|
||||
"notice_prefixes": [
|
||||
"websocketpp/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "zlib",
|
||||
"name": "zlib",
|
||||
"version": "v1.3.2",
|
||||
"license": "Zlib",
|
||||
"source": {
|
||||
"kind": "archive",
|
||||
"url": "https://github.com/madler/zlib/archive/refs/tags/v1.3.2.tar.gz",
|
||||
"sha256": "b99a0b86c0ba9360ec7e78c4f1e43b1cbdf1e6936c8fa0f6835c0cd694a495a1"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"sha256": "e32ff4e00d9d94930537635291da39e7e612703334bf6fde8c7f1686fe8a45a2"
|
||||
}
|
||||
],
|
||||
"recipe_sha256": "7832bcb086709a0739ae1abeff035f7a103c7169dc8af61ebf91dd887fcbd054",
|
||||
"recipe_path": "xmake-repo/packages/z/zlib/xmake.lua",
|
||||
"package": "zlib",
|
||||
"notice_prefixes": [
|
||||
"zlib.h"
|
||||
]
|
||||
}
|
||||
],
|
||||
"bundle_sha256": "09376251bbda3f5a1f7112f15dcbb1259fef7898f85b45aacafa767348aafdc2",
|
||||
"toolchain": {
|
||||
"xmake_version": "3.1.1",
|
||||
"xmake_repository_revision": "121109352da272cd1a3d4ed52f2dcda34f839955"
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ fi
|
||||
XMAKE_DEVELOPER_DIR="${DEVELOPER_DIR:-$(xcode-select -p)}"
|
||||
XMAKE_TOOLCHAIN_BIN="${XMAKE_DEVELOPER_DIR}/Toolchains/XcodeDefault.xctoolchain/usr/bin"
|
||||
run_xmake() {
|
||||
local xmake_env=()
|
||||
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
|
||||
xmake_env+=("XMAKE_GLOBALDIR=${XMAKE_GLOBALDIR}")
|
||||
fi
|
||||
/usr/bin/env -i \
|
||||
HOME="${HOME}" \
|
||||
PATH="${XMAKE_TOOLCHAIN_BIN}:${PATH}" \
|
||||
@@ -69,11 +73,16 @@ run_xmake() {
|
||||
DEVELOPER_DIR="${XMAKE_DEVELOPER_DIR}" \
|
||||
XMAKE_PKG_INSTALLDIR="${XMAKE_PKG_INSTALLDIR}" \
|
||||
CROSSDESK_SOURCE_DIR="${REPO_DIR}" \
|
||||
"${xmake_env[@]}" \
|
||||
"${XMAKE_BIN}" "$@"
|
||||
}
|
||||
|
||||
OUTPUT_DIR="${IOS_DIR}/Vendor/iphoneos/${CONFIG_NAME}"
|
||||
OUTPUT_LIBRARY="${OUTPUT_DIR}/libCrossDeskMiniRTC.a"
|
||||
XMAKE_REPOSITORY_DIR="$(run_xmake lua -c 'import("core.base.global"); print(path.join(global.directory(), "repositories", "xmake-repo"))')"
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
print -r -- "${XMAKE_REPOSITORY_DIR}" > "${OUTPUT_DIR}/xmake-repository.txt"
|
||||
run_xmake --version > "${OUTPUT_DIR}/xmake-version.txt"
|
||||
MINIRTC_BUILD_DIR="${IOS_DIR}/.xmake/minirtc-build"
|
||||
MINIRTC_LIBRARY="${MINIRTC_BUILD_DIR}/iphoneos/arm64/${MODE}/libminirtc.a"
|
||||
WIRE_BUILD_DIR="${IOS_DIR}/.xmake/wire-build"
|
||||
@@ -106,7 +115,8 @@ configure_xmake() {
|
||||
clean_flags=(-c)
|
||||
fi
|
||||
run_xmake f -P "${project_dir}" "${clean_flags[@]}" -o "${build_dir}" \
|
||||
-p iphoneos -a arm64 -m "${MODE}" --target_minver=16.0 -y "$@"
|
||||
-p iphoneos -a arm64 -m "${MODE}" --target_minver=16.0 \
|
||||
--policies=package.precompiled:n -y "$@"
|
||||
mkdir -p "${build_dir}"
|
||||
print -r -- "${CONFIG_SIGNATURE}" > "${stamp}"
|
||||
}
|
||||
@@ -148,6 +158,8 @@ TARGET_INFO="$(cd "${MINIRTC_DIR}" && TERM=dumb NO_COLOR=1 \
|
||||
CLEAN_INFO="$(print -r -- "${TARGET_INFO}" | sed $'s/\033\\[[0-9;]*[[:alpha:]]//g')"
|
||||
LINK_DIRS=("${(@f)$(print -r -- "${CLEAN_INFO}" | sed -nE 's|.*-> (/.*)/lib -> package.*|\1/lib|p' | sort -u)}")
|
||||
|
||||
print -r -- "${CLEAN_INFO}" > "${OUTPUT_DIR}/minirtc-target-info.txt"
|
||||
|
||||
REQUIRED_LINKS=(
|
||||
nice glib-2.0 gobject-2.0 gmodule-2.0 gio-2.0 gthread-2.0 intl
|
||||
gupnp-igd-1.6 gupnp-1.6 gssdp-1.6 soup-3.0 nghttp2 sqlite3 psl xml2
|
||||
@@ -158,6 +170,7 @@ if [[ "${MINIRTC_ENABLE_AOM}" == "true" ]]; then
|
||||
REQUIRED_LINKS+=(aom)
|
||||
fi
|
||||
DEPENDENCY_ARCHIVES=()
|
||||
MERGE_ARCHIVES=()
|
||||
|
||||
for link_name in "${REQUIRED_LINKS[@]}"; do
|
||||
archive_path=""
|
||||
@@ -173,6 +186,11 @@ for link_name in "${REQUIRED_LINKS[@]}"; do
|
||||
exit 66
|
||||
fi
|
||||
DEPENDENCY_ARCHIVES+=("${archive_path}")
|
||||
# OpenSSL is linked separately as a static framework with its SDK privacy
|
||||
# resources. Keep both archives in the input manifest for source validation.
|
||||
if [[ "${link_name}" != "ssl" && "${link_name}" != "crypto" ]]; then
|
||||
MERGE_ARCHIVES+=("${archive_path}")
|
||||
fi
|
||||
done
|
||||
|
||||
TEMP_LIBRARY="${OUTPUT_LIBRARY}.tmp"
|
||||
@@ -204,7 +222,7 @@ if [[ -f "${OUTPUT_LIBRARY}" ]] && cmp -s "${TEMP_MANIFEST}" "${MERGE_MANIFEST}"
|
||||
fi
|
||||
|
||||
/usr/bin/libtool -static -o "${TEMP_LIBRARY}" \
|
||||
"${MINIRTC_LIBRARY}" "${DEPENDENCY_ARCHIVES[@]}" \
|
||||
"${MINIRTC_LIBRARY}" "${MERGE_ARCHIVES[@]}" \
|
||||
"${WIRE_LIBRARY}"
|
||||
verify_no_openh264 "${TEMP_LIBRARY}"
|
||||
mv -f "${TEMP_LIBRARY}" "${OUTPUT_LIBRARY}"
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Collect iOS notices from pinned source archives; validate them offline at build time."""
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
IOS = Path(__file__).resolve().parents[1]
|
||||
REPO = IOS.parents[1]
|
||||
LOCK = IOS / "licenses/sources.json"
|
||||
OUTPUT = IOS / "CrossDeskMobile/Resources/ThirdPartyLicenses.json"
|
||||
COMMENT = re.compile(rb"/\*.*?\*/|(?://[^\n]*(?:\n|$))+", re.S)
|
||||
RIGHTS = re.compile(rb"copyright|SPDX-License|permission is hereby|author disclaims|public domain", re.I)
|
||||
|
||||
|
||||
def digest(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def json_bytes(value):
|
||||
return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode()
|
||||
|
||||
|
||||
def checked(data, expected, label):
|
||||
if digest(data) != expected:
|
||||
raise ValueError(f"Source checksum mismatch: {label}")
|
||||
return data
|
||||
|
||||
|
||||
def archive_path(component, cache):
|
||||
source = component["source"]
|
||||
target = cache / source["sha256"]
|
||||
if target.exists():
|
||||
checked(target.read_bytes(), source["sha256"], component["id"])
|
||||
return target
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
# Xmake may remove extracted sources or COPYING; read the original archive.
|
||||
candidates = (Path.home() / ".xmake/cache/packages").glob(
|
||||
f"*/{component['id'][0]}/{component['id']}/{component['version']}/*")
|
||||
for candidate in candidates:
|
||||
if candidate.is_file() and candidate.name.endswith((".tar.gz", ".tar.xz", ".zip")):
|
||||
data = candidate.read_bytes()
|
||||
if digest(data) == source["sha256"]:
|
||||
target.write_bytes(data)
|
||||
return target
|
||||
request = urllib.request.Request(source["url"], headers={"User-Agent": "CrossDesk-license-collector"})
|
||||
with urllib.request.urlopen(request, timeout=90) as response:
|
||||
data = checked(response.read(), source["sha256"], component["id"])
|
||||
target.write_bytes(data)
|
||||
return target
|
||||
|
||||
|
||||
@contextmanager
|
||||
def source_files(component, cache):
|
||||
if component["source"]["kind"] == "repository":
|
||||
yield {}, lambda name: (REPO / name).read_bytes()
|
||||
return
|
||||
path = archive_path(component, cache)
|
||||
if zipfile.is_zipfile(path):
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
names = {name.split("/", 1)[1]: name for name in archive.namelist()
|
||||
if "/" in name and not name.endswith("/")}
|
||||
yield names, lambda name: archive.read(names[name])
|
||||
else:
|
||||
with tarfile.open(path) as archive:
|
||||
names = {member.name.split("/", 1)[1]: member.name for member in archive
|
||||
if "/" in member.name and not member.isdir()}
|
||||
# No filesystem extraction. tarfile resolves COPYING symlinks within
|
||||
# the verified archive, including GLib's LICENSES directory.
|
||||
yield names, lambda name: archive.extractfile(names[name]).read()
|
||||
|
||||
|
||||
def component_documents(component, names, read):
|
||||
documents = []
|
||||
for item in component["documents"]:
|
||||
data = checked(read(item["path"]), item["sha256"], item["path"])
|
||||
text = data.decode(item.get("encoding", "utf-8"))
|
||||
if "end_before" in item:
|
||||
if item["end_before"] not in text:
|
||||
raise ValueError(f"Missing notice boundary: {item['path']}")
|
||||
text = text.split(item["end_before"], 1)[0]
|
||||
documents.append({"title": item["path"], "text": text})
|
||||
notices = defaultdict(list)
|
||||
for name in sorted(names):
|
||||
path = Path(name)
|
||||
selected = any(name.startswith(prefix) for prefix in component.get("notice_prefixes", []))
|
||||
selected |= component.get("notice_root_files", False) and "/" not in name
|
||||
if not selected or path.suffix not in (".h", ".hpp", ".c", ".cc", ".cpp", ".S", ".s", ".inc"):
|
||||
continue
|
||||
if any(part.lower() in ("test", "tests", "benchmarks", "examples") for part in path.parts):
|
||||
continue
|
||||
for block in COMMENT.findall(read(name)[:16384]):
|
||||
if RIGHTS.search(block):
|
||||
# Preserve complete original comment blocks, including bundled
|
||||
# fmt, semaphore and per-file copyright/permission exceptions.
|
||||
encoding = component.get("notice_encodings", {}).get(name, "utf-8")
|
||||
notices[block.decode(encoding)].append(name)
|
||||
if notices:
|
||||
documents.append({"title": "Source copyright and license notices", "text": "\n\n".join(
|
||||
"Files: " + ", ".join(files) + "\n\n" + text for text, files in notices.items())})
|
||||
if not documents:
|
||||
raise ValueError(f"No notices collected for {component['id']}")
|
||||
return documents
|
||||
|
||||
|
||||
def generate(lock, cache):
|
||||
components = []
|
||||
for component in lock["components"]:
|
||||
with source_files(component, cache) as (names, read):
|
||||
documents = component_documents(component, names, read)
|
||||
source = component["source"]
|
||||
source_url = source["url"]
|
||||
if source["kind"] == "repository" and source.get("revision"):
|
||||
source_url += "/tree/" + source["revision"]
|
||||
entry = {key: component[key] for key in ("id", "name", "version", "license")} | {
|
||||
"sourceURL": source_url, "documents": documents}
|
||||
recipe = component.get("recipe_path", "")
|
||||
if recipe.startswith("deps/submodules/minirtc/"):
|
||||
mini = next(c for c in lock["components"] if c["id"] == "minirtc")
|
||||
entry["buildSourceURL"] = (mini["source"]["url"] + "/tree/" +
|
||||
mini["source"]["revision"] + "/" +
|
||||
str(Path(recipe).relative_to("deps/submodules/minirtc").parent))
|
||||
components.append(entry)
|
||||
print(f"Collected {component['name']} {component['version']}")
|
||||
return json_bytes({"schemaVersion": 1, "components": components})
|
||||
|
||||
|
||||
def verify(lock, inputs, target_info, xmake_repository=None, xmake_version=None):
|
||||
checked(OUTPUT.read_bytes(), lock["bundle_sha256"], str(OUTPUT))
|
||||
if xmake_version is not None:
|
||||
version = re.search(r"xmake v(\d+\.\d+\.\d+)", xmake_version.read_text())
|
||||
if not version or version[1] != lock["toolchain"]["xmake_version"]:
|
||||
raise ValueError("Xmake version differs from the reviewed iOS toolchain; see apps/ios/README.md")
|
||||
if xmake_repository is not None:
|
||||
revision = subprocess.check_output(
|
||||
["git", "-C", str(xmake_repository), "rev-parse", "HEAD"], text=True).strip()
|
||||
if revision != lock["toolchain"]["xmake_repository_revision"]:
|
||||
raise ValueError("Xmake package repository differs from the reviewed iOS source catalog")
|
||||
changes = subprocess.check_output(
|
||||
["git", "-C", str(xmake_repository), "status", "--porcelain", "--untracked-files=normal"],
|
||||
text=True).strip()
|
||||
if changes:
|
||||
raise ValueError("Xmake package repository has local changes; use the unmodified pinned revision")
|
||||
for component in lock["components"]:
|
||||
recipe = component.get("recipe_path", "")
|
||||
if recipe.startswith("deps/"):
|
||||
checked((REPO / recipe).read_bytes(), component["recipe_sha256"], recipe)
|
||||
elif recipe.startswith("xmake-repo/") and xmake_repository is not None:
|
||||
checked((xmake_repository / Path(recipe).relative_to("xmake-repo")).read_bytes(),
|
||||
component["recipe_sha256"], recipe)
|
||||
if component["source"]["kind"] == "repository":
|
||||
for item in component["documents"]:
|
||||
checked((REPO / item["path"]).read_bytes(), item["sha256"], item["path"])
|
||||
if inputs is None:
|
||||
return
|
||||
if target_info is None:
|
||||
raise ValueError("--target-info is required with --inputs (includes header-only dependencies)")
|
||||
known = {c["package"]: c["version"] for c in lock["components"] if "package" in c}
|
||||
seen = {}
|
||||
# Package names/versions come from resolved Xmake link and include paths,
|
||||
# never from an inventory of all packages left in the installation cache.
|
||||
pattern = r"/packages/[^/\s]+/([^/\s]+)/([^/\s]+)/[a-f0-9]+/"
|
||||
for name, version in re.findall(pattern, inputs.read_text() + target_info.read_text()):
|
||||
if name not in known or known[name] != version:
|
||||
raise ValueError(f"Unreviewed iOS dependency: {name} {version}; regenerate licenses from its source")
|
||||
seen[name] = version
|
||||
if set(seen) != set(known):
|
||||
raise ValueError(f"License/build dependency mismatch; missing: {sorted(set(known) - set(seen))}")
|
||||
for line in inputs.read_text().splitlines():
|
||||
sha, path = line.split(maxsplit=1)
|
||||
checked(Path(path).read_bytes(), sha, path)
|
||||
revision = subprocess.check_output(
|
||||
["git", "-C", str(REPO / "deps/submodules/minirtc"), "rev-parse", "HEAD"], text=True).strip()
|
||||
mini = next(c for c in lock["components"] if c["id"] == "minirtc")
|
||||
if revision != mini["source"]["revision"]:
|
||||
raise ValueError("MiniRTC revision changed; review embedded notices and update sources.json")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--generate", action="store_true", help="download/check pinned sources and regenerate the bundled JSON")
|
||||
parser.add_argument("--cache", type=Path, default=Path.home() / ".cache/crossdesk/licenses")
|
||||
parser.add_argument("--inputs", type=Path, help="native archive .inputs.sha256 manifest")
|
||||
parser.add_argument("--target-info", type=Path, help="resolved Xmake minirtc target information")
|
||||
parser.add_argument("--xmake-repository", type=Path, help="verify the active pinned package repository and recipes")
|
||||
parser.add_argument("--xmake-version", type=Path, help="Xmake version output recorded by the native build")
|
||||
args = parser.parse_args()
|
||||
lock = json.loads(LOCK.read_text())
|
||||
if args.generate:
|
||||
data = generate(lock, args.cache)
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_bytes(data)
|
||||
lock["bundle_sha256"] = digest(data)
|
||||
LOCK.write_bytes(json_bytes(lock))
|
||||
verify(lock, args.inputs, args.target_info, args.xmake_repository, args.xmake_version)
|
||||
print(f"Verified {len(lock['components'])} iOS license components")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
|
||||
print(f"Third-party notices: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Package source-built iOS OpenSSL as a static framework with privacy resources."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
IOS = Path(__file__).resolve().parents[1]
|
||||
PRIVACY = IOS / "OpenSSL/PrivacyInfo.xcprivacy"
|
||||
# The reviewed iOS configuration removes these calls rather than declaring
|
||||
# unrelated purposes for system entropy devices and certificate directories.
|
||||
FORBIDDEN_APIS = {
|
||||
"stat", "fstat", "lstat", "fstatat", "stat64", "fstat64", "lstat64",
|
||||
"getattrlist", "fgetattrlist", "getattrlistbulk", "getdirentriesattr",
|
||||
"statfs", "fstatfs", "statvfs", "fstatvfs", "getfsstat",
|
||||
"mach_absolute_time", "mach_continuous_time", "sysctl",
|
||||
}
|
||||
|
||||
|
||||
def package(inputs, output):
|
||||
catalog = json.loads((IOS / "CrossDeskMobile/Resources/ThirdPartyLicenses.json").read_text())
|
||||
component = next(c for c in catalog["components"] if c["id"] == "openssl3")
|
||||
archives = {}
|
||||
for line in inputs.read_text().splitlines():
|
||||
sha, name = line.split(maxsplit=1)
|
||||
path = Path(name)
|
||||
if path.name in {"libssl.a", "libcrypto.a"}:
|
||||
if path.name in archives:
|
||||
raise ValueError(f"Ambiguous OpenSSL input: {path.name}")
|
||||
if hashlib.sha256(path.read_bytes()).hexdigest() != sha:
|
||||
raise ValueError(f"OpenSSL input checksum mismatch: {path}")
|
||||
archives[path.name] = (path, sha)
|
||||
if set(archives) != {"libssl.a", "libcrypto.a"}:
|
||||
raise ValueError("Both source-built OpenSSL archives must be present")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix="openssl-framework-", dir=output.parent) as temp:
|
||||
binary = Path(temp) / "OpenSSL"
|
||||
subprocess.run(["/usr/bin/libtool", "-static", "-o", str(binary),
|
||||
*(str(archives[name][0]) for name in sorted(archives))], check=True)
|
||||
subprocess.run(["xcrun", "lipo", str(binary), "-verify_arch", "arm64"], check=True)
|
||||
symbols = subprocess.check_output(["xcrun", "nm", "-u", str(binary)], text=True)
|
||||
imports = {line.split()[-1].lstrip("_").split("$")[0]
|
||||
for line in symbols.splitlines() if line.strip() and not line.endswith(":")}
|
||||
unexpected = imports & FORBIDDEN_APIS
|
||||
if unexpected:
|
||||
raise ValueError(f"OpenSSL privacy configuration has unreviewed APIs: {sorted(unexpected)}")
|
||||
# armcap.c queries only CPU instruction support/brand to select crypto
|
||||
# implementations. This is not boot time, disk space or fingerprinting.
|
||||
member = ""
|
||||
for line in symbols.splitlines():
|
||||
if line.endswith(":"):
|
||||
member = line
|
||||
elif "_sysctlbyname" in line and member != "libcrypto-lib-armcap.o:":
|
||||
raise ValueError(f"Unreviewed OpenSSL sysctl caller: {member}")
|
||||
# getrandom uses Darwin's getentropy via OpenSSL's existing DSO lookup;
|
||||
# the random device fallback is excluded at compile time.
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
files = {
|
||||
"OpenSSL": binary.read_bytes(),
|
||||
"PrivacyInfo.xcprivacy": PRIVACY.read_bytes(),
|
||||
"Info.plist": plistlib.dumps({
|
||||
"CFBundleIdentifier": "cn.crossdesk.OpenSSL",
|
||||
"CFBundleName": "OpenSSL",
|
||||
"CFBundleExecutable": "OpenSSL",
|
||||
"CFBundlePackageType": "FMWK",
|
||||
"CFBundleInfoDictionaryVersion": "6.0",
|
||||
"CFBundleVersion": component["version"],
|
||||
"CFBundleShortVersionString": component["version"],
|
||||
"CFBundleSupportedPlatforms": ["iPhoneOS"],
|
||||
"MinimumOSVersion": "16.0",
|
||||
}),
|
||||
"LICENSE.txt": "\n\n".join(d["text"] for d in component["documents"]).encode(),
|
||||
"BuildMetadata.json": (json.dumps({
|
||||
"version": component["version"],
|
||||
"sourceURL": component["sourceURL"],
|
||||
"buildSourceURL": component["buildSourceURL"],
|
||||
"archives": {name: sha for name, (_, sha) in sorted(archives.items())},
|
||||
"privacySHA256": hashlib.sha256(PRIVACY.read_bytes()).hexdigest(),
|
||||
}, indent=2) + "\n").encode(),
|
||||
}
|
||||
for name, data in files.items():
|
||||
target = output / name
|
||||
if not target.exists() or target.read_bytes() != data:
|
||||
target.write_bytes(data)
|
||||
print(f"Packaged source-built OpenSSL {component['version']}: {output}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--inputs", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
package(args.inputs, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError, KeyError, StopIteration, subprocess.CalledProcessError) as error:
|
||||
print(f"OpenSSL framework: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record the source corresponding to an iOS build without changing the checkout."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
REPOSITORY_URL = "https://github.com/kunkundi/crossdesk"
|
||||
MINIRTC_URL = "https://github.com/kunkundi/minirtc"
|
||||
|
||||
|
||||
def git(*args, root=REPO):
|
||||
return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip()
|
||||
|
||||
|
||||
def source_metadata(require_release=False, tag=None):
|
||||
revision = git("rev-parse", "HEAD")
|
||||
dirty = bool(git("status", "--porcelain", "--untracked-files=normal", "--ignore-submodules=none"))
|
||||
tags = git("tag", "--points-at", "HEAD").splitlines()
|
||||
if tag:
|
||||
if tag not in tags:
|
||||
raise ValueError(f"Source tag {tag!r} does not point to the build commit")
|
||||
else:
|
||||
# 'latest' is a moving alias, never a corresponding-source identifier.
|
||||
versions = sorted(t for t in tags if t.startswith("v"))
|
||||
tag = versions[0] if versions else None
|
||||
if require_release and (dirty or not tag or not tag.startswith("v")):
|
||||
raise ValueError("Release source must be a clean checkout of a version tag, with matching submodules")
|
||||
mini_revision = git("rev-parse", "HEAD", root=REPO / "deps/submodules/minirtc")
|
||||
lock_path = REPO / "apps/ios/licenses/sources.json"
|
||||
lock = json.loads(lock_path.read_text())
|
||||
ref = quote(tag if tag and not dirty else revision, safe="")
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"repositoryURL": REPOSITORY_URL,
|
||||
"revision": revision,
|
||||
"tag": tag,
|
||||
"isModified": dirty,
|
||||
"sourceURL": f"{REPOSITORY_URL}/tree/{ref}",
|
||||
"buildInstructionsURL": f"{REPOSITORY_URL}/blob/{ref}/apps/ios/README.md",
|
||||
"privacyPolicyURL": f"{REPOSITORY_URL}/blob/{ref}/PRIVACY.md",
|
||||
"miniRTCRevision": mini_revision,
|
||||
"miniRTCSourceURL": f"{MINIRTC_URL}/tree/{mini_revision}",
|
||||
"licenseCatalogSHA256": lock["bundle_sha256"],
|
||||
"sourceCatalogSHA256": hashlib.sha256(lock_path.read_bytes()).hexdigest(),
|
||||
"toolchain": lock["toolchain"],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--tag", default=os.environ.get("CROSSDESK_SOURCE_TAG") or None)
|
||||
parser.add_argument("--require-release", action="store_true",
|
||||
default=os.environ.get("CROSSDESK_RELEASE_BUILD") == "YES")
|
||||
args = parser.parse_args()
|
||||
metadata = source_metadata(args.require_release, args.tag)
|
||||
data = (json.dumps(metadata, ensure_ascii=False, indent=2) + "\n").encode()
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not args.output.exists() or args.output.read_bytes() != data:
|
||||
args.output.write_bytes(data)
|
||||
print(f"Recorded iOS source: {metadata['revision']} (modified: {metadata['isModified']})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError, subprocess.CalledProcessError) as error:
|
||||
print(f"iOS source metadata: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check release resources and known restricted imports in a built iOS app."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def verify(app, require_release=False):
|
||||
info = plistlib.loads((app / "Info.plist").read_bytes())
|
||||
binary = app / info["CFBundleExecutable"]
|
||||
subprocess.run(["xcrun", "lipo", str(binary), "-verify_arch", "arm64"], check=True)
|
||||
symbols = subprocess.check_output(["xcrun", "nm", "-u", str(binary)], text=True)
|
||||
imports = set(line.split()[-1] for line in symbols.splitlines() if line.strip())
|
||||
disk_apis = {"_statfs", "_fstatfs", "_statvfs", "_fstatvfs", "_getfsstat"}
|
||||
if imports & disk_apis:
|
||||
raise ValueError(f"Unreviewed disk-space APIs remain in iOS app: {sorted(imports & disk_apis)}")
|
||||
defined = subprocess.check_output(["xcrun", "nm", "-g", str(binary)], text=True)
|
||||
if re.search(r"\b_Wels(?:CreateDecoder|CreateSVCEncoder|GetCodecVersion)\b", defined):
|
||||
raise ValueError("OpenH264 must not be present in the iOS app")
|
||||
if re.search(r"\b_of_(?:hweight\w*|hw8table|popcount_3)\b", defined):
|
||||
raise ValueError("OpenFEC's optional CC-BY-SA-3.0 hamming-weight code requires a separate distribution review")
|
||||
privacy = plistlib.loads((app / "PrivacyInfo.xcprivacy").read_bytes())
|
||||
reasons = {entry["NSPrivacyAccessedAPIType"]: entry["NSPrivacyAccessedAPITypeReasons"]
|
||||
for entry in privacy["NSPrivacyAccessedAPITypes"]}
|
||||
required = {
|
||||
"NSPrivacyAccessedAPICategoryUserDefaults": {"CA92.1"},
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp": {"C617.1", "3B52.1"},
|
||||
"NSPrivacyAccessedAPICategorySystemBootTime": {"35F9.1", "8FFB.1"},
|
||||
}
|
||||
for category, codes in required.items():
|
||||
if set(reasons.get(category, [])) != codes:
|
||||
raise ValueError(f"Missing or changed reviewed privacy reasons: {category}")
|
||||
data = (app / "ThirdPartyLicenses.json").read_bytes()
|
||||
catalog = json.loads(data)
|
||||
ids = {c["id"] for c in catalog["components"]}
|
||||
if not {"crossdesk", "minirtc", "openfec"} <= ids or "openh264" in ids:
|
||||
raise ValueError("Unexpected iOS license catalog")
|
||||
openssl = next(c for c in catalog["components"] if c["id"] == "openssl3")
|
||||
framework = app / "Frameworks/OpenSSL.framework"
|
||||
sdk_info = plistlib.loads((framework / "Info.plist").read_bytes())
|
||||
sdk_privacy_data = (framework / "PrivacyInfo.xcprivacy").read_bytes()
|
||||
sdk_privacy = plistlib.loads(sdk_privacy_data)
|
||||
expected_privacy = {
|
||||
"NSPrivacyTracking": False,
|
||||
"NSPrivacyTrackingDomains": [],
|
||||
"NSPrivacyCollectedDataTypes": [],
|
||||
"NSPrivacyAccessedAPITypes": [],
|
||||
}
|
||||
if sdk_privacy != expected_privacy:
|
||||
raise ValueError("OpenSSL SDK privacy declaration differs from the reviewed iOS configuration")
|
||||
sdk_build = json.loads((framework / "BuildMetadata.json").read_text())
|
||||
if (sdk_info["CFBundleShortVersionString"] != openssl["version"]
|
||||
or sdk_build["version"] != openssl["version"]
|
||||
or sdk_build["sourceURL"] != openssl["sourceURL"]
|
||||
or sdk_build["buildSourceURL"] != openssl["buildSourceURL"]
|
||||
or sdk_build["privacySHA256"] != hashlib.sha256(sdk_privacy_data).hexdigest()):
|
||||
raise ValueError("OpenSSL SDK resources do not match the source/license catalog")
|
||||
if (framework / "LICENSE.txt").read_text() != "\n\n".join(d["text"] for d in openssl["documents"]):
|
||||
raise ValueError("OpenSSL SDK license resource differs from its source notices")
|
||||
embedded_binary = framework / sdk_info["CFBundleExecutable"]
|
||||
# Xcode removes the archive and may generate a small codeless-framework
|
||||
# stub. It must not ship a second static archive or dynamic OpenSSL copy.
|
||||
if embedded_binary.exists():
|
||||
if embedded_binary.read_bytes().startswith(b"!<arch>\n"):
|
||||
raise ValueError("Xcode must remove the static OpenSSL archive when embedding its resources")
|
||||
sdk_symbols = subprocess.check_output(["xcrun", "nm", "-gU", str(embedded_binary)], text=True)
|
||||
if re.search(r"\b_(?:SSL_new|OPENSSL_init_crypto)\b", sdk_symbols):
|
||||
raise ValueError("Unexpected dynamic OpenSSL implementation in resource framework")
|
||||
linked = subprocess.check_output(["xcrun", "otool", "-L", str(binary)], text=True)
|
||||
if "OpenSSL.framework" in linked:
|
||||
raise ValueError("OpenSSL must be statically linked into the application")
|
||||
source = json.loads((app / "SourceMetadata.json").read_text())
|
||||
if source["licenseCatalogSHA256"] != hashlib.sha256(data).hexdigest():
|
||||
raise ValueError("Bundled licenses differ from the recorded source metadata")
|
||||
if require_release and (source["isModified"] or not source["tag"]):
|
||||
raise ValueError("The app was not built from an unmodified version tag")
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", source["revision"]):
|
||||
raise ValueError("Invalid source revision")
|
||||
print("Verified arm64 app, source/license resources, app privacy declarations and OpenSSL SDK privacy packaging")
|
||||
print("Signing, App Store validation and license authorization require separate review.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("app", type=Path)
|
||||
parser.add_argument("--require-release", action="store_true")
|
||||
args = parser.parse_args()
|
||||
verify(args.app, args.require_release)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
|
||||
print(f"iOS bundle verification: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: 74551f0629...6286ae6c1c
Reference in New Issue
Block a user