diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e512eb9..31cdf43 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -175,6 +175,7 @@ jobs: test \ -only-testing:MParticleSampleTests/RCTConvertCommerceMappingTests \ -only-testing:MParticleSampleTests/RoktNativeLayoutComponentViewSizingTests \ + -only-testing:MParticleSampleTests/RNMParticleDuplicateSDKTests \ | bundle exec xcpretty -k pr-notify: diff --git a/README.md b/README.md index b7774f7..9a07862 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,10 @@ npm start and build your workspace from xCode. +### Swift Package Manager + +Take the mParticle SDK and its kits from one dependency manager only. If the core comes from CocoaPods (through this package) and a kit such as `mParticle-Rokt` is added to the app with Swift Package Manager, the app contains two copies of the SDK. It builds and archives without warnings, then crashes at runtime. Debug builds detect this and show a red box: `[mParticle] The mParticle SDK is loaded more than once`. To fix it, remove the mParticle and Rokt Swift packages from the app and add the kits with CocoaPods, as shown above. + ## Android (Manual Setup) 1. Copy your mParticle key and secret from [your workspace's dashboard](https://app.mparticle.com/setup/inputs/apps) and construct an `MParticleOptions` object. diff --git a/ios/RNMParticle/RNMParticle.mm b/ios/RNMParticle/RNMParticle.mm index da15077..01529f1 100644 --- a/ios/RNMParticle/RNMParticle.mm +++ b/ios/RNMParticle/RNMParticle.mm @@ -9,6 +9,10 @@ #import #endif #import +#import +#if DEBUG +#import +#endif #ifdef RCT_NEW_ARCH_ENABLED #import @@ -143,6 +147,65 @@ + (void)load { RCTRegisterModule(self); } +#if DEBUG +- (instancetype)init +{ + if (self = [super init]) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [RNMParticle warnIfSDKLoadedTwice]; + }); + } + return self; +} + +// Counts, per class name, the loaded images inside the app bundle that define it. System +// images are skipped: scanning them all takes tens of seconds, the bundle takes milliseconds. ++ (NSDictionary *)imageCountsForClassNames:(NSArray *)classNames +{ + NSMutableDictionary *counts = [NSMutableDictionary dictionary]; + for (NSString *name in classNames) { + counts[name] = @0; + } + NSString *bundlePath = NSBundle.mainBundle.bundlePath; + unsigned int imageCount = 0; + const char **images = objc_copyImageNames(&imageCount); + for (unsigned int i = 0; i < imageCount; i++) { + if (![@(images[i]) hasPrefix:bundlePath]) { + continue; + } + unsigned int classCount = 0; + const char **classes = objc_copyClassNamesForImage(images[i], &classCount); + for (unsigned int j = 0; j < classCount; j++) { + NSString *name = @(classes[j]); + if (counts[name] != nil) { + counts[name] = @(counts[name].unsignedIntegerValue + 1); + } + } + free(classes); + } + free(images); + return counts; +} + +// Mixing CocoaPods and Swift Package Manager for the mParticle SDKs links a second copy that +// builds and archives silently, then crashes at runtime. Say so early in development. ++ (void)warnIfSDKLoadedTwice +{ + NSArray *classNames = @[ @"MParticle", @"RoktEmbeddedView" ]; + NSDictionary *counts = [self imageCountsForClassNames:classNames]; + NSMutableArray *duplicates = [NSMutableArray array]; + for (NSString *name in classNames) { + if (counts[name].unsignedIntegerValue > 1) { + [duplicates addObject:[NSString stringWithFormat:@"%@ x%@", name, counts[name]]]; + } + } + if (duplicates.count > 0) { + RCTLogError(@"[mParticle] The mParticle SDK is loaded more than once (%@). This happens when the SDK comes from both CocoaPods and Swift Package Manager. See README › Swift Package Manager.", [duplicates componentsJoinedByString:@", "]); + } +} +#endif + RCT_EXPORT_METHOD(upload) { [[MParticle sharedInstance] upload]; diff --git a/sample/ios/MParticleSample.xcodeproj/project.pbxproj b/sample/ios/MParticleSample.xcodeproj/project.pbxproj index 7c899c9..127cf13 100644 --- a/sample/ios/MParticleSample.xcodeproj/project.pbxproj +++ b/sample/ios/MParticleSample.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ B7C10E912E50AA1100000002 /* RCTConvertCommerceMappingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */; }; B7C10E932E50AA1100000004 /* RNMPRoktPlaceholderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E922E50AA1100000003 /* RNMPRoktPlaceholderTests.m */; }; B7C10E952E50AA1100000006 /* RoktNativeLayoutComponentViewSizingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E942E50AA1100000005 /* RoktNativeLayoutComponentViewSizingTests.mm */; }; + B7C10E972E50AA1100000008 /* RNMParticleDuplicateSDKTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E962E50AA1100000007 /* RNMParticleDuplicateSDKTests.m */; }; B7C10E992F6B000000000009 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E982F6B000000000008 /* SceneDelegate.swift */; }; /* End PBXBuildFile section */ @@ -49,6 +50,7 @@ B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTConvertCommerceMappingTests.m; sourceTree = ""; }; B7C10E922E50AA1100000003 /* RNMPRoktPlaceholderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNMPRoktPlaceholderTests.m; sourceTree = ""; }; B7C10E942E50AA1100000005 /* RoktNativeLayoutComponentViewSizingTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RoktNativeLayoutComponentViewSizingTests.mm; sourceTree = ""; }; + B7C10E962E50AA1100000007 /* RNMParticleDuplicateSDKTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNMParticleDuplicateSDKTests.m; sourceTree = ""; }; B7C10E982F6B000000000008 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = MParticleSample/SceneDelegate.swift; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -80,6 +82,7 @@ B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */, B7C10E922E50AA1100000003 /* RNMPRoktPlaceholderTests.m */, B7C10E942E50AA1100000005 /* RoktNativeLayoutComponentViewSizingTests.mm */, + B7C10E962E50AA1100000007 /* RNMParticleDuplicateSDKTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, ); path = MParticleSampleTests; @@ -399,6 +402,7 @@ B7C10E912E50AA1100000002 /* RCTConvertCommerceMappingTests.m in Sources */, B7C10E932E50AA1100000004 /* RNMPRoktPlaceholderTests.m in Sources */, B7C10E952E50AA1100000006 /* RoktNativeLayoutComponentViewSizingTests.mm in Sources */, + B7C10E972E50AA1100000008 /* RNMParticleDuplicateSDKTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/sample/ios/MParticleSampleTests/RNMParticleDuplicateSDKTests.m b/sample/ios/MParticleSampleTests/RNMParticleDuplicateSDKTests.m new file mode 100644 index 0000000..42d4ee3 --- /dev/null +++ b/sample/ios/MParticleSampleTests/RNMParticleDuplicateSDKTests.m @@ -0,0 +1,35 @@ +#import +#import "../../../ios/RNMParticle/RNMParticle.h" + +// Implemented in RNMParticle.mm, Debug builds only. +@interface RNMParticle (DuplicateSDKTests) ++ (NSDictionary *)imageCountsForClassNames:(NSArray *)classNames; +@end + +/** + * Guards the Debug-only check that warns when two copies of the mParticle SDK are loaded. + * + * Only `MParticle` is asserted: it lives in the SDK framework, which this test bundle shares + * with the app it hosts. The wrapper's own classes are linked into this bundle a second time + * (see RNMPRoktPlaceholderTests.m), so counting them would test the test linkage. + */ +@interface RNMParticleDuplicateSDKTests : XCTestCase +@end + +@implementation RNMParticleDuplicateSDKTests + +- (void)testCountsOneCopyOfTheSDK +{ + NSDictionary *counts = [RNMParticle imageCountsForClassNames:@[ @"MParticle" ]]; + + XCTAssertEqualObjects(counts[@"MParticle"], @1); +} + +- (void)testCountsZeroForAClassNoImageDefines +{ + NSDictionary *counts = [RNMParticle imageCountsForClassNames:@[ @"RNMPNoSuchClass" ]]; + + XCTAssertEqualObjects(counts[@"RNMPNoSuchClass"], @0); +} + +@end