import { PBXNativeTarget, PBXTargetDependency, XCBuildConfiguration, XcodeProject } from 'xcode'; import { getApplicationTargetNameForSchemeAsync } from './BuildScheme'; import { getBuildConfigurationForListIdAndName, getPbxproj, isNotComment, NativeTargetSectionEntry, } from './utils/Xcodeproj'; import { trimQuotes } from './utils/string'; export enum TargetType { APPLICATION = 'com.apple.product-type.application', EXTENSION = 'com.apple.product-type.app-extension', WATCH = 'com.apple.product-type.application.watchapp', APP_CLIP = 'com.apple.product-type.application.on-demand-install-capable', STICKER_PACK_EXTENSION = 'com.apple.product-type.app-extension.messages-sticker-pack', FRAMEWORK = 'com.apple.product-type.framework', OTHER = 'other', } export interface Target { name: string; type: TargetType; signable: boolean; dependencies?: Target[]; } export function getXCBuildConfigurationFromPbxproj( project: XcodeProject, { targetName, buildConfiguration = 'Release', }: { targetName?: string; buildConfiguration?: string } = {} ): XCBuildConfiguration | null { const [, nativeTarget] = targetName ? findNativeTargetByName(project, targetName) : findFirstNativeTarget(project); const [, xcBuildConfiguration] = getBuildConfigurationForListIdAndName(project, { configurationListId: nativeTarget.buildConfigurationList, buildConfiguration, }); return xcBuildConfiguration ?? null; } export async function findApplicationTargetWithDependenciesAsync( projectRoot: string, scheme: string ): Promise { const applicationTargetName = await getApplicationTargetNameForSchemeAsync(projectRoot, scheme); const project = getPbxproj(projectRoot); const [, applicationTarget] = findNativeTargetByName(project, applicationTargetName); const dependencies = getTargetDependencies(project, applicationTarget); return { name: trimQuotes(applicationTarget.name), type: TargetType.APPLICATION, signable: true, dependencies, }; } function getTargetDependencies( project: XcodeProject, parentTarget: PBXNativeTarget ): Target[] | undefined { if (!parentTarget.dependencies || parentTarget.dependencies.length === 0) { return undefined; } const nonSignableTargetTypes: TargetType[] = [TargetType.FRAMEWORK]; return parentTarget.dependencies.map(({ value }) => { const { target: targetId } = project.getPBXGroupByKeyAndType( value, 'PBXTargetDependency' ) as PBXTargetDependency; const [, target] = findNativeTargetById(project, targetId); const type = isTargetOfType(target, TargetType.EXTENSION) ? TargetType.EXTENSION : TargetType.OTHER; return { name: trimQuotes(target.name), type, signable: !nonSignableTargetTypes.some((signableTargetType) => isTargetOfType(target, signableTargetType) ), dependencies: getTargetDependencies(project, target), }; }); } export function isTargetOfType(target: PBXNativeTarget, targetType: TargetType): boolean { return trimQuotes(target.productType) === targetType; } export function getNativeTargets(project: XcodeProject): NativeTargetSectionEntry[] { const section = project.pbxNativeTargetSection(); return Object.entries(section).filter(isNotComment); } export function findSignableTargets(project: XcodeProject): NativeTargetSectionEntry[] { const targets = getNativeTargets(project); const signableTargetTypes: TargetType[] = [ TargetType.APPLICATION, TargetType.APP_CLIP, TargetType.EXTENSION, TargetType.WATCH, TargetType.STICKER_PACK_EXTENSION, ]; const applicationTargets = targets.filter(([, target]) => { for (const targetType of signableTargetTypes) { if (isTargetOfType(target, targetType)) { return true; } } return false; }); if (applicationTargets.length === 0) { throw new Error(`Could not find any signable targets in project.pbxproj`); } return applicationTargets; } export function findFirstNativeTarget(project: XcodeProject): NativeTargetSectionEntry { const targets = getNativeTargets(project); const applicationTargets = targets.filter(([, target]) => isTargetOfType(target, TargetType.APPLICATION) ); if (applicationTargets.length === 0) { throw new Error(`Could not find any application target in project.pbxproj`); } return applicationTargets[0]; } export function findNativeTargetByName( project: XcodeProject, targetName: string ): NativeTargetSectionEntry { const nativeTargets = getNativeTargets(project); const nativeTargetEntry = nativeTargets.find(([, i]) => trimQuotes(i.name) === targetName); if (!nativeTargetEntry) { throw new Error(`Could not find target '${targetName}' in project.pbxproj`); } return nativeTargetEntry; } function findNativeTargetById(project: XcodeProject, targetId: string): NativeTargetSectionEntry { const nativeTargets = getNativeTargets(project); const nativeTargetEntry = nativeTargets.find(([key]) => key === targetId); if (!nativeTargetEntry) { throw new Error(`Could not find target with id '${targetId}' in project.pbxproj`); } return nativeTargetEntry; }