build: make local Firebase configuration optional

This commit is contained in:
Kaede Akino committed 2026-10-20 11:30:15 +08:00
1 parent 16a25ea6fa
commit e2d31322f2
8 files changed
+130 -96

No files matched your search

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string></string>
<key>GCM_SENDER_ID</key>
<string></string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>UNCONFIGURED</string>
<key>PROJECT_ID</key>
<string></string>
<key>STORAGE_BUCKET</key>
<string></string>
<key>IS_ADS_ENABLED</key>
<false/>
<key>IS_ANALYTICS_ENABLED</key>
<false/>
<key>IS_APPINVITE_ENABLED</key>
<false/>
<key>IS_GCM_ENABLED</key>
<false/>
<key>IS_SIGNIN_ENABLED</key>
<false/>
<key>GOOGLE_APP_ID</key>
<string></string>
</dict>
</plist>
+11 -11
View File
@@ -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
@@ -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
+3 -1
View File
@@ -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"
@@ -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()
+18
View File
@@ -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
-36
View File
@@ -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"] {
+14 -16
View File
@@ -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 = "<group>"; };
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 */