1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types'; 2082815dcSEvan Baconimport plist, { PlistObject } from '@expo/plist'; 3082815dcSEvan Baconimport assert from 'assert'; 4082815dcSEvan Baconimport fs from 'fs'; 5082815dcSEvan Baconimport xcode, { XCBuildConfiguration } from 'xcode'; 6082815dcSEvan Bacon 7082815dcSEvan Baconimport { InfoPlist } from './IosConfig.types'; 8082815dcSEvan Baconimport { getAllInfoPlistPaths, getAllPBXProjectPaths, getPBXProjectPath } from './Paths'; 9082815dcSEvan Baconimport { findFirstNativeTarget, getXCBuildConfigurationFromPbxproj } from './Target'; 10082815dcSEvan Baconimport { ConfigurationSectionEntry, getBuildConfigurationsForListId } from './utils/Xcodeproj'; 11082815dcSEvan Baconimport { trimQuotes } from './utils/string'; 12*8a424bebSJames Ideimport { ConfigPlugin } from '../Plugin.types'; 13*8a424bebSJames Ideimport { withDangerousMod } from '../plugins/withDangerousMod'; 14082815dcSEvan Bacon 15082815dcSEvan Baconexport const withBundleIdentifier: ConfigPlugin<{ bundleIdentifier?: string }> = ( 16082815dcSEvan Bacon config, 17082815dcSEvan Bacon { bundleIdentifier } 18082815dcSEvan Bacon) => { 19082815dcSEvan Bacon return withDangerousMod(config, [ 20082815dcSEvan Bacon 'ios', 21082815dcSEvan Bacon async (config) => { 22082815dcSEvan Bacon const bundleId = bundleIdentifier ?? config.ios?.bundleIdentifier; 23082815dcSEvan Bacon assert( 24082815dcSEvan Bacon bundleId, 25082815dcSEvan Bacon '`bundleIdentifier` must be defined in the app config (`expo.ios.bundleIdentifier`) or passed to the plugin `withBundleIdentifier`.' 26082815dcSEvan Bacon ); 27082815dcSEvan Bacon await setBundleIdentifierForPbxproj(config.modRequest.projectRoot, bundleId!); 28082815dcSEvan Bacon return config; 29082815dcSEvan Bacon }, 30082815dcSEvan Bacon ]); 31082815dcSEvan Bacon}; 32082815dcSEvan Bacon 33082815dcSEvan Baconfunction getBundleIdentifier(config: Pick<ExpoConfig, 'ios'>): string | null { 34082815dcSEvan Bacon return config.ios?.bundleIdentifier ?? null; 35082815dcSEvan Bacon} 36082815dcSEvan Bacon 37082815dcSEvan Bacon/** 38082815dcSEvan Bacon * In Turtle v1 we set the bundleIdentifier directly on Info.plist rather 39082815dcSEvan Bacon * than in pbxproj 40082815dcSEvan Bacon */ 41082815dcSEvan Baconfunction setBundleIdentifier(config: ExpoConfig, infoPlist: InfoPlist): InfoPlist { 42082815dcSEvan Bacon const bundleIdentifier = getBundleIdentifier(config); 43082815dcSEvan Bacon 44082815dcSEvan Bacon if (!bundleIdentifier) { 45082815dcSEvan Bacon return infoPlist; 46082815dcSEvan Bacon } 47082815dcSEvan Bacon 48082815dcSEvan Bacon return { 49082815dcSEvan Bacon ...infoPlist, 50082815dcSEvan Bacon CFBundleIdentifier: bundleIdentifier, 51082815dcSEvan Bacon }; 52082815dcSEvan Bacon} 53082815dcSEvan Bacon 54082815dcSEvan Bacon/** 55082815dcSEvan Bacon * Gets the bundle identifier defined in the Xcode project found in the project directory. 56082815dcSEvan Bacon * 57082815dcSEvan Bacon * A bundle identifier is stored as a value in XCBuildConfiguration entry. 58082815dcSEvan Bacon * Those entries exist for every pair (build target, build configuration). 59082815dcSEvan Bacon * Unless target name is passed, the first target defined in the pbxproj is used 60082815dcSEvan Bacon * (to keep compatibility with the inaccurate legacy implementation of this function). 61082815dcSEvan Bacon * The build configuration is usually 'Release' or 'Debug'. However, it could be any arbitrary string. 62082815dcSEvan Bacon * Defaults to 'Release'. 63082815dcSEvan Bacon * 64082815dcSEvan Bacon * @param {string} projectRoot Path to project root containing the ios directory 65082815dcSEvan Bacon * @param {string} targetName Target name 66082815dcSEvan Bacon * @param {string} buildConfiguration Build configuration. Defaults to 'Release'. 67082815dcSEvan Bacon * @returns {string | null} bundle identifier of the Xcode project or null if the project is not configured 68082815dcSEvan Bacon */ 69082815dcSEvan Baconfunction getBundleIdentifierFromPbxproj( 70082815dcSEvan Bacon projectRoot: string, 71082815dcSEvan Bacon { 72082815dcSEvan Bacon targetName, 73082815dcSEvan Bacon buildConfiguration = 'Release', 74082815dcSEvan Bacon }: { targetName?: string; buildConfiguration?: string } = {} 75082815dcSEvan Bacon): string | null { 76082815dcSEvan Bacon let pbxprojPath: string; 77082815dcSEvan Bacon try { 78082815dcSEvan Bacon pbxprojPath = getPBXProjectPath(projectRoot); 79082815dcSEvan Bacon } catch { 80082815dcSEvan Bacon return null; 81082815dcSEvan Bacon } 82082815dcSEvan Bacon const project = xcode.project(pbxprojPath); 83082815dcSEvan Bacon project.parseSync(); 84082815dcSEvan Bacon 85082815dcSEvan Bacon const xcBuildConfiguration = getXCBuildConfigurationFromPbxproj(project, { 86082815dcSEvan Bacon targetName, 87082815dcSEvan Bacon buildConfiguration, 88082815dcSEvan Bacon }); 89082815dcSEvan Bacon if (!xcBuildConfiguration) { 90082815dcSEvan Bacon return null; 91082815dcSEvan Bacon } 92082815dcSEvan Bacon return getProductBundleIdentifierFromBuildConfiguration(xcBuildConfiguration); 93082815dcSEvan Bacon} 94082815dcSEvan Bacon 95082815dcSEvan Baconfunction getProductBundleIdentifierFromBuildConfiguration( 96082815dcSEvan Bacon xcBuildConfiguration: XCBuildConfiguration 97082815dcSEvan Bacon): string | null { 98082815dcSEvan Bacon const bundleIdentifierRaw = xcBuildConfiguration.buildSettings.PRODUCT_BUNDLE_IDENTIFIER; 99082815dcSEvan Bacon if (bundleIdentifierRaw) { 100082815dcSEvan Bacon const bundleIdentifier = trimQuotes(bundleIdentifierRaw); 101082815dcSEvan Bacon // it's possible to use interpolation for the bundle identifier 102082815dcSEvan Bacon // the most common case is when the last part of the id is set to `$(PRODUCT_NAME:rfc1034identifier)` 103082815dcSEvan Bacon // in this case, PRODUCT_NAME should be replaced with its value 104082815dcSEvan Bacon // the `rfc1034identifier` modifier replaces all non-alphanumeric characters with dashes 105082815dcSEvan Bacon const bundleIdentifierParts = bundleIdentifier.split('.'); 106082815dcSEvan Bacon if ( 107082815dcSEvan Bacon bundleIdentifierParts[bundleIdentifierParts.length - 1] === 108082815dcSEvan Bacon '$(PRODUCT_NAME:rfc1034identifier)' && 109082815dcSEvan Bacon xcBuildConfiguration.buildSettings.PRODUCT_NAME 110082815dcSEvan Bacon ) { 111082815dcSEvan Bacon bundleIdentifierParts[bundleIdentifierParts.length - 1] = 112082815dcSEvan Bacon xcBuildConfiguration.buildSettings.PRODUCT_NAME.replace(/[^a-zA-Z0-9]/g, '-'); 113082815dcSEvan Bacon } 114082815dcSEvan Bacon return bundleIdentifierParts.join('.'); 115082815dcSEvan Bacon } else { 116082815dcSEvan Bacon return null; 117082815dcSEvan Bacon } 118082815dcSEvan Bacon} 119082815dcSEvan Bacon 120082815dcSEvan Bacon/** 121082815dcSEvan Bacon * Updates the bundle identifier for a given pbxproj 122082815dcSEvan Bacon * 123082815dcSEvan Bacon * @param {string} pbxprojPath Path to pbxproj file 124082815dcSEvan Bacon * @param {string} bundleIdentifier Bundle identifier to set in the pbxproj 125082815dcSEvan Bacon * @param {boolean} [updateProductName=true] Whether to update PRODUCT_NAME 126082815dcSEvan Bacon */ 127082815dcSEvan Baconfunction updateBundleIdentifierForPbxproj( 128082815dcSEvan Bacon pbxprojPath: string, 129082815dcSEvan Bacon bundleIdentifier: string, 130082815dcSEvan Bacon updateProductName: boolean = true 131082815dcSEvan Bacon): void { 132082815dcSEvan Bacon const project = xcode.project(pbxprojPath); 133082815dcSEvan Bacon project.parseSync(); 134082815dcSEvan Bacon 135082815dcSEvan Bacon const [, nativeTarget] = findFirstNativeTarget(project); 136082815dcSEvan Bacon 137082815dcSEvan Bacon getBuildConfigurationsForListId(project, nativeTarget.buildConfigurationList).forEach( 138082815dcSEvan Bacon ([, item]: ConfigurationSectionEntry) => { 139082815dcSEvan Bacon if (item.buildSettings.PRODUCT_BUNDLE_IDENTIFIER === bundleIdentifier) { 140082815dcSEvan Bacon return; 141082815dcSEvan Bacon } 142082815dcSEvan Bacon 143082815dcSEvan Bacon item.buildSettings.PRODUCT_BUNDLE_IDENTIFIER = `"${bundleIdentifier}"`; 144082815dcSEvan Bacon 145082815dcSEvan Bacon if (updateProductName) { 146082815dcSEvan Bacon const productName = bundleIdentifier.split('.').pop(); 147082815dcSEvan Bacon if (!productName?.includes('$')) { 148082815dcSEvan Bacon item.buildSettings.PRODUCT_NAME = productName; 149082815dcSEvan Bacon } 150082815dcSEvan Bacon } 151082815dcSEvan Bacon } 152082815dcSEvan Bacon ); 153082815dcSEvan Bacon fs.writeFileSync(pbxprojPath, project.writeSync()); 154082815dcSEvan Bacon} 155082815dcSEvan Bacon 156082815dcSEvan Bacon/** 157082815dcSEvan Bacon * Updates the bundle identifier for pbx projects inside the ios directory of the given project root 158082815dcSEvan Bacon * 159082815dcSEvan Bacon * @param {string} projectRoot Path to project root containing the ios directory 160082815dcSEvan Bacon * @param {string} bundleIdentifier Desired bundle identifier 161082815dcSEvan Bacon * @param {boolean} [updateProductName=true] Whether to update PRODUCT_NAME 162082815dcSEvan Bacon */ 163082815dcSEvan Baconfunction setBundleIdentifierForPbxproj( 164082815dcSEvan Bacon projectRoot: string, 165082815dcSEvan Bacon bundleIdentifier: string, 166082815dcSEvan Bacon updateProductName: boolean = true 167082815dcSEvan Bacon): void { 168082815dcSEvan Bacon // Get all pbx projects in the ${projectRoot}/ios directory 169082815dcSEvan Bacon let pbxprojPaths: string[] = []; 170082815dcSEvan Bacon try { 171082815dcSEvan Bacon pbxprojPaths = getAllPBXProjectPaths(projectRoot); 172082815dcSEvan Bacon } catch {} 173082815dcSEvan Bacon 174082815dcSEvan Bacon for (const pbxprojPath of pbxprojPaths) { 175082815dcSEvan Bacon updateBundleIdentifierForPbxproj(pbxprojPath, bundleIdentifier, updateProductName); 176082815dcSEvan Bacon } 177082815dcSEvan Bacon} 178082815dcSEvan Bacon 179082815dcSEvan Bacon/** 180082815dcSEvan Bacon * Reset bundle identifier field in Info.plist to use PRODUCT_BUNDLE_IDENTIFIER, as recommended by Apple. 181082815dcSEvan Bacon */ 182082815dcSEvan Bacon 183082815dcSEvan Baconconst defaultBundleId = '$(PRODUCT_BUNDLE_IDENTIFIER)'; 184082815dcSEvan Bacon 185082815dcSEvan Baconfunction resetAllPlistBundleIdentifiers(projectRoot: string): void { 186082815dcSEvan Bacon const infoPlistPaths = getAllInfoPlistPaths(projectRoot); 187082815dcSEvan Bacon 188082815dcSEvan Bacon for (const plistPath of infoPlistPaths) { 189082815dcSEvan Bacon resetPlistBundleIdentifier(plistPath); 190082815dcSEvan Bacon } 191082815dcSEvan Bacon} 192082815dcSEvan Bacon 193082815dcSEvan Baconfunction resetPlistBundleIdentifier(plistPath: string): void { 194082815dcSEvan Bacon const rawPlist = fs.readFileSync(plistPath, 'utf8'); 195082815dcSEvan Bacon const plistObject = plist.parse(rawPlist) as PlistObject; 196082815dcSEvan Bacon 197082815dcSEvan Bacon if (plistObject.CFBundleIdentifier) { 198082815dcSEvan Bacon if (plistObject.CFBundleIdentifier === defaultBundleId) return; 199082815dcSEvan Bacon 200082815dcSEvan Bacon // attempt to match default Info.plist format 201082815dcSEvan Bacon const format = { pretty: true, indent: `\t` }; 202082815dcSEvan Bacon 203082815dcSEvan Bacon const xml = plist.build( 204082815dcSEvan Bacon { 205082815dcSEvan Bacon ...plistObject, 206082815dcSEvan Bacon CFBundleIdentifier: defaultBundleId, 207082815dcSEvan Bacon }, 208082815dcSEvan Bacon format 209082815dcSEvan Bacon ); 210082815dcSEvan Bacon 211082815dcSEvan Bacon if (xml !== rawPlist) { 212082815dcSEvan Bacon fs.writeFileSync(plistPath, xml); 213082815dcSEvan Bacon } 214082815dcSEvan Bacon } 215082815dcSEvan Bacon} 216082815dcSEvan Bacon 217082815dcSEvan Baconexport { 218082815dcSEvan Bacon getBundleIdentifier, 219082815dcSEvan Bacon setBundleIdentifier, 220082815dcSEvan Bacon getBundleIdentifierFromPbxproj, 221082815dcSEvan Bacon updateBundleIdentifierForPbxproj, 222082815dcSEvan Bacon setBundleIdentifierForPbxproj, 223082815dcSEvan Bacon resetAllPlistBundleIdentifiers, 224082815dcSEvan Bacon resetPlistBundleIdentifier, 225082815dcSEvan Bacon}; 226