From 828303ba0ecc9b65652489b75fa59bec56bedd25 Mon Sep 17 00:00:00 2001 From: Adam Essenmacher Date: Sat, 29 Aug 2026 17:34:42 -0400 Subject: [PATCH 1/2] Make Google Maps package self-contained --- Readme.md | 2 +- build.cake | 2 + components.cake | 4 +- custom_externals_download.cake | 71 ++++++++++++++++++- .../Google/Maps/Maps.buildTransitive.targets | 4 ++ source/Google/Maps/Maps.csproj | 25 +++++-- source/Google/Maps/Maps.targets | 24 +------ 7 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 source/Google/Maps/Maps.buildTransitive.targets diff --git a/Readme.md b/Readme.md index dda107db..4139294a 100644 --- a/Readme.md +++ b/Readme.md @@ -196,7 +196,7 @@ Firebase `12.10.0` is the current published Firebase package line. | Package | Version | | --- | --- | -| `Maps` | `9.2.0.8` | +| `Maps` | `9.2.0.9` | | `Places` | `7.4.0.3` | | `SignIn` | `9.0.0` | diff --git a/build.cake b/build.cake index f6c31c47..faf8a515 100644 --- a/build.cake +++ b/build.cake @@ -171,6 +171,8 @@ Task ("externals") FirebaseAnalyticsDownload (); if (ARTIFACTS_TO_BUILD.Contains (GOOGLE_GOOGLE_APP_MEASUREMENT_ARTIFACT)) GoogleAppMeasurementDownload (); + if (ARTIFACTS_TO_BUILD.Contains (GOOGLE_MAPS_ARTIFACT)) + GoogleMapsDownload (); if (ARTIFACTS_TO_BUILD.Contains (GOOGLE_PLACES_ARTIFACT)) GooglePlacesDownload (); }); diff --git a/components.cake b/components.cake index 2fa8b851..9859b0bd 100644 --- a/components.cake +++ b/components.cake @@ -19,7 +19,7 @@ Artifact FIREBASE_APP_CHECK_ARTIFACT = new Artifact ("Firebase.App // Google artifacts available to be built. These artifacts generate NuGets. Artifact GOOGLE_ANALYTICS_ARTIFACT = new Artifact ("Google.Analytics", "3.20.0.2", "15.0", ComponentGroup.Google, csprojName: "Analytics"); Artifact GOOGLE_CAST_ARTIFACT = new Artifact ("Google.Cast", "4.7.0.1", "15.0", ComponentGroup.Google, csprojName: "Cast"); -Artifact GOOGLE_MAPS_ARTIFACT = new Artifact ("Google.Maps", "9.2.0.8", "15.0", ComponentGroup.Google, csprojName: "Maps"); +Artifact GOOGLE_MAPS_ARTIFACT = new Artifact ("Google.Maps", "9.2.0.9", "15.0", ComponentGroup.Google, csprojName: "Maps"); Artifact GOOGLE_UMP_ARTIFACT = new Artifact ("Google.UserMessagingPlatform", "1.1.0.1", "15.0", ComponentGroup.Google, csprojName: "UserMessagingPlatform"); Artifact GOOGLE_PLACES_ARTIFACT = new Artifact ("Google.Places", "7.4.0.3", "15.0", ComponentGroup.Google, csprojName: "Places"); Artifact GOOGLE_APP_CHECK_CORE_ARTIFACT = new Artifact ("Google.AppCheckCore", "11.2.0.0", "15.0", ComponentGroup.Google, csprojName: "AppCheckCore"); @@ -220,7 +220,7 @@ void SetArtifactsPodSpecs () PodSpec.Create ("google-cast-sdk", "4.7.0") }; GOOGLE_MAPS_ARTIFACT.PodSpecs = new [] { - PodSpec.Create ("GoogleMaps", "9.2.0") + PodSpec.Create ("GoogleMaps", "9.2.0", frameworkSource: FrameworkSource.Custom) }; GOOGLE_UMP_ARTIFACT.PodSpecs = new [] { PodSpec.Create ("GoogleUserMessagingPlatform", "1.1.0") diff --git a/custom_externals_download.cake b/custom_externals_download.cake index ab38ebc1..5e65c706 100644 --- a/custom_externals_download.cake +++ b/custom_externals_download.cake @@ -8,27 +8,44 @@ class ExternalDownloadSource public string Version { get; } public string ArchiveKey { get; } public string UrlPrefix { get; } - - public ExternalDownloadSource (string id, string version, string archiveKey, string urlPrefix = DefaultUrlPrefix) + public string ExtractionRootName { get; } + public string ExpectedSha256 { get; } + + public ExternalDownloadSource ( + string id, + string version, + string archiveKey, + string urlPrefix = DefaultUrlPrefix, + string extractionRootName = null, + string expectedSha256 = null) { Id = id; Version = version; ArchiveKey = archiveKey; UrlPrefix = urlPrefix; + ExtractionRootName = extractionRootName ?? $"{Id}-{Version}"; + ExpectedSha256 = expectedSha256; } public string ArchiveFileName => $"{Id}-{Version}.tar.gz"; - public string ExtractionRootName => $"{Id}-{Version}"; public string Url => $"{UrlPrefix}/{ArchiveKey}/{ArchiveFileName}"; } // *.tar.gz URLs can be found in the podspecs (e.g., CocoaPods Specs repo paths), such as: // FirebaseAnalytics: https://github.com/CocoaPods/Specs/tree/master/Specs/e/2/1/FirebaseAnalytics // GoogleAppMeasurement: https://github.com/CocoaPods/Specs/tree/master/Specs/e/3/b/GoogleAppMeasurement +// GoogleMaps: https://github.com/CocoaPods/Specs/tree/master/Specs/1/9/0/GoogleMaps // GooglePlaces: https://github.com/CocoaPods/Specs/tree/master/Specs/c/3/2/GooglePlaces var ExternalDownloads = new Dictionary { { "FirebaseAnalytics", new ExternalDownloadSource ("FirebaseAnalytics", "12.10.0", "3c185b45848d98d8") }, { "GoogleAppMeasurement", new ExternalDownloadSource ("GoogleAppMeasurement", "12.10.0", "5f5e4d8cb469941e") }, + { "GoogleMaps", new ExternalDownloadSource ( + "GoogleMaps", + "9.2.0", + "33a7ac549361ab23", + "https://dl.google.com/dl/cpdc", + extractionRootName: "Maps", + expectedSha256: "81bbd92c2d627087ae222ae955e5f746590812d7389b9d800add15e4004b6431") }, { "GooglePlaces", new ExternalDownloadSource ("GooglePlaces", "7.4.0", "3e8dc2602895d53405d075ff4eb569bff93ff1af97e69915d1e657c07ef28dd8", "https://dl.google.com/dl/geosdk") }, }; @@ -60,6 +77,7 @@ void DownloadAndExtract (ExternalDownloadSource source, Func artifactsAlre DeleteFile (archivePath); DownloadArchive (source, archivePath); + VerifyArchiveHash (source, archivePath); var exitCode = ExtractArchive (source, externalsPath, archivePath); @@ -84,6 +102,22 @@ void DownloadArchive (ExternalDownloadSource source, FilePath archivePath) DownloadFile (source.Url, archivePath); } +void VerifyArchiveHash (ExternalDownloadSource source, FilePath archivePath) +{ + if (string.IsNullOrWhiteSpace (source.ExpectedSha256)) + return; + + string actualSha256; + using (var stream = System.IO.File.OpenRead (archivePath.FullPath)) + using (var sha256 = System.Security.Cryptography.SHA256.Create ()) + actualSha256 = BitConverter.ToString (sha256.ComputeHash (stream)).Replace ("-", "").ToLowerInvariant (); + + if (!string.Equals (actualSha256, source.ExpectedSha256, StringComparison.OrdinalIgnoreCase)) + throw new Exception ($"SHA-256 mismatch for {source.ArchiveFileName}: expected {source.ExpectedSha256}, got {actualSha256}."); + + Information ($"Verified SHA-256 for {source.ArchiveFileName}."); +} + void FirebaseAnalyticsDownload () { var source = ExternalDownloads["FirebaseAnalytics"]; @@ -126,6 +160,37 @@ void GooglePlacesDownload () }); } +void GoogleMapsDownload () +{ + var source = ExternalDownloads["GoogleMaps"]; + + DownloadAndExtract ( + source, + () => DirectoryExists (new DirectoryPath ("./externals/GoogleMaps.xcframework")) && + DirectoryExists (new DirectoryPath ("./externals/GoogleMaps.bundle")), + (extractionRoot, externalsPath, deleteSettings) => { + var frameworkSource = extractionRoot.Combine ("Frameworks").Combine ("GoogleMaps.xcframework"); + var resourceSource = extractionRoot.Combine ("Resources").Combine ("GoogleMapsResources").Combine ("GoogleMaps.bundle"); + var frameworkDestination = externalsPath.Combine ("GoogleMaps.xcframework"); + var resourceDestination = externalsPath.Combine ("GoogleMaps.bundle"); + + if (!DirectoryExists (frameworkSource)) + throw new Exception ($"Expected GoogleMaps.xcframework at {frameworkSource} after extraction."); + + if (!DirectoryExists (resourceSource)) + throw new Exception ($"Expected GoogleMaps.bundle at {resourceSource} after extraction."); + + if (DirectoryExists (frameworkDestination)) + DeleteDirectory (frameworkDestination, deleteSettings); + + if (DirectoryExists (resourceDestination)) + DeleteDirectory (resourceDestination, deleteSettings); + + CopyDirectory (frameworkSource, frameworkDestination); + CopyDirectory (resourceSource, resourceDestination); + }); +} + void GoogleAppMeasurementDownload () { var source = ExternalDownloads["GoogleAppMeasurement"]; diff --git a/source/Google/Maps/Maps.buildTransitive.targets b/source/Google/Maps/Maps.buildTransitive.targets new file mode 100644 index 00000000..1a269739 --- /dev/null +++ b/source/Google/Maps/Maps.buildTransitive.targets @@ -0,0 +1,4 @@ + + + + diff --git a/source/Google/Maps/Maps.csproj b/source/Google/Maps/Maps.csproj index 8c7fceaf..18cf3fd2 100644 --- a/source/Google/Maps/Maps.csproj +++ b/source/Google/Maps/Maps.csproj @@ -4,12 +4,12 @@ enable true true - false + true 15.0 Google.Maps Google.Maps - 9.2.0.8 - 9.2.0.8 + 9.2.0.9 + 9.2.0.9 Resources true @@ -25,7 +25,7 @@ https://github.com/AdamEssenmacher/GoogleApisForiOSComponents License.md true - 9.2.0.8 + 9.2.0.9 @@ -39,6 +39,20 @@ + + + + + + + Framework + True + True + -ObjC -lc++ -lz + Accelerate Contacts CoreData CoreGraphics CoreImage CoreLocation CoreTelephony CoreText GLKit ImageIO Metal OpenGLES QuartzCore Security SystemConfiguration UIKit + + @@ -46,7 +60,4 @@ - - - diff --git a/source/Google/Maps/Maps.targets b/source/Google/Maps/Maps.targets index 96c005e4..bd55d521 100644 --- a/source/Google/Maps/Maps.targets +++ b/source/Google/Maps/Maps.targets @@ -1,27 +1,8 @@ - <_GoogleMapsAssemblyName>Google.Maps, Version=9.2.0.7, Culture=neutral, PublicKeyToken=null - <_GoogleMapsItemsFolder>GMps-9.2.0 - <_GoogleMapsSDKBaseFolder>$(XamarinBuildDownloadDir)$(_GoogleMapsItemsFolder)\Maps\Frameworks\ - <_GoogleMapsResourcesBaseFolder>$(XamarinBuildDownloadDir)$(_GoogleMapsItemsFolder)\Maps\Resources\GoogleMapsResources\GoogleMaps.bundle\ + <_GoogleMapsResourcesBaseFolder>$(MSBuildThisFileDirectory)GoogleMaps.bundle\ - - - https://dl.google.com/dl/cpdc/33a7ac549361ab23/GoogleMaps-9.2.0.tar.gz - Tgz - - - Framework - True - True - -ObjC -lc++ -lz - Accelerate Contacts CoreData CoreGraphics CoreImage CoreLocation CoreTelephony CoreText GLKit ImageIO Metal OpenGLES QuartzCore Security SystemConfiguration UIKit - - - - - GoogleMaps.bundle\Assets.car False @@ -678,6 +659,5 @@ GoogleMaps.bundle\GMSCoreResources.bundle\zh_TW.lproj\GMSCore.strings - - + From e29e373fdaaf937cd56ee733ee3660e9c40d00ff Mon Sep 17 00:00:00 2001 From: Adam Essenmacher Date: Sat, 29 Aug 2026 17:37:07 -0400 Subject: [PATCH 2/2] Add Maps self-contained package validation --- .../google-foundation-validation.yml | 15 +- scripts/check-maps-resource-manifest.py | 368 ------------------ source/Google/Maps/Maps.csproj | 2 +- .../GoogleFoundationE2E.csproj | 2 +- tests/E2E/Google.Foundation/README.md | 7 +- tools/e2e/check-consumer-shapes.sh | 25 +- tools/e2e/check-offline-build.sh | 6 +- tools/e2e/check-package-structure.sh | 65 +++- 8 files changed, 97 insertions(+), 393 deletions(-) delete mode 100755 scripts/check-maps-resource-manifest.py diff --git a/.github/workflows/google-foundation-validation.yml b/.github/workflows/google-foundation-validation.yml index 01cb8d39..260bd899 100644 --- a/.github/workflows/google-foundation-validation.yml +++ b/.github/workflows/google-foundation-validation.yml @@ -12,7 +12,6 @@ on: - "*.cake" - "icons/googleiosmaps_128x128.png" - "icons/googleiosplaces_128x128.png" - - "scripts/check-maps-resource-manifest.py" - "source/AssemblyInfo.cs" - "source/Google/Maps/**" - "source/Google/Places/**" @@ -34,7 +33,6 @@ on: - "*.cake" - "icons/googleiosmaps_128x128.png" - "icons/googleiosplaces_128x128.png" - - "scripts/check-maps-resource-manifest.py" - "source/AssemblyInfo.cs" - "source/Google/Maps/**" - "source/Google/Places/**" @@ -154,8 +152,11 @@ jobs: - name: Pack Google.Maps run: dotnet tool run dotnet-cake -- --target=nuget --names=Google.Maps - - name: Maps resource manifest check - run: python3 scripts/check-maps-resource-manifest.py + - name: Package structure checks + run: >- + tools/e2e/check-package-structure.sh + --target Maps + --package-dir output - name: Direct and transitive consumer checks run: >- @@ -163,6 +164,12 @@ jobs: --target Maps --package-dir output + - name: Offline build proof + run: >- + tools/e2e/check-offline-build.sh + --target Maps + --package-dir output + - name: Simulator E2E run: >- tools/e2e/run-google-foundation.sh diff --git a/scripts/check-maps-resource-manifest.py b/scripts/check-maps-resource-manifest.py deleted file mode 100755 index 90954521..00000000 --- a/scripts/check-maps-resource-manifest.py +++ /dev/null @@ -1,368 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Google Maps resources against the pinned SDK archive.""" - -from __future__ import annotations - -import argparse -from collections import Counter -import hashlib -from pathlib import Path, PurePosixPath -import shutil -import sys -import tarfile -import tempfile -import time -from urllib.error import URLError -from urllib.request import Request, urlopen -import xml.etree.ElementTree as ET - - -EXPECTED_ARCHIVE_SHA256 = ( - "81bbd92c2d627087ae222ae955e5f746590812d7389b9d800add15e4004b6431" -) -ARCHIVE_URL = "https://dl.google.com/dl/cpdc/33a7ac549361ab23/GoogleMaps-9.2.0.tar.gz" -ARCHIVE_RESOURCE_ROOT = "Maps/Resources/GoogleMapsResources/GoogleMaps.bundle" -EXPECTED_RESOURCE_FILE_COUNT = 190 -RESOURCE_PROPERTY = "_GoogleMapsResourcesBaseFolder" -RESOURCE_TOKEN = f"$({RESOURCE_PROPERTY})" -LOGICAL_ROOT = "GoogleMaps.bundle" - - -def repository_root() -> Path: - return Path(__file__).resolve().parent.parent - - -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--targets", - type=Path, - default=repository_root() / "source/Google/Maps/Maps.targets", - help="Maps.targets to inspect (default: repository source file)", - ) - parser.add_argument( - "--archive", - type=Path, - help="Use an existing Google Maps .tar.gz instead of downloading the pinned URL", - ) - parser.add_argument( - "--copy-bundle-to", - type=Path, - metavar="DIRECTORY", - help=( - "After all checks pass, copy the verified bundle to " - "DIRECTORY/GoogleMaps.bundle; the destination must not already exist" - ), - ) - return parser.parse_args() - - -def elements(parent: ET.Element, name: str) -> list[ET.Element]: - return [element for element in parent.iter() if element.tag.rsplit("}", 1)[-1] == name] - - -def normalize_relative_path(value: str, label: str, errors: list[str]) -> str: - normalized = value.replace("\\", "/") - path = PurePosixPath(normalized) - if ( - not normalized - or normalized.startswith("/") - or path.as_posix() != normalized - or any(part in ("", ".", "..") for part in path.parts) - ): - errors.append(f"{label} is not a normalized relative path: {value!r}") - return normalized - - -def parse_targets(targets_path: Path) -> tuple[list[str], list[str], list[str]]: - errors: list[str] = [] - try: - root = ET.parse(targets_path).getroot() - except (OSError, ET.ParseError) as exc: - raise RuntimeError(f"could not parse {targets_path}: {exc}") from exc - - includes: list[str] = [] - logical_names: list[str] = [] - bundle_resources = elements(root, "BundleResource") - if not bundle_resources: - errors.append("Maps.targets declares no BundleResource items") - - for index, resource in enumerate(bundle_resources, start=1): - include = resource.get("Include", "") - if not include.startswith(RESOURCE_TOKEN): - errors.append( - f"BundleResource #{index} Include must start with {RESOURCE_TOKEN}: {include!r}" - ) - include_suffix = include - else: - include_suffix = include[len(RESOURCE_TOKEN) :] - include_suffix = normalize_relative_path( - include_suffix, f"BundleResource #{index} Include", errors - ) - includes.append(include_suffix) - - if resource.get("Visible", "").lower() != "false": - errors.append(f"BundleResource #{index} must set Visible=\"False\"") - - logical_elements = [ - child for child in list(resource) if child.tag.rsplit("}", 1)[-1] == "LogicalName" - ] - if len(logical_elements) != 1 or not (logical_elements[0].text or "").strip(): - errors.append( - f"BundleResource #{index} must contain exactly one non-empty LogicalName" - ) - logical_name = "" - else: - logical_name = normalize_relative_path( - (logical_elements[0].text or "").strip(), - f"BundleResource #{index} LogicalName", - errors, - ) - logical_names.append(logical_name) - - expected_logical_name = f"{LOGICAL_ROOT}/{include_suffix}" - if logical_name and logical_name != expected_logical_name: - errors.append( - f"BundleResource #{index} maps {include_suffix!r} to {logical_name!r}; " - f"expected {expected_logical_name!r}" - ) - - return includes, logical_names, errors - - -def download_archive(url: str, destination: Path) -> None: - last_error: Exception | None = None - for attempt in range(1, 4): - try: - request = Request(url, headers={"User-Agent": "GoogleApisForiOSComponents-resource-audit"}) - with urlopen(request, timeout=120) as response, destination.open("wb") as output: - shutil.copyfileobj(response, output) - return - except (OSError, URLError) as exc: - last_error = exc - destination.unlink(missing_ok=True) - if attempt < 3: - time.sleep(attempt * 2) - raise RuntimeError(f"could not download {url} after 3 attempts: {last_error}") - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as source: - for chunk in iter(lambda: source.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def archive_resource_files(archive_path: Path, resource_root: str) -> tuple[list[str], list[str]]: - errors: list[str] = [] - files: list[str] = [] - normalized_root = resource_root.strip("/") - prefix = f"{normalized_root}/" - - try: - with tarfile.open(archive_path, "r:gz") as archive: - for member in archive.getmembers(): - name = member.name - while name.startswith("./"): - name = name[2:] - if not name.startswith(prefix): - continue - relative = name[len(prefix) :] - if not relative: - continue - if member.isfile(): - files.append(normalize_relative_path(relative, "archive member", errors)) - elif not member.isdir(): - errors.append(f"archive contains a non-file resource member: {name}") - except (OSError, tarfile.TarError) as exc: - raise RuntimeError(f"could not inspect {archive_path}: {exc}") from exc - - if not files: - errors.append(f"archive contains no regular files below {normalized_root}") - return files, errors - - -def duplicate_messages(values: list[str], label: str) -> list[str]: - return [ - f"duplicate {label} ({count} occurrences): {value}" - for value, count in sorted(Counter(values).items()) - if count > 1 - ] - - -def compare_sets(actual: set[str], expected: set[str], actual_label: str) -> list[str]: - errors: list[str] = [] - for value in sorted(expected - actual): - errors.append(f"missing {actual_label}: {value}") - for value in sorted(actual - expected): - errors.append(f"stale {actual_label}: {value}") - return errors - - -def copy_verified_bundle( - archive_path: Path, - resource_root: str, - relative_files: list[str], - destination_parent: Path, -) -> Path: - """Copy regular bundle files from a verified archive without trusting tar paths.""" - - destination_parent = destination_parent.expanduser().resolve() - if destination_parent.exists() and not destination_parent.is_dir(): - raise RuntimeError(f"bundle destination parent is not a directory: {destination_parent}") - destination_parent.mkdir(parents=True, exist_ok=True) - - destination_bundle = destination_parent / LOGICAL_ROOT - if destination_bundle.exists() or destination_bundle.is_symlink(): - raise RuntimeError(f"bundle destination already exists: {destination_bundle}") - - normalized_root = resource_root.strip("/") - prefix = f"{normalized_root}/" - expected_files = sorted(relative_files) - - try: - with tempfile.TemporaryDirectory( - prefix=".googlemaps-bundle-", dir=destination_parent - ) as staging_directory, tarfile.open(archive_path, "r:gz") as archive: - staging_bundle = Path(staging_directory) / LOGICAL_ROOT - - for member in archive.getmembers(): - member_name = member.name - while member_name.startswith("./"): - member_name = member_name[2:] - if not member_name.startswith(prefix): - continue - - relative_name = member_name[len(prefix) :] - if not relative_name: - continue - if member.isdir(): - relative_name = relative_name.rstrip("/") - relative_path = PurePosixPath(relative_name) - if ( - relative_path.is_absolute() - or relative_path.as_posix() != relative_name - or any(part in ("", ".", "..") for part in relative_path.parts) - ): - raise RuntimeError(f"unsafe archive resource path: {member.name!r}") - - output_path = staging_bundle.joinpath(*relative_path.parts) - if member.isdir(): - output_path.mkdir(parents=True, exist_ok=True) - continue - if not member.isfile(): - raise RuntimeError(f"archive resource is not a regular file: {member.name}") - - output_path.parent.mkdir(parents=True, exist_ok=True) - source = archive.extractfile(member) - if source is None: - raise RuntimeError(f"could not read archive resource: {member.name}") - with source, output_path.open("xb") as destination: - shutil.copyfileobj(source, destination) - - copied_files = sorted( - path.relative_to(staging_bundle).as_posix() - for path in staging_bundle.rglob("*") - if path.is_file() - ) - if copied_files != expected_files: - raise RuntimeError("copied bundle file set differs from the verified archive manifest") - - staging_bundle.rename(destination_bundle) - except (OSError, tarfile.TarError) as exc: - raise RuntimeError(f"could not copy verified Google Maps resources: {exc}") from exc - - return destination_bundle - - -def main() -> int: - args = parse_arguments() - targets_path = args.targets.resolve() - - try: - includes, logical_names, errors = parse_targets(targets_path) - with tempfile.TemporaryDirectory(prefix="maps-resource-manifest-") as temp_dir: - if args.archive: - archive_path = args.archive.resolve() - else: - archive_path = Path(temp_dir) / "GoogleMaps.tar.gz" - download_archive(ARCHIVE_URL, archive_path) - - actual_sha256 = sha256(archive_path) - if actual_sha256 != EXPECTED_ARCHIVE_SHA256: - errors.append( - f"archive SHA-256 is {actual_sha256}, expected {EXPECTED_ARCHIVE_SHA256}" - ) - - archive_files, archive_errors = archive_resource_files( - archive_path, ARCHIVE_RESOURCE_ROOT - ) - errors.extend(archive_errors) - - errors.extend(duplicate_messages(includes, "BundleResource Include")) - errors.extend(duplicate_messages(logical_names, "LogicalName")) - errors.extend(duplicate_messages(archive_files, "archive resource path")) - - include_set = set(includes) - archive_set = set(archive_files) - errors.extend(compare_sets(include_set, archive_set, "BundleResource Include")) - expected_logical_names = {f"{LOGICAL_ROOT}/{path}" for path in archive_set} - errors.extend( - compare_sets(set(logical_names), expected_logical_names, "LogicalName") - ) - - if len(includes) != EXPECTED_RESOURCE_FILE_COUNT: - errors.append( - f"expected {EXPECTED_RESOURCE_FILE_COUNT} BundleResource declarations, " - f"found {len(includes)}" - ) - if len(include_set) != EXPECTED_RESOURCE_FILE_COUNT: - errors.append( - f"expected {EXPECTED_RESOURCE_FILE_COUNT} unique target paths, " - f"found {len(include_set)}" - ) - if len(archive_files) != EXPECTED_RESOURCE_FILE_COUNT: - errors.append( - f"expected {EXPECTED_RESOURCE_FILE_COUNT} archive files, " - f"found {len(archive_files)}" - ) - - print(f"Targets: {targets_path}") - print(f"Archive URL: {ARCHIVE_URL}") - print(f"Archive resource root: {ARCHIVE_RESOURCE_ROOT}") - print(f"SHA-256: {actual_sha256}") - print( - f"Resources: {len(includes)} declarations, {len(include_set)} unique target paths, " - f"{len(archive_files)} archive files" - ) - - if errors: - print( - f"Maps resource manifest check failed with {len(errors)} error(s):", - file=sys.stderr, - ) - for error in errors: - print(f" - {error}", file=sys.stderr) - return 1 - - copied_bundle = None - if args.copy_bundle_to: - copied_bundle = copy_verified_bundle( - archive_path, - ARCHIVE_RESOURCE_ROOT, - archive_files, - args.copy_bundle_to, - ) - - print("Maps resource manifest check passed.") - if copied_bundle: - print(f"Verified bundle: {copied_bundle}") - return 0 - except (OSError, RuntimeError) as exc: - print(f"Maps resource manifest check failed: {exc}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/source/Google/Maps/Maps.csproj b/source/Google/Maps/Maps.csproj index 18cf3fd2..42a40403 100644 --- a/source/Google/Maps/Maps.csproj +++ b/source/Google/Maps/Maps.csproj @@ -35,7 +35,7 @@ - + diff --git a/tests/E2E/Google.Foundation/GoogleFoundationE2E/GoogleFoundationE2E.csproj b/tests/E2E/Google.Foundation/GoogleFoundationE2E/GoogleFoundationE2E.csproj index 6222f123..8e36ea00 100644 --- a/tests/E2E/Google.Foundation/GoogleFoundationE2E/GoogleFoundationE2E.csproj +++ b/tests/E2E/Google.Foundation/GoogleFoundationE2E/GoogleFoundationE2E.csproj @@ -20,7 +20,7 @@ Places 7.4.0.3 - 9.2.0.8 + 9.2.0.9 diff --git a/tests/E2E/Google.Foundation/README.md b/tests/E2E/Google.Foundation/README.md index bbcea086..df9902c9 100644 --- a/tests/E2E/Google.Foundation/README.md +++ b/tests/E2E/Google.Foundation/README.md @@ -11,7 +11,7 @@ binding-layer failures such as `EntryPointNotFoundException`, `DllNotFoundExcept ## Current scope and baseline Runtime adapters currently cover `AdamE.Google.iOS.Places` 7.4.0.3 and -`AdamE.Google.iOS.Maps` 9.2.0.8. +`AdamE.Google.iOS.Maps` 9.2.0.9. The behavioral expectations were established against the 7.4.0.2 pre-migration package, which used `Xamarin.Build.Download` to fetch the Google Places SDK during the consumer build. The harness does @@ -26,8 +26,9 @@ The Places adapter verifies: - The bundle contains the baseline's 59 files, including representative data, localized string, and image files. -The Maps expectations are established against the 9.2.0.8 package while it still uses -`Xamarin.Build.Download`. Like the Places adapter, its checks are independent of how the SDK arrives. +The Maps expectations were established against the 9.2.0.8 package while it used +`Xamarin.Build.Download`; the current package is 9.2.0.9 and carries the SDK itself. Like the Places +adapter, its checks are independent of how the SDK arrives. The Maps adapter verifies: - `Google.Maps.MapView` loads from the restored binding assembly. diff --git a/tools/e2e/check-consumer-shapes.sh b/tools/e2e/check-consumer-shapes.sh index 1c7a7a14..0d89a5f8 100755 --- a/tools/e2e/check-consumer-shapes.sh +++ b/tools/e2e/check-consumer-shapes.sh @@ -61,6 +61,7 @@ case "$target" in compare_expected_bundle="true" forbidden_dynamic_framework="GoogleMaps.framework" source_targets="$repo_root/source/Google/Maps/Maps.targets" + source_transitive_targets="$repo_root/source/Google/Maps/Maps.buildTransitive.targets" # Calling the managed binding above creates the native reference; no extra DllImport is needed. native_probe_decl="" native_probe_call="" @@ -79,6 +80,7 @@ case "$target" in compare_expected_bundle="false" forbidden_dynamic_framework="" source_targets="" + source_transitive_targets="" # A trivial app that never reaches a native entry point lets the linker drop the static # library, which is correct behaviour and not a delivery failure. Call into the native SDK so # the symbol assertion below actually measures whether the package delivered it. @@ -109,6 +111,7 @@ case "$target" in compare_expected_bundle="false" forbidden_dynamic_framework="" source_targets="" + source_transitive_targets="" ;; *) echo "Unknown target: $target" >&2; exit 1 ;; esac @@ -189,19 +192,17 @@ trap cleanup EXIT expected_bundle="" if [[ "$compare_expected_bundle" == "true" ]]; then - expected_bundle_parent="$work/verified-upstream" - if python3 "$repo_root/scripts/check-maps-resource-manifest.py" \ - --copy-bundle-to "$expected_bundle_parent" > "$work/maps-resource-manifest.log" 2>&1; then - expected_bundle="$expected_bundle_parent/$resource_bundle" - if [[ -d "$expected_bundle" ]]; then - pass "verified upstream $resource_bundle materialized" + expected_bundle="$repo_root/externals/$resource_bundle" + if [[ -d "$expected_bundle" ]]; then + expected_count="$(find "$expected_bundle" -type f | wc -l | tr -d ' ')" + if [[ "$expected_count" == "$expected_resource_file_count" ]]; then + pass "verified upstream $resource_bundle is present ($expected_count files)" else - fail "verified upstream checker did not materialize $expected_bundle" + fail "verified upstream $resource_bundle contains $expected_count files; expected $expected_resource_file_count" exit 1 fi else - fail "could not validate and materialize the upstream $resource_bundle" - tail -25 "$work/maps-resource-manifest.log" >&2 + fail "verified upstream $resource_bundle is missing from externals" exit 1 fi fi @@ -211,11 +212,13 @@ if [[ -n "$source_targets" ]]; then echo "Packaged MSBuild integration" for folder in build buildTransitive; do packaged_targets="$work/$folder.targets" + expected_source="$source_targets" + [[ "$folder" == "buildTransitive" && -n "$source_transitive_targets" ]] && expected_source="$source_transitive_targets" if unzip -p "$nupkg" "$folder/$package_id.targets" > "$packaged_targets" 2>/dev/null; then - if diff -u "$source_targets" "$packaged_targets" > "$work/$folder-targets.diff"; then + if diff -u "$expected_source" "$packaged_targets" > "$work/$folder-targets.diff"; then pass "$folder/$package_id.targets matches source" else - fail "$folder/$package_id.targets differs from $source_targets (see $work/$folder-targets.diff)" + fail "$folder/$package_id.targets differs from $expected_source (see $work/$folder-targets.diff)" fi else fail "$folder/$package_id.targets is missing from the selected package" diff --git a/tools/e2e/check-offline-build.sh b/tools/e2e/check-offline-build.sh index 8599a5ac..b22794a5 100755 --- a/tools/e2e/check-offline-build.sh +++ b/tools/e2e/check-offline-build.sh @@ -3,7 +3,7 @@ set -euo pipefail usage() { cat <<'EOF' -Usage: tools/e2e/check-offline-build.sh --target Places [options] +Usage: tools/e2e/check-offline-build.sh --target [options] --package-dir Local NuGet feed (default: output) --package-version Exact package version (required when the feed contains multiple) @@ -45,6 +45,10 @@ done [[ "$package_dir" != /* ]] && package_dir="$repo_root/$package_dir" case "$target" in + Maps) + package_id="AdamE.Google.iOS.Maps" + probe_expr="typeof(Google.Maps.MapView).FullName!" + ;; Places) package_id="AdamE.Google.iOS.Places" probe_expr="typeof(Google.Places.AutocompleteFilter).FullName!" diff --git a/tools/e2e/check-package-structure.sh b/tools/e2e/check-package-structure.sh index 00130f78..54c64999 100755 --- a/tools/e2e/check-package-structure.sh +++ b/tools/e2e/check-package-structure.sh @@ -3,7 +3,7 @@ set -euo pipefail usage() { cat <<'EOF' -Usage: tools/e2e/check-package-structure.sh --target Places [options] +Usage: tools/e2e/check-package-structure.sh --target [options] --package-dir Local NuGet feed (default: output) --package-version Exact package version (required when the feed contains multiple) @@ -42,6 +42,34 @@ done typeset -A expected_slice_archs case "$target" in + Maps) + package_id="AdamE.Google.iOS.Maps" + assembly_name="Google.Maps" + xcframework="GoogleMaps.xcframework" + framework_binary="GoogleMaps" + expected_slices=("ios-arm64" "ios-arm64_x86_64-simulator") + expected_slice_archs=( + "ios-arm64" "arm64" + "ios-arm64_x86_64-simulator" "arm64 x86_64" + ) + resource_bundle="GoogleMaps.bundle" + expected_bundle_files=190 + expected_resource_mappings=190 + source_project="$repo_root/source/Google/Maps/Maps.csproj" + source_build_targets="$repo_root/source/Google/Maps/Maps.targets" + source_transitive_targets="$repo_root/source/Google/Maps/Maps.buildTransitive.targets" + source_files=( + "$source_project" + "$source_build_targets" + "$source_transitive_targets" + ) + expected_kind="Framework" + expected_smartlink="True" + expected_forceload="True" + expected_frameworks="Accelerate Contacts CoreData CoreGraphics CoreImage CoreLocation CoreTelephony CoreText GLKit ImageIO Metal OpenGLES QuartzCore Security SystemConfiguration UIKit" + expected_linkerflags="-ObjC -lc++ -lz" + upstream_bundle="$repo_root/externals/GoogleMaps.bundle" + ;; Places) package_id="AdamE.Google.iOS.Places" assembly_name="Google.Places" @@ -54,6 +82,7 @@ case "$target" in ) resource_bundle="GooglePlaces.bundle" expected_bundle_files=59 + expected_resource_mappings="" source_project="$repo_root/source/Google/Places/Places.csproj" source_build_targets="$repo_root/source/Google/Places/Places.targets" source_transitive_targets="$repo_root/source/Google/Places/Places.buildTransitive.targets" @@ -237,6 +266,30 @@ for version_field in AssemblyVersion FileVersion PackageVersion; do fi done +if [[ "$target" == "Maps" ]]; then + if grep -Eq "Artifact GOOGLE_MAPS_ARTIFACT[[:space:]]*=.*\"$package_version\"" "$repo_root/components.cake"; then + pass "components.cake Maps artifact version = $package_version" + else + fail "components.cake Maps artifact version does not match $package_version" + fi + if grep -Fq "| \`Maps\` | \`$package_version\` |" "$repo_root/Readme.md"; then + pass "README Maps version = $package_version" + else + fail "README Maps version does not match $package_version" + fi +fi + +if [[ -n "$expected_resource_mappings" ]]; then + include_count="$(grep -c '' "$source_build_targets" || true)" + duplicate_count="$(grep -oE '[^<]+' "$source_build_targets" | sed 's///' | LC_ALL=C sort | uniq -d | wc -l | tr -d ' ')" + if [[ "$include_count" == "$expected_resource_mappings" && "$logical_count" == "$expected_resource_mappings" && "$duplicate_count" == "0" ]]; then + pass "Maps targets contain $expected_resource_mappings unique resource mappings" + else + fail "Maps targets contain Include=$include_count LogicalName=$logical_count duplicate=$duplicate_count; expected $expected_resource_mappings unique mappings" + fi +fi + echo echo "Native payload per TFM" lib_dirs=("$work/pkg"/lib/*(/N)) @@ -341,10 +394,14 @@ for lib_dir in "${lib_dirs[@]}"; do fail "$tfm: native reference manifest is missing" fi - upstream_bundle="$framework_root/ios-arm64/GooglePlaces.framework/Resources/$resource_bundle" + if [[ "$target" == "Maps" ]]; then + upstream_bundle="$repo_root/externals/GoogleMaps.bundle" + else + upstream_bundle="$framework_root/ios-arm64/GooglePlaces.framework/Resources/$resource_bundle" + fi packaged_bundle="$work/pkg/build/$resource_bundle" if [[ ! -d "$upstream_bundle" || ! -d "$packaged_bundle" ]]; then - [[ -d "$upstream_bundle" ]] || fail "$tfm: upstream $resource_bundle is missing from the device slice" + [[ -d "$upstream_bundle" ]] || fail "$tfm: verified upstream $resource_bundle is missing" [[ -d "$packaged_bundle" ]] || fail "build/$resource_bundle is missing from the package" continue fi @@ -414,7 +471,7 @@ check_packaged_targets build "$source_build_targets" check_packaged_targets buildTransitive "$source_transitive_targets" transitive_targets="$work/pkg/buildTransitive/$package_id.targets" -expected_import='' +expected_import="" if [[ -f "$transitive_targets" ]] && grep -Fq "$expected_import" "$transitive_targets"; then pass "buildTransitive imports the primary build target" else