import { existsSync, readFileSync } from 'fs'; import { sync as globSync } from 'glob'; import * as path from 'path'; import * as Entitlements from './Entitlements'; import { UnexpectedError } from '../utils/errors'; import { addWarningIOS } from '../utils/warnings'; const ignoredPaths = ['**/@(Carthage|Pods|vendor|node_modules)/**']; interface ProjectFile { path: string; language: L; contents: string; } type AppleLanguage = 'objc' | 'objcpp' | 'swift'; export type AppDelegateProjectFile = ProjectFile; export function getAppDelegateHeaderFilePath(projectRoot: string): string { const [using, ...extra] = globSync('ios/*/AppDelegate.h', { absolute: true, cwd: projectRoot, ignore: ignoredPaths, }); if (!using) { throw new UnexpectedError( `Could not locate a valid AppDelegate header at root: "${projectRoot}"` ); } if (extra.length) { warnMultipleFiles({ tag: 'app-delegate-header', fileName: 'AppDelegate', projectRoot, using, extra, }); } return using; } export function getAppDelegateFilePath(projectRoot: string): string { const [using, ...extra] = globSync('ios/*/AppDelegate.@(m|mm|swift)', { absolute: true, cwd: projectRoot, ignore: ignoredPaths, }); if (!using) { throw new UnexpectedError(`Could not locate a valid AppDelegate at root: "${projectRoot}"`); } if (extra.length) { warnMultipleFiles({ tag: 'app-delegate', fileName: 'AppDelegate', projectRoot, using, extra, }); } return using; } export function getAppDelegateObjcHeaderFilePath(projectRoot: string): string { const [using, ...extra] = globSync('ios/*/AppDelegate.h', { absolute: true, cwd: projectRoot, ignore: ignoredPaths, }); if (!using) { throw new UnexpectedError(`Could not locate a valid AppDelegate.h at root: "${projectRoot}"`); } if (extra.length) { warnMultipleFiles({ tag: 'app-delegate-objc-header', fileName: 'AppDelegate.h', projectRoot, using, extra, }); } return using; } function getLanguage(filePath: string): AppleLanguage { const extension = path.extname(filePath); switch (extension) { case '.mm': return 'objcpp'; case '.m': case '.h': return 'objc'; case '.swift': return 'swift'; default: throw new UnexpectedError(`Unexpected iOS file extension: ${extension}`); } } export function getFileInfo(filePath: string) { return { path: path.normalize(filePath), contents: readFileSync(filePath, 'utf8'), language: getLanguage(filePath), }; } export function getAppDelegate(projectRoot: string): AppDelegateProjectFile { const filePath = getAppDelegateFilePath(projectRoot); return getFileInfo(filePath); } export function getSourceRoot(projectRoot: string): string { const appDelegate = getAppDelegate(projectRoot); return path.dirname(appDelegate.path); } export function findSchemePaths(projectRoot: string): string[] { return globSync('ios/*.xcodeproj/xcshareddata/xcschemes/*.xcscheme', { absolute: true, cwd: projectRoot, ignore: ignoredPaths, }); } export function findSchemeNames(projectRoot: string): string[] { const schemePaths = findSchemePaths(projectRoot); return schemePaths.map((schemePath) => path.parse(schemePath).name); } export function getAllXcodeProjectPaths(projectRoot: string): string[] { const iosFolder = 'ios'; const pbxprojPaths = globSync('ios/**/*.xcodeproj', { cwd: projectRoot, ignore: ignoredPaths }) .filter( (project) => !/test|example|sample/i.test(project) || path.dirname(project) === iosFolder ) // sort alphabetically to ensure this works the same across different devices (Fail in CI (linux) without this) .sort() .sort((a, b) => { const isAInIos = path.dirname(a) === iosFolder; const isBInIos = path.dirname(b) === iosFolder; // preserve previous sort order if ((isAInIos && isBInIos) || (!isAInIos && !isBInIos)) { return 0; } return isAInIos ? -1 : 1; }); if (!pbxprojPaths.length) { throw new UnexpectedError( `Failed to locate the ios/*.xcodeproj files relative to path "${projectRoot}".` ); } return pbxprojPaths.map((value) => path.join(projectRoot, value)); } /** * Get the pbxproj for the given path */ export function getXcodeProjectPath(projectRoot: string): string { const [using, ...extra] = getAllXcodeProjectPaths(projectRoot); if (extra.length) { warnMultipleFiles({ tag: 'xcodeproj', fileName: '*.xcodeproj', projectRoot, using, extra, }); } return using; } export function getAllPBXProjectPaths(projectRoot: string): string[] { const projectPaths = getAllXcodeProjectPaths(projectRoot); const paths = projectPaths .map((value) => path.join(value, 'project.pbxproj')) .filter((value) => existsSync(value)); if (!paths.length) { throw new UnexpectedError( `Failed to locate the ios/*.xcodeproj/project.pbxproj files relative to path "${projectRoot}".` ); } return paths; } export function getPBXProjectPath(projectRoot: string): string { const [using, ...extra] = getAllPBXProjectPaths(projectRoot); if (extra.length) { warnMultipleFiles({ tag: 'project-pbxproj', fileName: 'project.pbxproj', projectRoot, using, extra, }); } return using; } export function getAllInfoPlistPaths(projectRoot: string): string[] { const paths = globSync('ios/*/Info.plist', { absolute: true, cwd: projectRoot, ignore: ignoredPaths, }).sort( // longer name means more suffixes, we want the shortest possible one to be first. (a, b) => a.length - b.length ); if (!paths.length) { throw new UnexpectedError( `Failed to locate Info.plist files relative to path "${projectRoot}".` ); } return paths; } export function getInfoPlistPath(projectRoot: string): string { const [using, ...extra] = getAllInfoPlistPaths(projectRoot); if (extra.length) { warnMultipleFiles({ tag: 'info-plist', fileName: 'Info.plist', projectRoot, using, extra, }); } return using; } export function getAllEntitlementsPaths(projectRoot: string): string[] { const paths = globSync('ios/*/*.entitlements', { absolute: true, cwd: projectRoot, ignore: ignoredPaths, }); return paths; } /** * @deprecated: use Entitlements.getEntitlementsPath instead */ export function getEntitlementsPath(projectRoot: string): string | null { return Entitlements.getEntitlementsPath(projectRoot); } export function getSupportingPath(projectRoot: string): string { return path.resolve(projectRoot, 'ios', path.basename(getSourceRoot(projectRoot)), 'Supporting'); } export function getExpoPlistPath(projectRoot: string): string { const supportingPath = getSupportingPath(projectRoot); return path.join(supportingPath, 'Expo.plist'); } function warnMultipleFiles({ tag, fileName, projectRoot, using, extra, }: { tag: string; fileName: string; projectRoot?: string; using: string; extra: string[]; }) { const usingPath = projectRoot ? path.relative(projectRoot, using) : using; const extraPaths = projectRoot ? extra.map((v) => path.relative(projectRoot, v)) : extra; addWarningIOS( `paths-${tag}`, `Found multiple ${fileName} file paths, using "${usingPath}". Ignored paths: ${JSON.stringify( extraPaths )}` ); }