mirror of
https://github.com/cfal/shoes.git
synced 2026-11-06 16:14:11 +00:00
* build: remove distributable mobile SDK wrappers Remove the Android Kotlin/Gradle wrapper, AAR and XCFramework packaging, and mobile SDK release publishing. Preserve Rust JNI/C embedding and native cross-compilation CI, with checks for library outputs and the generated C header. Document app-owned integration and retain desktop release-notice verification. * ci: verify mobile header after locked native builds cbindgen resolves Cargo metadata without --locked and can update Cargo.lock. Run it after the native builds so it cannot hide a stale lockfile from their checks. * docs: remove obsolete mobile wrapper examples Remove examples referencing nonexistent JNI wrapper methods and clarify the app-owned callback contract. Keep native mobile support unchanged. * docs: clarify native mobile CI coverage
150 lines
6.8 KiB
Python
150 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercises dependency-notice generation and release tarball verification."""
|
|
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
SPEC = importlib.util.spec_from_file_location(
|
|
"notices", Path(__file__).with_name("check-release-notices.py")
|
|
)
|
|
NOTICES = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(NOTICES)
|
|
|
|
GENERATOR_SPEC = importlib.util.spec_from_file_location(
|
|
"generate_notices", Path(__file__).with_name("generate-release-notices.py")
|
|
)
|
|
GENERATOR = importlib.util.module_from_spec(GENERATOR_SPEC)
|
|
GENERATOR_SPEC.loader.exec_module(GENERATOR)
|
|
|
|
# SPDX's ISC fallback names its publisher rather than the dependency's authors.
|
|
PLACEHOLDER_ATTRIBUTION = re.compile(
|
|
r"<(?:year|copyright holders?|owner)>|YEAR by AUTHOR EMAIL"
|
|
r"|Copyright \(c\) 2004-2010 by Internet Systems Consortium, Inc\.",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
class ReleaseNoticesTests(unittest.TestCase):
|
|
def test_generation_preserves_bundle_on_warning_or_failure(self):
|
|
for status, stderr in [(0, ""), (0, "WARN checksum mismatch"), (1, ""), (1, "failed")]:
|
|
with self.subTest(status=status, stderr=stderr), tempfile.TemporaryDirectory() as tmp:
|
|
output = Path(tmp) / "dependencies.txt"
|
|
output.write_text("original")
|
|
|
|
def run(command, **kwargs):
|
|
Path(command[-1]).write_text("generated")
|
|
return subprocess.CompletedProcess(command, status, stderr=stderr)
|
|
|
|
with mock.patch.object(GENERATOR.subprocess, "run", side_effect=run):
|
|
if status or stderr:
|
|
with self.assertRaises(RuntimeError):
|
|
GENERATOR.generate(output)
|
|
self.assertEqual(output.read_text(), "original")
|
|
else:
|
|
GENERATOR.generate(output)
|
|
self.assertEqual(output.read_text(), "generated")
|
|
|
|
def test_bundle_has_no_placeholder_attributions(self):
|
|
bundle = (NOTICES.ROOT / "licenses/dependencies.txt").read_text()
|
|
for block in bundle.split("\nLicense: ")[1:]:
|
|
heading = block.splitlines()[0]
|
|
if heading.endswith(("(MIT)", "(BSD-2-Clause)", "(BSD-3-Clause)", "(ISC)", "(0BSD)")):
|
|
with self.subTest(license=heading):
|
|
text = block.partition("\n\n")[2]
|
|
self.assertNotRegex(text, PLACEHOLDER_ATTRIBUTION)
|
|
|
|
def test_placeholder_detection_includes_spdx_isc_publisher(self):
|
|
for text in [
|
|
"Copyright (c) <year> <copyright holders>",
|
|
"Copyright (c) <year> <owner>",
|
|
"Copyright (C) YEAR by AUTHOR EMAIL",
|
|
'Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC")\n'
|
|
"Copyright (c) 1995-2003 by Internet Software Consortium",
|
|
]:
|
|
with self.subTest(text=text):
|
|
self.assertRegex(text, PLACEHOLDER_ATTRIBUTION)
|
|
|
|
def test_bundle_covers_production_dependency_graph(self):
|
|
cargo_options = [
|
|
"--locked", "--offline", "--all-features",
|
|
"--manifest-path", str(NOTICES.ROOT / "Cargo.toml"),
|
|
]
|
|
# Cargo tree excludes features enabled only by dev dependencies, unlike metadata.
|
|
tree = subprocess.check_output([
|
|
"cargo", "tree", *cargo_options, "--target", "all",
|
|
"--edges", "normal,build", "--prefix", "none", "--format", "{p}",
|
|
], text=True)
|
|
production = {tuple(line.split()[:2]) for line in tree.splitlines()}
|
|
metadata = json.loads(subprocess.check_output([
|
|
"cargo", "metadata", *cargo_options, "--format-version", "1",
|
|
]))
|
|
gotatun_versions = [p["version"] for p in metadata["packages"] if p["name"] == "gotatun"]
|
|
self.assertEqual(len(gotatun_versions), 1)
|
|
version = gotatun_versions[0]
|
|
source_notice = (NOTICES.ROOT / "THIRD_PARTY_NOTICES.md").read_text()
|
|
for required in [
|
|
f"GotaTun {version},",
|
|
f"https://crates.io/api/v1/crates/gotatun/{version}/download",
|
|
f"https://github.com/mullvad/gotatun/tree/v{version}",
|
|
]:
|
|
self.assertIn(required, source_notice)
|
|
bundle = (NOTICES.ROOT / "licenses/dependencies.txt").read_text()
|
|
original_notices = {
|
|
"curve25519-dalek": ["LICENSE"], "x25519-dalek": ["LICENSE"],
|
|
"ip_network": ["LICENSE"], "ip_network_table": ["LICENSE"],
|
|
"aws-lc-sys": ["LICENSE"], "aws-lc-rs": ["LICENSE"],
|
|
"tikv-jemalloc-sys": ["jemalloc/COPYING"], "moka": ["NOTICE"],
|
|
"ring": [
|
|
"LICENSE-BoringSSL", "LICENSE-other-bits",
|
|
"src/polyfill/once_cell/LICENSE-MIT", "third_party/fiat/LICENSE",
|
|
],
|
|
"h3": ["../LICENSE"], "h3-quinn": ["../LICENSE"],
|
|
}
|
|
for package in metadata["packages"]:
|
|
if (package["name"], "v" + package["version"]) not in production:
|
|
continue
|
|
if package["source"] is not None:
|
|
entry = f"Package: {package['name']} {package['version']}\n"
|
|
self.assertTrue(entry in bundle, f"Missing {entry.strip()}")
|
|
for notice in original_notices.get(package["name"], []):
|
|
source_license = Path(package["manifest_path"]).parent / notice
|
|
self.assertTrue(
|
|
source_license.read_text().strip() in bundle,
|
|
f"Missing original {notice} for {package['name']}",
|
|
)
|
|
|
|
def test_artifacts_reject_missing_and_modified_notices(self):
|
|
expected = NOTICES.expected_notices()
|
|
for kind, mode in [("tar", "w"), ("tar.gz", "w:gz")]:
|
|
for mutation in [None, "missing", "modified"]:
|
|
with self.subTest(kind=kind, mutation=mutation), tempfile.TemporaryDirectory() as tmp:
|
|
entries = expected.copy()
|
|
name = "licenses/dependencies.txt"
|
|
if mutation == "missing":
|
|
del entries[name]
|
|
elif mutation == "modified":
|
|
entries[name] = b"incomplete notice"
|
|
artifact = Path(tmp) / ("release." + kind)
|
|
with tarfile.open(artifact, mode) as archive:
|
|
for name, contents in entries.items():
|
|
info = tarfile.TarInfo(name)
|
|
info.size = len(contents)
|
|
archive.addfile(info, io.BytesIO(contents))
|
|
if mutation is None:
|
|
NOTICES.check(artifact)
|
|
else:
|
|
with self.assertRaises((KeyError, ValueError)):
|
|
NOTICES.check(artifact)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|