mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[fix] decouple iOS license checks from minirtc revisions
This commit is contained in:
@@ -422,6 +422,11 @@ jobs:
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Verify iOS license catalog
|
||||
run: |
|
||||
python3 -B -m unittest discover -s apps/ios/tests -p 'third_party_licenses_test.py' -v
|
||||
python3 -B apps/ios/scripts/generate_third_party_licenses.py
|
||||
|
||||
- name: Install xmake
|
||||
uses: xmake-io/github-action-setup-xmake@v1
|
||||
with:
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/zsh;
|
||||
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";
|
||||
shellScript = "set -e\npython3 \"${PROJECT_DIR}/scripts/generate_third_party_licenses.py\"\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 */
|
||||
|
||||
|
||||
@@ -20,7 +20,19 @@ private struct LicensedComponent: Decodable, Identifiable {
|
||||
if id == "crossdesk" {
|
||||
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
|
||||
}
|
||||
return version.count == 40 ? String(version.prefix(12)) : version
|
||||
let currentVersion = SourceMetadata.bundled?.componentSources[id]?.version ?? version
|
||||
return currentVersion.count == 40 ? String(currentVersion.prefix(12)) : currentVersion
|
||||
}
|
||||
|
||||
var resolvedSourceURL: URL {
|
||||
if id == "crossdesk" {
|
||||
return SourceMetadata.bundled?.sourceURL ?? sourceURL
|
||||
}
|
||||
return SourceMetadata.bundled?.componentSources[id]?.sourceURL ?? sourceURL
|
||||
}
|
||||
|
||||
var resolvedBuildSourceURL: URL? {
|
||||
SourceMetadata.bundled?.componentSources[id]?.buildSourceURL ?? buildSourceURL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +52,12 @@ private struct LicenseCatalog: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ComponentSourceMetadata: Decodable {
|
||||
let version: String?
|
||||
let sourceURL: URL?
|
||||
let buildSourceURL: URL?
|
||||
}
|
||||
|
||||
struct SourceMetadata: Decodable {
|
||||
let revision: String
|
||||
let tag: String?
|
||||
@@ -47,6 +65,7 @@ struct SourceMetadata: Decodable {
|
||||
let sourceURL: URL
|
||||
let buildInstructionsURL: URL
|
||||
let privacyPolicyURL: URL
|
||||
let componentSources: [String: ComponentSourceMetadata]
|
||||
|
||||
static let bundled: SourceMetadata? = {
|
||||
guard let url = Bundle.main.url(forResource: "SourceMetadata", withExtension: "json"),
|
||||
@@ -137,10 +156,8 @@ private struct ComponentLicenseView: View {
|
||||
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: component.resolvedSourceURL)
|
||||
if let buildSourceURL = component.resolvedBuildSourceURL {
|
||||
Link("构建配方与项目补丁", destination: buildSourceURL)
|
||||
}
|
||||
}
|
||||
|
||||
+21
-12
@@ -98,8 +98,8 @@ 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.
|
||||
`licenses/sources.json` pins the reviewed source archives (SHA-256), notice-review
|
||||
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,
|
||||
@@ -122,13 +122,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 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.
|
||||
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.
|
||||
|
||||
The source screen links to the exact application tag/commit, MiniRTC commit,
|
||||
and custom dependency recipes/patches. `SourceMetadata.json` is generated in
|
||||
@@ -136,7 +139,12 @@ 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.
|
||||
notice. `ThirdPartySources.json` preserves the notice-review catalog;
|
||||
`SourceMetadata.json` records the actual build commit and per-component version,
|
||||
source and recipe links used by the license screen, including MiniRTC, inih and
|
||||
WebRTC-derived code. These links update automatically on every build without
|
||||
rewriting the checked-in license resource. 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.
|
||||
|
||||
@@ -144,8 +152,9 @@ 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
|
||||
update recipe hashes, 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
|
||||
modified build; the maintainer's signing credentials are not required.
|
||||
|
||||
|
||||
@@ -178,11 +178,9 @@ def verify(lock, inputs, target_info, xmake_repository=None, xmake_version=None)
|
||||
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")
|
||||
# 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():
|
||||
|
||||
@@ -19,6 +19,24 @@ def git(*args, root=REPO):
|
||||
return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip()
|
||||
|
||||
|
||||
def component_sources(lock, mini_revision):
|
||||
"""Resolve MiniRTC source and recipe links without rewriting reviewed notices."""
|
||||
mini_url = f"{MINIRTC_URL}/tree/{mini_revision}"
|
||||
sources = {}
|
||||
for component in lock["components"]:
|
||||
source = component["source"]
|
||||
entry = {}
|
||||
if source["kind"] == "repository" and source["url"] == MINIRTC_URL:
|
||||
entry.update(version=mini_revision, sourceURL=mini_url)
|
||||
recipe = component.get("recipe_path", "")
|
||||
if recipe.startswith("deps/submodules/minirtc/"):
|
||||
directory = Path(recipe).relative_to("deps/submodules/minirtc").parent
|
||||
entry["buildSourceURL"] = f"{mini_url}/{directory.as_posix()}"
|
||||
if entry:
|
||||
sources[component["id"]] = entry
|
||||
return sources
|
||||
|
||||
|
||||
def source_metadata(require_release=False, tag=None):
|
||||
revision = git("rev-parse", "HEAD")
|
||||
dirty = bool(git("status", "--porcelain", "--untracked-files=normal", "--ignore-submodules=none"))
|
||||
@@ -47,6 +65,7 @@ def source_metadata(require_release=False, tag=None):
|
||||
"privacyPolicyURL": f"{REPOSITORY_URL}/blob/{ref}/PRIVACY.md",
|
||||
"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"],
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Regression coverage for notice validation and build-specific source links."""
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def load_script(name):
|
||||
path = Path(__file__).resolve().parents[1] / "scripts" / f"{name}.py"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
licenses = load_script("generate_third_party_licenses")
|
||||
metadata = load_script("prepare_source_metadata")
|
||||
|
||||
|
||||
class LicenseValidationTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory(prefix="crossdesk-ios-licenses-")
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
self.mini = self.root / "deps/submodules/minirtc"
|
||||
self.mini.mkdir(parents=True)
|
||||
self.notice = self.mini / "LICENSE"
|
||||
self.notice.write_text("Reviewed license text\n")
|
||||
self.recipe = self.mini / "thirdparty/opus/xmake.lua"
|
||||
self.recipe.parent.mkdir(parents=True)
|
||||
self.recipe.write_text('package("opus")\n')
|
||||
self.source = self.mini / "transport.cpp"
|
||||
self.source.write_text("int transport() { return 1; }\n")
|
||||
self.git("init", "-q")
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-qm", "Reviewed source")
|
||||
self.reviewed_revision = self.git("rev-parse", "HEAD")
|
||||
|
||||
self.bundle = self.root / "ThirdPartyLicenses.json"
|
||||
self.bundle.write_text('{"schemaVersion": 1, "components": []}\n')
|
||||
self.archive = self.root / "packages/o/opus/1.0/abcd/lib/libopus.a"
|
||||
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.lock = {
|
||||
"bundle_sha256": licenses.digest(self.bundle.read_bytes()),
|
||||
"toolchain": {"xmake_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())}],
|
||||
},
|
||||
{
|
||||
"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(
|
||||
["git", "-C", str(self.mini), "-c", "user.name=Test",
|
||||
"-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 test_source_only_commit_keeps_reviewed_catalog_and_uses_actual_links(self):
|
||||
self.source.write_text("int transport() { return 2; }\n")
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-qm", "Change transport implementation")
|
||||
current = self.git("rev-parse", "HEAD")
|
||||
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)
|
||||
self.assertEqual(sources["minirtc"]["sourceURL"], f"{metadata.MINIRTC_URL}/tree/{current}")
|
||||
self.assertEqual(sources["opus"]["buildSourceURL"],
|
||||
f"{metadata.MINIRTC_URL}/tree/{current}/thirdparty/opus")
|
||||
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_changed_native_archive_is_rejected(self):
|
||||
self.archive.write_bytes(b"changed archive")
|
||||
with self.assertRaisesRegex(ValueError, "Source checksum mismatch: .*libopus.a"):
|
||||
self.verify()
|
||||
|
||||
def test_unreviewed_toolchain_is_rejected(self):
|
||||
version = self.root / "xmake-version.txt"
|
||||
version.write_text("xmake v3.1.2\n")
|
||||
with self.assertRaisesRegex(ValueError, "Xmake version differs"):
|
||||
self.verify(xmake_version=version)
|
||||
|
||||
|
||||
class ComponentSourceTest(unittest.TestCase):
|
||||
def test_all_minirtc_components_and_custom_recipes_follow_build_revision(self):
|
||||
lock_path = Path(__file__).resolve().parents[1] / "licenses/sources.json"
|
||||
lock = json.loads(lock_path.read_text())
|
||||
reviewed = copy.deepcopy(lock)
|
||||
revision = "a" * 40
|
||||
base = f"{metadata.MINIRTC_URL}/tree/{revision}"
|
||||
sources = metadata.component_sources(lock, revision)
|
||||
for name in ("minirtc", "inih", "webrtc"):
|
||||
self.assertEqual(sources[name], {"version": revision, "sourceURL": base})
|
||||
self.assertEqual(sources["openssl3"], {"buildSourceURL": base + "/thirdparty/openssl"})
|
||||
self.assertNotIn("asio", sources)
|
||||
self.assertNotIn("crossdesk", sources)
|
||||
self.assertEqual(lock, reviewed)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user