diff --git a/Configuration/Firebase/Example/GoogleService-Info.plist b/Configuration/Firebase/Example/GoogleService-Info.plist deleted file mode 100644 index f371a38b..00000000 --- a/Configuration/Firebase/Example/GoogleService-Info.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - API_KEY - - GCM_SENDER_ID - - PLIST_VERSION - 1 - BUNDLE_ID - UNCONFIGURED - PROJECT_ID - - STORAGE_BUCKET - - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - - - diff --git a/Configuration/README.md b/Configuration/README.md index d0af99ce..502b9e52 100644 --- a/Configuration/README.md +++ b/Configuration/README.md @@ -2,9 +2,9 @@ The repository does not contain Firebase project configuration or RevenueCat SDK keys. Both app targets use `Services.xcconfig` for Debug and Release builds. -Without local overrides, Firebase stays unconfigured, Crashlytics symbol upload -is disabled, and RevenueCat purchases are unavailable. This does not bypass Pro -feature access checks. +Without a local Firebase plist or RevenueCat key, the corresponding service stays +unconfigured. Crashlytics symbol upload is disabled by default. This does not +bypass Pro feature access checks. ## Configure your own services @@ -13,17 +13,17 @@ feature access checks. 2. Set `REVENUECAT_API_KEY` to your RevenueCat public Apple SDK key and set `REVENUECAT_ENTITLEMENT_ID` to the entitlement configured in your project. Never put a private RevenueCat management key in an app build. -3. Download your own Firebase Apple app's `GoogleService-Info.plist` to - `Configuration/Firebase/GoogleService-Info.plist`. Its bundle ID must match the - app being built. Uncomment `FIREBASE_CONFIG_PATH` in the local xcconfig. +3. Create `Configuration/Firebase/` and download your own Firebase Apple app's + `GoogleService-Info.plist` there. Its bundle ID must match the app being built. 4. To upload Crashlytics symbols from Release builds, also uncomment `FIREBASE_UPLOAD_SYMBOLS = YES`. Runtime diagnostics still require user consent. -The local xcconfig and Firebase plist are ignored by Git. Keep the tracked -Firebase example unconfigured: its intentionally mismatched bundle ID prevents -SDK initialization. CI can provision the same ignored files from its secret -store, or pass build-setting overrides to `xcodebuild`. Never publish those files -or build logs containing their values. +The local xcconfig and Firebase plist are ignored by Git. There is no Firebase +example plist: the build copies the real file only when it exists, and removes a +previously bundled copy when it does not. A missing plist also skips symbol upload. +CI can provision the same ignored files from its secret store, or pass RevenueCat +and symbol-upload build-setting overrides to `xcodebuild`. Never publish those +files or build logs containing their values. Firebase client configuration and RevenueCat public SDK keys are shipped in app binaries; they are not server-side secrets. Keeping them out of this repository diff --git a/Configuration/Services.local.xcconfig.example b/Configuration/Services.local.xcconfig.example index ea3c1d30..d4df549e 100644 --- a/Configuration/Services.local.xcconfig.example +++ b/Configuration/Services.local.xcconfig.example @@ -3,6 +3,5 @@ REVENUECAT_API_KEY = REVENUECAT_ENTITLEMENT_ID = termind_pro -// Download your own Firebase Apple app configuration before enabling these. -// FIREBASE_CONFIG_PATH = Configuration/Firebase/GoogleService-Info.plist +// Download your own Firebase Apple app configuration before enabling uploads. // FIREBASE_UPLOAD_SYMBOLS = YES diff --git a/Configuration/Services.xcconfig b/Configuration/Services.xcconfig index 386ffac1..2fafde5b 100644 --- a/Configuration/Services.xcconfig +++ b/Configuration/Services.xcconfig @@ -1,7 +1,9 @@ // Public builds have no service credentials. Local overrides are never tracked. REVENUECAT_API_KEY = REVENUECAT_ENTITLEMENT_ID = termind_pro -FIREBASE_CONFIG_PATH = Configuration/Firebase/Example/GoogleService-Info.plist FIREBASE_UPLOAD_SYMBOLS = NO +// Let sandboxed scripts read the optional plist through their directory input. +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES + #include? "Services.local.xcconfig" diff --git a/Scripts/Tests/test_firebase_configuration.py b/Scripts/Tests/test_firebase_configuration.py new file mode 100644 index 00000000..9f4390e0 --- /dev/null +++ b/Scripts/Tests/test_firebase_configuration.py @@ -0,0 +1,83 @@ +import os +from pathlib import Path +import plistlib +import subprocess +import tempfile +import unittest + + +SCRIPT = Path(__file__).resolve().parents[1] / "configure-firebase.sh" + + +class FirebaseConfigurationTests(unittest.TestCase): + def setUp(self): + self.temporary_directory = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary_directory.cleanup) + self.root = Path(self.temporary_directory.name) + self.source = self.root / "Configuration/Firebase/GoogleService-Info.plist" + self.bundled = self.root / "Products/Termind.app/Contents/Resources/GoogleService-Info.plist" + self.upload_marker = self.root / "upload-arguments" + uploader = self.root / "SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run" + uploader.parent.mkdir(parents=True) + uploader.write_text('#!/bin/sh\nprintf "%s\\n" "$@" > "$UPLOAD_MARKER"\n') + uploader.chmod(0o700) + self.environment = { + **os.environ, + "SRCROOT": str(self.root), + "TARGET_BUILD_DIR": str(self.root / "Products"), + "UNLOCALIZED_RESOURCES_FOLDER_PATH": "Termind.app/Contents/Resources", + "BUILD_DIR": str(self.root / "Build/Products"), + "UPLOAD_MARKER": str(self.upload_marker), + } + + def write_configuration(self): + self.source.parent.mkdir(parents=True) + self.source.write_bytes(plistlib.dumps({"BUNDLE_ID": "com.example.test"})) + + def run_phase(self, configuration="Debug", upload="NO"): + subprocess.run( + ["/bin/sh", str(SCRIPT)], + env={ + **self.environment, + "CONFIGURATION": configuration, + "FIREBASE_UPLOAD_SYMBOLS": upload, + }, + check=True, + capture_output=True, + text=True, + ) + + def test_missing_configuration_neither_creates_a_plist_nor_uploads(self): + self.run_phase(configuration="Release", upload="YES") + self.assertFalse(self.bundled.exists()) + self.assertFalse(self.upload_marker.exists()) + + def test_local_configuration_is_copied_without_uploading_by_default(self): + self.write_configuration() + self.run_phase(configuration="Release") + self.assertEqual(self.bundled.read_bytes(), self.source.read_bytes()) + self.assertFalse(self.upload_marker.exists()) + + def test_debug_never_uploads_even_when_enabled(self): + self.write_configuration() + self.run_phase(upload="YES") + self.assertTrue(self.bundled.exists()) + self.assertFalse(self.upload_marker.exists()) + + def test_release_uploads_the_bundled_configuration_only_when_enabled(self): + self.write_configuration() + self.run_phase(configuration="Release", upload="YES") + self.assertEqual(self.upload_marker.read_text().splitlines(), ["-gsp", str(self.bundled)]) + + def test_removing_local_configuration_clears_a_previous_build(self): + self.write_configuration() + self.run_phase() + self.assertTrue(self.bundled.exists()) + self.source.unlink() + self.run_phase(configuration="Release", upload="YES") + self.assertFalse(self.bundled.exists()) + self.assertFalse(self.upload_marker.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/Scripts/configure-firebase.sh b/Scripts/configure-firebase.sh new file mode 100644 index 00000000..dae66e5f --- /dev/null +++ b/Scripts/configure-firebase.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +source_plist="${SRCROOT}/Configuration/Firebase/GoogleService-Info.plist" +bundled_plist="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist" + +if [ ! -f "${source_plist}" ]; then + # An incremental build must not retain another developer's service configuration. + rm -f "${bundled_plist}" + exit 0 +fi + +mkdir -p "$(dirname "${bundled_plist}")" +cp "${source_plist}" "${bundled_plist}" + +if [ "${CONFIGURATION}" = "Release" ] && [ "${FIREBASE_UPLOAD_SYMBOLS:-NO}" = "YES" ]; then + "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run" -gsp "${bundled_plist}" +fi diff --git a/TFMTests/ArchitectureBoundaryTests.swift b/TFMTests/ArchitectureBoundaryTests.swift index 69d6fbc7..1c091ae5 100644 --- a/TFMTests/ArchitectureBoundaryTests.swift +++ b/TFMTests/ArchitectureBoundaryTests.swift @@ -66,42 +66,6 @@ struct ArchitectureBoundaryTests { try assertNoImports(in: "../Frameworks/TermindInfrastructure/Commerce", forbidden: ["StoreKit"]) } - @Test - func serviceCredentialsStayOutsideSharedBuildSettings() throws { - let repositoryRoot = sourceRoot.deletingLastPathComponent().standardizedFileURL - let projectData = try Data(contentsOf: repositoryRoot.appending(path: "Termind.xcodeproj/project.pbxproj")) - let project = try #require( - PropertyListSerialization.propertyList(from: projectData, format: nil) as? [String: Any]) - let objects = try #require(project["objects"] as? [String: [String: Any]]) - let rootID = try #require(project["rootObject"] as? String) - let configurationListID = try #require(objects[rootID]?["buildConfigurationList"] as? String) - let configurationIDs = try #require(objects[configurationListID]?["buildConfigurations"] as? [String]) - for id in configurationIDs { - let configuration = try #require(objects[id]) - let reference = try #require(configuration["baseConfigurationReference"] as? String) - #expect(objects[reference]?["path"] as? String == "Configuration/Services.xcconfig") - } - for object in objects.values where object["isa"] as? String == "XCBuildConfiguration" { - let settings = try #require(object["buildSettings"] as? [String: Any]) - #expect(settings["REVENUECAT_API_KEY"] == nil) - } - - let defaults = try String( - contentsOf: repositoryRoot.appending(path: "Configuration/Services.xcconfig"), encoding: .utf8) - #expect(defaults.contains("REVENUECAT_API_KEY =\n")) - #expect(defaults.contains("FIREBASE_UPLOAD_SYMBOLS = NO\n")) - #expect(defaults.contains("#include? \"Services.local.xcconfig\"")) - - let templateData = try Data( - contentsOf: repositoryRoot.appending(path: "Configuration/Firebase/Example/GoogleService-Info.plist")) - let template = try #require( - PropertyListSerialization.propertyList(from: templateData, format: nil) as? [String: Any]) - for field in ["API_KEY", "GCM_SENDER_ID", "PROJECT_ID", "STORAGE_BUCKET", "GOOGLE_APP_ID"] { - #expect((template[field] as? String)?.isEmpty == true) - } - #expect(template["BUNDLE_ID"] as? String == "UNCONFIGURED") - } - @Test func diagnosisSDKStaysBehindInfrastructureAndStartsWithCollectionDisabled() throws { for module in ["TermindDomain", "TermindApplication", "TermindPresentation", "TermindPersistence"] { diff --git a/Termind.xcodeproj/project.pbxproj b/Termind.xcodeproj/project.pbxproj index 485442ad..e5286d60 100644 --- a/Termind.xcodeproj/project.pbxproj +++ b/Termind.xcodeproj/project.pbxproj @@ -100,8 +100,6 @@ F1BA00000000000000000302 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000002 /* FirebaseCrashlytics */; }; F1BA00000000000000000401 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000001 /* FirebaseCore */; }; F1BA00000000000000000402 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000002 /* FirebaseCrashlytics */; }; - F1BA0000000000000000A001 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F1BA00000000000000000020 /* GoogleService-Info.plist */; }; - F1BA0000000000000000A002 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F1BA00000000000000000020 /* GoogleService-Info.plist */; }; F6ACDF3012081771F6C8F659 /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; F996BFD771BC77E67AB91FFB /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; }; FACB579B671967F922A33CD9 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; @@ -493,7 +491,6 @@ DBE378B130478CFB00B16AA1 /* Termind.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = Termind.storekit; path = StoreKit/Termind.storekit; sourceTree = ""; }; F04700000000000000000011 /* TFMTests/SSHForwardingPumpTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFMTests/SSHForwardingPumpTests.swift; sourceTree = SOURCE_ROOT; }; F04700000000000000000012 /* TFMTests/SSHForwardingWireTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFMTests/SSHForwardingWireTests.swift; sourceTree = SOURCE_ROOT; }; - F1BA00000000000000000020 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "$(FIREBASE_CONFIG_PATH)"; sourceTree = SOURCE_ROOT; }; F1BA00000000000000000021 /* Services.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Configuration/Services.xcconfig; sourceTree = SOURCE_ROOT; }; TRMD00000000000000000001 /* Termind.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Termind.app; sourceTree = BUILT_PRODUCTS_DIR; }; TRMD00000000000000020001 /* TFMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TFMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -858,7 +855,6 @@ TRMD00000000000000000030 = { isa = PBXGroup; children = ( - F1BA00000000000000000020 /* GoogleService-Info.plist */, F1BA00000000000000000021 /* Services.xcconfig */, BENC00000000000000000010 /* Benchmarks/PortForwarding */, CB622EAF077B1253532F1E18 /* TFIWidgets */, @@ -908,7 +904,7 @@ AC89B83FA8E4154383D9AC81 /* Sources */, 7B09CFC5B32584951BD4C8F9 /* Frameworks */, 8F0C5245592A0E2AFB85E50F /* Resources */, - F1BA00000000000000003002 /* Upload Crashlytics Symbols */, + F1BA00000000000000003002 /* Configure Firebase */, ); buildRules = ( ); @@ -1236,7 +1232,7 @@ TRMD00000000000000000020 /* Frameworks */, TRMD00000000000000000080 /* Resources */, TRMD00000000000000000085 /* Embed MCP Bridge */, - F1BA00000000000000002002 /* Upload Crashlytics Symbols */, + F1BA00000000000000002002 /* Configure Firebase */, ); buildRules = ( ); @@ -1482,7 +1478,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - F1BA0000000000000000A002 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1511,7 +1506,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - F1BA0000000000000000A001 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1525,7 +1519,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - F1BA00000000000000002002 /* Upload Crashlytics Symbols */ = { + F1BA00000000000000002002 /* Configure Firebase */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -1537,18 +1531,20 @@ "$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)", "$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist", "$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)", - "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", + "$(SRCROOT)/Configuration/", + "$(SRCROOT)/Scripts/configure-firebase.sh", "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", ); - name = "Upload Crashlytics Symbols"; + name = "Configure Firebase"; outputPaths = ( + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ \"${CONFIGURATION}\" = \"Release\" ] && [ \"${FIREBASE_UPLOAD_SYMBOLS}\" = \"YES\" ]; then\n \"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\" -gsp \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist\"\nfi\n"; + shellScript = "/bin/sh \"${SRCROOT}/Scripts/configure-firebase.sh\"\n"; }; - F1BA00000000000000003002 /* Upload Crashlytics Symbols */ = { + F1BA00000000000000003002 /* Configure Firebase */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -1560,16 +1556,18 @@ "$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)", "$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist", "$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)", - "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", + "$(SRCROOT)/Configuration/", + "$(SRCROOT)/Scripts/configure-firebase.sh", "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", ); - name = "Upload Crashlytics Symbols"; + name = "Configure Firebase"; outputPaths = ( + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ \"${CONFIGURATION}\" = \"Release\" ] && [ \"${FIREBASE_UPLOAD_SYMBOLS}\" = \"YES\" ]; then\n \"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\" -gsp \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist\"\nfi\n"; + shellScript = "/bin/sh \"${SRCROOT}/Scripts/configure-firebase.sh\"\n"; }; /* End PBXShellScriptBuildPhase section */