diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 34886671..c7401f64 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -36,7 +36,6 @@ on: - "apps/android/**" - "apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json" - "apps/ios/licenses/**" - - "apps/ios/scripts/generate_third_party_licenses.py" - "deps/submodules/minirtc" - "libs/wire/**" @@ -155,9 +154,6 @@ jobs: cache-provider: basic cache-read-only: ${{ github.event_name == 'pull_request' }} - - name: Verify shared license catalog - run: python3 -B apps/ios/scripts/generate_third_party_licenses.py - - name: Resolve Android version shell: python env: diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index fd922c7e..62fae770 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -169,9 +169,6 @@ jobs: with: python-version: "3.13" - - name: Verify iOS license catalog - run: python3 -B apps/ios/scripts/generate_third_party_licenses.py - - name: Install xmake uses: xmake-io/github-action-setup-xmake@v1 with: @@ -181,7 +178,7 @@ jobs: shell: bash run: | xmake --version | tee "${RUNNER_TEMP}/xmake-version.txt" - python3 -B apps/ios/scripts/generate_third_party_licenses.py \ + python3 -B apps/ios/scripts/verify_native_build.py \ --xmake-version "${RUNNER_TEMP}/xmake-version.txt" - name: Pin xmake package repository diff --git a/apps/android/README.md b/apps/android/README.md index 7c78d44f..f45eee51 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -246,4 +246,4 @@ AV1 硬件路径尚无对应实机验证;Retroid 上使用 dav1d 软件解码 设置 → 关于使用与 iOS 相同的分组及入口:版本、软件许可、开源组件、源码与构建说明。软件许可中可分别阅读 GNU GPL 第 3 版及开源软件权利说明;开源组件页单独展示 OpenFEC 告知,其余组件列出名称和许可证,详情包含版本、源码、适用的构建配方和每一份许可文档。内容按 Android 构建生成,不显示未使用的 iOS 依赖,Android 签名说明替换 Apple 签名说明。 -APK 保留完整 `THIRD_PARTY_NOTICES.txt`,并生成轻量的 `ThirdPartyLicenses.json` 索引、72 份独立许可文档及 `SourceMetadata.json`。打开文档时才读取正文,长文按有限长度分块显示,支持文本选择、复制与网页链接;打包验证核对每份正文的 SHA-256。源码页面显示标签或提交版本及未发布修改提示,源码链接指向基础提交。当前 Android 构建说明、构建脚本、MiniRTC、OpenSSL 及 SVT-AV1 配方随 APK 离线提供,避免开发版链接指向基础提交中尚不存在的 Android 文件。 +APK 保留完整 `THIRD_PARTY_NOTICES.txt`,并生成轻量的 `ThirdPartyLicenses.json` 索引、72 份独立许可文档及 `SourceMetadata.json`。打开文档时才读取正文,长文按有限长度分块显示,支持文本选择、复制与网页链接。构建继续生成并打包许可资料,CI 和 APK 验证不再检查许可证内容、哈希或组件清单。源码页面显示标签或提交版本及未发布修改提示,源码链接指向基础提交。当前 Android 构建说明、构建脚本、MiniRTC、OpenSSL 及 SVT-AV1 配方随 APK 离线提供,避免开发版链接指向基础提交中尚不存在的 Android 文件。 diff --git a/apps/android/scripts/generate_notices.py b/apps/android/scripts/generate_notices.py index fc5c89b0..33c38aa6 100644 --- a/apps/android/scripts/generate_notices.py +++ b/apps/android/scripts/generate_notices.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """Package existing reviewed notices for the Android runtime dependency set.""" import json -import hashlib import os from pathlib import Path import subprocess @@ -70,8 +69,7 @@ def write_pages(output, catalog, additional, revision, minirtc): target = output / asset target.parent.mkdir(parents=True, exist_ok=True) target.write_text(document['text'], encoding='utf-8') - documents.append({'title': document['title'], 'asset': asset, - 'sha256': hashlib.sha256(document['text'].encode('utf-8')).hexdigest()}) + documents.append({'title': document['title'], 'asset': asset}) component['documents'] = documents (output / 'ThirdPartyLicenses.json').write_text(json.dumps( {'schemaVersion': 1, 'components': components}, ensure_ascii=False, indent=2)+'\n', encoding='utf-8') diff --git a/apps/android/test/verify_apk.py b/apps/android/test/verify_apk.py index d6bb79c2..0624e1e1 100644 --- a/apps/android/test/verify_apk.py +++ b/apps/android/test/verify_apk.py @@ -2,7 +2,6 @@ """Check the shipped native ABI, ELF load alignment and APK zip alignment.""" from pathlib import Path import argparse -import hashlib import json import re import struct @@ -40,33 +39,15 @@ def verify(path): name, extra = struct.unpack('-.zip`, @@ -366,16 +366,16 @@ 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, local license files and -package recipes offline before compiling native code. After compilation they -also compare all resolved dependency versions (including header-only packages) -with the catalog and check the merged archive's input hashes and pinned -toolchain. A missing, stale or altered catalog fails the build. Ordinary MiniRTC -code changes do not require changing the notice-review revision or regenerating -the catalog. Changes to dependencies, license texts or embedded third-party -notices still require reviewing and updating the affected catalog entries; -regeneration alone does not select a new dependency version. -`MINIRTC_ENABLE_AOM=true` also requires a reviewed AOM entry. +Builds package the license resources without validating their contents, hashes +or agreement with resolved dependency versions. License generation is a manual +maintenance step and is not run by CI or Xcode. Update the catalog and regenerate +the resource when the notices need to change; regeneration does not select a +new dependency version. + +`scripts/verify_native_build.py` independently checks the native archive input +hashes and pinned Xmake toolchain after compilation. These checks do not read +the license catalog. Downloaded source archives retain their SHA-256 checks +when manually regenerating notices. The source screen links to the exact application tag/commit, MiniRTC commit, and custom dependency recipes/patches. `SourceMetadata.json` is generated in @@ -395,8 +395,8 @@ 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, and commit MiniRTC. When the notice catalog changes, +package) so Xmake does not reuse a previous binary. Update affected notices +and commit MiniRTC. When the notice catalog changes, record the reviewed revision in `sources.json` and regenerate the catalog. Build the application again to relink it with the modified library. Use your own team and bundle identifier to install the diff --git a/apps/ios/licenses/sources.json b/apps/ios/licenses/sources.json index b2aa9258..26f0b689 100644 --- a/apps/ios/licenses/sources.json +++ b/apps/ios/licenses/sources.json @@ -14,15 +14,12 @@ }, "documents": [ { - "path": "asio/COPYING", - "sha256": "5cc69ca9afd3417ed8e58bcd9c0bb730375e9e3e92b5aaf6483b93a2837e0cac" + "path": "asio/COPYING" }, { - "path": "asio/LICENSE_1_0.txt", - "sha256": "c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566" + "path": "asio/LICENSE_1_0.txt" } ], - "recipe_sha256": "52bddc12cf0837117e935ea5d71aa5c35e7fd6ae802aa7401c6dba598cae0989", "recipe_path": "xmake-repo/packages/a/asio/xmake.lua", "package": "asio", "notice_prefixes": [ @@ -41,11 +38,9 @@ }, "documents": [ { - "path": "LICENSE.md", - "sha256": "22331571e5e3dbe4c79ad715e23127bdf3beabd75052c953ddabf191ef799e14" + "path": "LICENSE.md" } ], - "recipe_sha256": "23c69dc09488527b0d6adf1d25f795a9f0afdc7a1d594c44603c2dd07162faa4", "recipe_path": "xmake-repo/packages/c/concurrentqueue/xmake.lua", "package": "concurrentqueue", "notice_prefixes": [ @@ -66,12 +61,10 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986" + "path": "LICENSE" }, { - "path": "apps/ios/licenses/OPEN_SOURCE_NOTICE.txt", - "sha256": "34720d126e6373603d0c1802a8c3276802b93f2a8fbbf2b61837c6fdefea8877" + "path": "apps/ios/licenses/OPEN_SOURCE_NOTICE.txt" } ] }, @@ -88,12 +81,10 @@ }, "documents": [ { - "path": "COPYING", - "sha256": "b327887de263238deaa80c34cdd2ff3e0ba1d35db585ce14a37ce3e74ee389e9" + "path": "COPYING" }, { - "path": "doc/PATENTS", - "sha256": "335eca574598bf4ca181b12f708d6669e5a5e78c8e1513e5b35fa1f03901484b" + "path": "doc/PATENTS" } ], "package": "dav1d", @@ -114,39 +105,30 @@ }, "documents": [ { - "path": "COPYING", - "sha256": "fa6f36630bb1e0c571d34b2bbdf188d08495c9dbf58f28cac112f303fc1f58fb" + "path": "COPYING" }, { - "path": "LICENSES/Apache-2.0.txt", - "sha256": "074e6e32c86a4c0ef8b3ed25b721ca23aca83df277cd88106ef7177c354615ff" + "path": "LICENSES/Apache-2.0.txt" }, { - "path": "LICENSES/CC0-1.0.txt", - "sha256": "a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499" + "path": "LICENSES/CC0-1.0.txt" }, { - "path": "LICENSES/LGPL-2.1-only.txt", - "sha256": "fa6f36630bb1e0c571d34b2bbdf188d08495c9dbf58f28cac112f303fc1f58fb" + "path": "LICENSES/LGPL-2.1-only.txt" }, { - "path": "LICENSES/LLVM-exception.txt", - "sha256": "e34c58338bd89d43e709e226610d8f32b3e3c47f4ad9a99a8dc1d4ac7842488e" + "path": "LICENSES/LLVM-exception.txt" }, { - "path": "LICENSES/MIT.txt", - "sha256": "b85dcd3e453d05982552c52b5fc9e0bdd6d23c6f8e844b984a88af32570b0cc0" + "path": "LICENSES/MIT.txt" }, { - "path": "LICENSES/MPL-1.1.txt", - "sha256": "6214f8b1300bb9f37b16ebc146f8f61af0187e0025042ff95c8b3030744a8795" + "path": "LICENSES/MPL-1.1.txt" }, { - "path": "subprojects/gvdb/COPYING", - "sha256": "bdbcdaa7858872393e9d6b216bbdc8a23e99d98dd6cdb7cda19458025b9e19fc" + "path": "subprojects/gvdb/COPYING" } ], - "recipe_sha256": "650e2f9a1c4b5079694a5626cfcb625c650a938f32878cf0caab3b0cf58216b9", "recipe_path": "deps/submodules/minirtc/thirdparty/glib/xmake.lua", "package": "glib", "notice_prefixes": [ @@ -169,8 +151,7 @@ }, "documents": [ { - "path": "deps/submodules/minirtc/src/inih/LICENSE", - "sha256": "bd2a675a5a26fbff77f171f3a15afb1a9c7855be1a01c7e3fd16b8d2617acdda" + "path": "deps/submodules/minirtc/src/inih/LICENSE" } ] }, @@ -186,11 +167,9 @@ }, "documents": [ { - "path": "LICENSE.MIT", - "sha256": "86b998c792894ccb911a1cb7994f7a9652894e7a094c0b5e45be2f553f45cf14" + "path": "LICENSE.MIT" } ], - "recipe_sha256": "eadd211f0521a18207e9544c0c5a08c41dca578fce99df5c2577f6fcf2a0fdce", "recipe_path": "xmake-repo/packages/n/nlohmann_json/xmake.lua", "package": "nlohmann_json", "notice_prefixes": [ @@ -209,11 +188,9 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "6587fd02a924b97dbd6758b0573ddec056fb494a84f3709c5aaa33ce1f7f029c" + "path": "LICENSE" } ], - "recipe_sha256": "f86cac2c4b884f3347a39a59f3dd8de566c257c7fd481c6e49b05f4730f12c4f", "recipe_path": "xmake-repo/packages/k/kcp/xmake.lua", "package": "kcp", "notice_prefixes": [ @@ -233,11 +210,9 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85" + "path": "LICENSE" } ], - "recipe_sha256": "b3ec15652a47d65e0a1f85f62d02c098c5cf5b8d8c3cf7b03ff099efa7016326", "recipe_path": "deps/submodules/minirtc/thirdparty/libdatachannel/xmake.lua", "package": "libdatachannel", "notice_prefixes": [ @@ -257,11 +232,9 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "67894089811f93fca47a76f85e017da6f8582d4ba0905963c6e0f1ad6df7a195" + "path": "LICENSE" } ], - "recipe_sha256": "de19ac4e3e91ef5715ac582c75e0d10404dc14ec4e33b2e764dc2637a78d24e9", "recipe_path": "xmake-repo/packages/l/libffi/xmake.lua", "package": "libffi", "notice_prefixes": [ @@ -281,23 +254,18 @@ }, "documents": [ { - "path": "COPYING", - "sha256": "e5504e5ffe894870ea1a652df957a120ead9c3f9a5e9d314ca41ba06f2d72024" + "path": "COPYING" }, { - "path": "COPYING.LGPL", - "sha256": "a9bdde5616ecdd1e980b44f360600ee8783b1f99b8cc83a2beb163a0a390e861" + "path": "COPYING.LGPL" }, { - "path": "COPYING.MPL", - "sha256": "3b3c75ae6c663c4f93987647eb5463d33dc1602a1aded3ac479773f2c3928e3a" + "path": "COPYING.MPL" }, { - "path": "AUTHORS", - "sha256": "d72eb6fa9d5e93abb11d13b6c6f04fda2ba183d0705e703887cda243f6f1eead" + "path": "AUTHORS" } ], - "recipe_sha256": "dbb3cd85fd197f6495c611a08f99b1c70b88c48c91f6e6ea24946a1b0701a3d5", "recipe_path": "deps/submodules/minirtc/thirdparty/libnice/xmake.lua", "package": "libnice", "notice_prefixes": [ @@ -320,11 +288,9 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "8e19d42a1eec9561f3f347253ddf2e385c55f392f025bb0fd41b88dbf38db5ae" + "path": "LICENSE" } ], - "recipe_sha256": "9629edb8d395ea3017af1702cafea57173ca0a6a1ea305a6174b1936598faec6", "recipe_path": "deps/submodules/minirtc/thirdparty/libsrtp/xmake.lua", "package": "libsrtp", "notice_prefixes": [ @@ -346,16 +312,13 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "2b2cc1180c7e6988328ad2033b04b80117419db9c4c584918bbb3cfec7e9364f" + "path": "LICENSE" }, { - "path": "PATENTS", - "sha256": "4da84211f947c6f9f7dd8fbce513e1522e01833407ae732b8c9a2bbb75fed495" + "path": "PATENTS" }, { - "path": "AUTHORS", - "sha256": "0c4a51c72a06b3431bb2b750e3b04dbe3694b22e2fcae6e370216424eec56d52" + "path": "AUTHORS" } ], "package": "libyuv", @@ -376,8 +339,7 @@ }, "documents": [ { - "path": "deps/submodules/minirtc/LICENSE", - "sha256": "ea8af5e789cb2d4e9b10bce3874982ade163b749b6bfbdb32e2df21c4d106de1" + "path": "deps/submodules/minirtc/LICENSE" } ] }, @@ -394,11 +356,9 @@ "documents": [ { "path": "LICENCE_CeCILL-C_V1-en.txt", - "sha256": "b48f5dc16f144cd79ddaadce3cc8ef183778a0b4f75ab4362e134b347ca20f3d", "encoding": "iso-8859-1" } ], - "recipe_sha256": "a9bc4a460195c77543a91c0a2b40a094f4a3869dc917e4eb98e7e8005ea441ab", "recipe_path": "deps/submodules/minirtc/thirdparty/openfec/xmake.lua", "package": "openfec", "notice_prefixes": [ @@ -422,11 +382,9 @@ }, "documents": [ { - "path": "LICENSE.txt", - "sha256": "7d5450cb2d142651b8afa315b5f238efc805dad827d91ba367d8516bc9d49e7a" + "path": "LICENSE.txt" } ], - "recipe_sha256": "230631f3cddc72674fa21d2639044b00120ee346ef186eddfb5dc04bf4df3ee9", "recipe_path": "deps/submodules/minirtc/thirdparty/openssl/xmake.lua", "package": "openssl3", "notice_prefixes": [ @@ -448,11 +406,9 @@ }, "documents": [ { - "path": "COPYING", - "sha256": "01e1167d54a096d123cf6dfbbeb19587278845c6481d2d66d545669846079551" + "path": "COPYING" } ], - "recipe_sha256": "c965643eb4e093858c27c402e6cde544620460f2169c89d8ff2bab01111d022b", "recipe_path": "xmake-repo/packages/l/libopus/xmake.lua", "package": "libopus", "notice_prefixes": [ @@ -475,19 +431,15 @@ }, "documents": [ { - "path": "LICENCE.md", - "sha256": "197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811" + "path": "LICENCE.md" }, { - "path": "COPYING", - "sha256": "99272c55f3dcfa07a8a7e15a5c1a33096e4727de74241d65fa049fccfdd59507" + "path": "COPYING" }, { - "path": "deps/sljit/LICENSE", - "sha256": "5f216505c0f6ea3273caec89e766eef93cdeb7bbb0c429f9360116d7c938feeb" + "path": "deps/sljit/LICENSE" } ], - "recipe_sha256": "821f1fbc91434f4324be88eeea754db09c5d7be31446242764861ca2aee136b7", "recipe_path": "xmake-repo/packages/p/pcre2/xmake.lua", "package": "pcre2", "notice_prefixes": [ @@ -508,11 +460,9 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "e4d01796524cbc13b1571a1f0914823c6a23b316ba039e7014f47a4eef7fd4c3" + "path": "LICENSE" } ], - "recipe_sha256": "deb525d09b5b79351eeb524ab3bc6a3009e434974e585e5e8852c491d7fa07c8", "recipe_path": "xmake-repo/packages/p/plog/xmake.lua", "package": "plog", "notice_prefixes": [ @@ -532,8 +482,7 @@ }, "documents": [ { - "path": "COPYING", - "sha256": "d245807f90032872d1438d741ed21e2490e1175dc8aa3afa5ddb6c8e529b58e5" + "path": "COPYING" } ], "bundled_by": "glib", @@ -555,11 +504,9 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "4ccecab18d1ff0b61174fe3d6c430541625d3ddb865b0d5887db296f883c76e7" + "path": "LICENSE" } ], - "recipe_sha256": "5b299effb366420b4920c5b89e6535b76173136d91ee01f8f7d3423486a93549", "recipe_path": "xmake-repo/packages/s/spdlog/xmake.lua", "package": "spdlog", "notice_prefixes": [ @@ -579,24 +526,19 @@ }, "documents": [ { - "path": "LICENSE.md", - "sha256": "0acc2fcb27472bdc9aaf8b71f37055bbdac4f54671b7d922f241bd7fcd0dd3e6" + "path": "LICENSE.md" }, { - "path": "LICENSE-BSD2.md", - "sha256": "c582fcbeb9cf972a60d298bfaf41dc5adff2dd14208946edcb9bb56a281e8438" + "path": "LICENSE-BSD2.md" }, { - "path": "PATENTS.md", - "sha256": "20678ab10402659106dc4c147c97b2a6e94b5c0695415e15a8f195ebc3547922" + "path": "PATENTS.md" }, { - "path": "third_party/fastfeat/LICENSE", - "sha256": "043dcfd059386f9facd376351b2bd79325778744aa442177390cdfcca54babed" + "path": "third_party/fastfeat/LICENSE" }, { - "path": "third_party/safestringlib/LICENSE", - "sha256": "c0f99a208799ca20c9ccf31a30533d49334eb458dca5a3d14f9afe3e884e5d7d" + "path": "third_party/safestringlib/LICENSE" } ], "package": "svt-av1", @@ -618,11 +560,9 @@ }, "documents": [ { - "path": "LICENSE.md", - "sha256": "fa53711b25af4b9a9b8dadfea3cb38166ec4b96760c8d62b284055554537d9ef" + "path": "LICENSE.md" } ], - "recipe_sha256": "e3dac7d1db8e5bc79d99851e2486371af19962a4b8fca6eaac3156c00eb73b92", "recipe_path": "xmake-repo/packages/u/usrsctp/xmake.lua", "package": "usrsctp", "notice_prefixes": [ @@ -641,12 +581,10 @@ }, "documents": [ { - "path": "deps/submodules/minirtc/thirdparty/webrtc/LICENSE", - "sha256": "ab00a482b6a3902e40211b43c5d0441962ea99b6cc7c25c0f243fa270b78d482" + "path": "deps/submodules/minirtc/thirdparty/webrtc/LICENSE" }, { - "path": "deps/submodules/minirtc/thirdparty/webrtc/PATENTS", - "sha256": "01462e2068d1a04c2274f3389773014c14ed9bc3446b28303543bd3e3c064145" + "path": "deps/submodules/minirtc/thirdparty/webrtc/PATENTS" } ] }, @@ -662,11 +600,9 @@ }, "documents": [ { - "path": "COPYING", - "sha256": "b31e597bc777d884a39663c11d0557a0af40780e1f451de11ea5103924a69667" + "path": "COPYING" } ], - "recipe_sha256": "beac245a717c2bc3bc8d17265d9fc9b54f699f3833b11513f8723c4dbaffd539", "recipe_path": "deps/submodules/minirtc/thirdparty/websocketpp/xmake.lua", "package": "websocketpp", "notice_prefixes": [ @@ -685,11 +621,9 @@ }, "documents": [ { - "path": "LICENSE", - "sha256": "e32ff4e00d9d94930537635291da39e7e612703334bf6fde8c7f1686fe8a45a2" + "path": "LICENSE" } ], - "recipe_sha256": "7832bcb086709a0739ae1abeff035f7a103c7169dc8af61ebf91dd887fcbd054", "recipe_path": "xmake-repo/packages/z/zlib/xmake.lua", "package": "zlib", "notice_prefixes": [ @@ -697,7 +631,6 @@ ] } ], - "bundle_sha256": "a27c89beaaee2397a306417ff133d7f48c74fe6db9041e145fc08f18ec69a080", "toolchain": { "xmake_min_version": "3.1.1", "xmake_repository_revision": "121109352da272cd1a3d4ed52f2dcda34f839955" diff --git a/apps/ios/scripts/generate_third_party_licenses.py b/apps/ios/scripts/generate_third_party_licenses.py index f47bed30..e745552e 100644 --- a/apps/ios/scripts/generate_third_party_licenses.py +++ b/apps/ios/scripts/generate_third_party_licenses.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Collect iOS notices from pinned source archives; validate them offline at build time.""" +"""Generate iOS notice resources from pinned source archives.""" import argparse from collections import defaultdict @@ -8,7 +8,6 @@ import hashlib import json from pathlib import Path import re -import subprocess import sys import tarfile import urllib.request @@ -82,7 +81,7 @@ def source_files(component, cache): def component_documents(component, names, read): documents = [] for item in component["documents"]: - data = checked(read(item["path"]), item["sha256"], item["path"]) + data = read(item["path"]) text = data.decode(item.get("encoding", "utf-8")) if "end_before" in item: if item["end_before"] not in text: @@ -134,79 +133,22 @@ def generate(lock, cache): 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: - minimum = lock["toolchain"]["xmake_min_version"] - version = re.search(r"xmake v(\d+)\.(\d+)\.(\d+)(?:\+|[,\s]|$)", xmake_version.read_text()) - if not version or tuple(map(int, version.groups())) < tuple(map(int, minimum.split("."))): - raise ValueError(f"Xmake {minimum} or newer is required; 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(?:-simulator)?/[^/\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) - # Repository revisions identify the reviewed notices, not a required build - # commit. SourceMetadata records the actual MiniRTC revision and links for - # this build; the content and dependency checks above detect stale notices. - - 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("--generate", action="store_true", required=True, + help="download 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") + data = generate(lock, args.cache) + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + OUTPUT.write_bytes(data) + print(f"Generated {len(lock['components'])} iOS license components") if __name__ == "__main__": try: main() - except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error: + except (OSError, ValueError, KeyError) as error: print(f"Third-party notices: {error}", file=sys.stderr) sys.exit(1) diff --git a/apps/ios/scripts/prepare_source_metadata.py b/apps/ios/scripts/prepare_source_metadata.py index a806a81a..d91f1eb0 100644 --- a/apps/ios/scripts/prepare_source_metadata.py +++ b/apps/ios/scripts/prepare_source_metadata.py @@ -2,7 +2,6 @@ """Record the source corresponding to an iOS build without changing the checkout.""" import argparse -import hashlib import json import os from pathlib import Path @@ -66,8 +65,6 @@ def source_metadata(require_release=False, tag=None): "miniRTCRevision": mini_revision, "miniRTCSourceURL": f"{MINIRTC_URL}/tree/{mini_revision}", "componentSources": component_sources(lock, mini_revision), - "licenseCatalogSHA256": lock["bundle_sha256"], - "sourceCatalogSHA256": hashlib.sha256(lock_path.read_bytes()).hexdigest(), "toolchain": lock["toolchain"], } diff --git a/apps/ios/scripts/verify_app_bundle.py b/apps/ios/scripts/verify_app_bundle.py index 573859ba..c6fa9cb3 100644 --- a/apps/ios/scripts/verify_app_bundle.py +++ b/apps/ios/scripts/verify_app_bundle.py @@ -38,12 +38,6 @@ def verify(app, require_release=False): 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() @@ -57,14 +51,9 @@ def verify(app, require_release=False): 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"] + if (sdk_info["CFBundleShortVersionString"] != sdk_build["version"] 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") + raise ValueError("OpenSSL SDK resources do not match its build metadata") 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. @@ -78,13 +67,11 @@ def verify(app, require_release=False): 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("Verified arm64 app, source metadata, app privacy declarations and OpenSSL SDK privacy packaging") print("Signing, App Store validation and license authorization require separate review.") diff --git a/apps/ios/scripts/verify_native_build.py b/apps/ios/scripts/verify_native_build.py new file mode 100644 index 00000000..c7e095f6 --- /dev/null +++ b/apps/ios/scripts/verify_native_build.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Verify iOS native build inputs and toolchain without consulting license data.""" + +import argparse +import hashlib +from pathlib import Path +import re +import subprocess +import sys + +XMAKE_MIN_VERSION = "3.1.1" +XMAKE_REPOSITORY_REVISION = "121109352da272cd1a3d4ed52f2dcda34f839955" + + +def verify(inputs=None, xmake_repository=None, xmake_version=None): + if xmake_version is not None: + version = re.search(r"xmake v(\d+)\.(\d+)\.(\d+)(?:\+|[,\s]|$)", xmake_version.read_text()) + if not version or tuple(map(int, version.groups())) < tuple(map(int, XMAKE_MIN_VERSION.split("."))): + raise ValueError(f"Xmake {XMAKE_MIN_VERSION} or newer is required; 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 != XMAKE_REPOSITORY_REVISION: + raise ValueError("Xmake package repository differs from the pinned iOS toolchain") + 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") + if inputs is not None: + for line in inputs.read_text().splitlines(): + sha, name = line.split(maxsplit=1) + if hashlib.sha256(Path(name).read_bytes()).hexdigest() != sha: + raise ValueError(f"Native input checksum mismatch: {name}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--inputs", type=Path, help="native archive .inputs.sha256 manifest") + parser.add_argument("--xmake-repository", type=Path, help="active pinned Xmake package repository") + parser.add_argument("--xmake-version", type=Path, help="Xmake version output recorded by the native build") + args = parser.parse_args() + if not any((args.inputs, args.xmake_repository, args.xmake_version)): + parser.error("provide --inputs, --xmake-repository or --xmake-version") + verify(args.inputs, args.xmake_repository, args.xmake_version) + print("Verified iOS native build inputs and toolchain") + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, subprocess.CalledProcessError) as error: + print(f"iOS native build: {error}", file=sys.stderr) + sys.exit(1) diff --git a/apps/ios/tests/third_party_licenses_test.py b/apps/ios/tests/third_party_licenses_test.py index 74d5ec90..9ddbae7b 100644 --- a/apps/ios/tests/third_party_licenses_test.py +++ b/apps/ios/tests/third_party_licenses_test.py @@ -1,13 +1,13 @@ -"""Regression coverage for notice validation and build-specific source links.""" +"""Regression coverage for native validation and build-specific source links.""" import copy +import hashlib import importlib.util import json from pathlib import Path import subprocess import tempfile import unittest -from unittest.mock import patch def load_script(name): @@ -18,11 +18,11 @@ def load_script(name): return module -licenses = load_script("generate_third_party_licenses") +native = load_script("verify_native_build") metadata = load_script("prepare_source_metadata") -class LicenseValidationTest(unittest.TestCase): +class NativeValidationTest(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory(prefix="crossdesk-ios-licenses-") self.addCleanup(temporary.cleanup) @@ -47,34 +47,24 @@ class LicenseValidationTest(unittest.TestCase): self.archive.parent.mkdir(parents=True) self.archive.write_bytes(b"reviewed archive") self.inputs = self.root / "inputs.sha256" - self.inputs.write_text(f"{licenses.digest(self.archive.read_bytes())} {self.archive}\n") - self.target = self.root / "target-info.txt" - self.target.write_text(str(self.root / "packages/a/asio/1.0/abcd/include") + "\n") + self.inputs.write_text(f"{hashlib.sha256(self.archive.read_bytes()).hexdigest()} {self.archive}\n") self.lock = { - "bundle_sha256": licenses.digest(self.bundle.read_bytes()), - "toolchain": {"xmake_min_version": "3.1.1"}, "components": [ { "id": "minirtc", "version": self.reviewed_revision, "source": {"kind": "repository", "url": metadata.MINIRTC_URL, "revision": self.reviewed_revision}, - "documents": [{"path": str(self.notice.relative_to(self.root)), - "sha256": licenses.digest(self.notice.read_bytes())}], + "documents": [{"path": str(self.notice.relative_to(self.root))}], }, { "id": "opus", "package": "opus", "version": "1.0", "source": {"kind": "archive", "url": "https://example.com/opus.tar.gz"}, "recipe_path": str(self.recipe.relative_to(self.root)), - "recipe_sha256": licenses.digest(self.recipe.read_bytes()), }, {"id": "asio", "package": "asio", "version": "1.0", "source": {"kind": "archive", "url": "https://example.com/asio.tar.gz"}}, ], } - for name, value in (("REPO", self.root), ("OUTPUT", self.bundle)): - patcher = patch.object(licenses, name, value) - patcher.start() - self.addCleanup(patcher.stop) def git(self, *args): return subprocess.check_output( @@ -82,9 +72,8 @@ class LicenseValidationTest(unittest.TestCase): "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], text=True).strip() - def verify(self, preflight=False, **kwargs): - licenses.verify(self.lock, None if preflight else self.inputs, - None if preflight else self.target, **kwargs) + def verify(self, **kwargs): + native.verify(self.inputs, **kwargs) def test_source_only_commit_keeps_reviewed_catalog_and_uses_actual_links(self): self.source.write_text("int transport() { return 2; }\n") @@ -94,7 +83,6 @@ class LicenseValidationTest(unittest.TestCase): self.assertNotEqual(current, self.reviewed_revision) original_lock = copy.deepcopy(self.lock) original_bundle = self.bundle.read_bytes() - self.verify(preflight=True) self.verify() sources = metadata.component_sources(self.lock, current) self.assertEqual(sources["minirtc"]["version"], current) @@ -104,49 +92,18 @@ class LicenseValidationTest(unittest.TestCase): self.assertEqual(self.lock, original_lock) self.assertEqual(self.bundle.read_bytes(), original_bundle) - def test_changed_license_is_rejected_before_compilation(self): - self.notice.write_text("Changed license text\n") - with self.assertRaisesRegex(ValueError, "Source checksum mismatch: .*LICENSE"): - self.verify(preflight=True) - - def test_changed_recipe_is_rejected_before_compilation(self): - self.recipe.write_text('package("different-opus")\n') - with self.assertRaisesRegex(ValueError, "Source checksum mismatch: .*xmake.lua"): - self.verify(preflight=True) - - def test_changed_bundle_is_rejected_before_compilation(self): - self.bundle.write_text("{}\n") - with self.assertRaisesRegex(ValueError, "Source checksum mismatch:"): - self.verify(preflight=True) - - def test_unreviewed_header_only_dependency_is_rejected(self): - self.target.write_text(str(self.root / "packages/a/asio/2.0/abcd/include") + "\n") - with self.assertRaisesRegex(ValueError, "Unreviewed iOS dependency: asio 2.0"): - self.verify() - - def test_missing_dependency_is_rejected(self): - self.target.write_text("") - with self.assertRaisesRegex(ValueError, "License/build dependency mismatch; missing:.*asio"): - self.verify() - - def test_simulator_cache_keeps_dependency_and_archive_validation(self): + def test_simulator_cache_keeps_archive_validation(self): (self.root / "packages").rename(self.root / "packages-simulator") self.archive = Path(str(self.archive).replace("/packages/", "/packages-simulator/")) self.inputs.write_text(self.inputs.read_text().replace("/packages/", "/packages-simulator/")) - self.target.write_text(self.target.read_text().replace("/packages/", "/packages-simulator/")) self.verify() - valid_target = self.target.read_text() - self.target.write_text(valid_target.replace("/asio/1.0/", "/asio/2.0/")) - with self.assertRaisesRegex(ValueError, "Unreviewed iOS dependency: asio 2.0"): - self.verify() - self.target.write_text(valid_target) self.archive.write_bytes(b"changed simulator archive") - with self.assertRaisesRegex(ValueError, "Source checksum mismatch: .*libopus.a"): + with self.assertRaisesRegex(ValueError, "Native input checksum mismatch: .*libopus.a"): self.verify() def test_changed_native_archive_is_rejected(self): self.archive.write_bytes(b"changed archive") - with self.assertRaisesRegex(ValueError, "Source checksum mismatch: .*libopus.a"): + with self.assertRaisesRegex(ValueError, "Native input checksum mismatch: .*libopus.a"): self.verify() def test_minimum_and_newer_xmake_versions_are_accepted(self):