1import { PBXNativeTarget, PBXTargetDependency, XCBuildConfiguration, XcodeProject } from 'xcode';
2
3import { getApplicationTargetNameForSchemeAsync } from './BuildScheme';
4import {
5  getBuildConfigurationForListIdAndName,
6  getPbxproj,
7  isNotComment,
8  NativeTargetSectionEntry,
9} from './utils/Xcodeproj';
10import { trimQuotes } from './utils/string';
11
12export enum TargetType {
13  APPLICATION = 'com.apple.product-type.application',
14  EXTENSION = 'com.apple.product-type.app-extension',
15  WATCH = 'com.apple.product-type.application.watchapp',
16  APP_CLIP = 'com.apple.product-type.application.on-demand-install-capable',
17  STICKER_PACK_EXTENSION = 'com.apple.product-type.app-extension.messages-sticker-pack',
18  OTHER = 'other',
19}
20
21export interface Target {
22  name: string;
23  type: TargetType;
24  dependencies?: Target[];
25}
26
27export function getXCBuildConfigurationFromPbxproj(
28  project: XcodeProject,
29  {
30    targetName,
31    buildConfiguration = 'Release',
32  }: { targetName?: string; buildConfiguration?: string } = {}
33): XCBuildConfiguration | null {
34  const [, nativeTarget] = targetName
35    ? findNativeTargetByName(project, targetName)
36    : findFirstNativeTarget(project);
37  const [, xcBuildConfiguration] = getBuildConfigurationForListIdAndName(project, {
38    configurationListId: nativeTarget.buildConfigurationList,
39    buildConfiguration,
40  });
41  return xcBuildConfiguration ?? null;
42}
43
44export async function findApplicationTargetWithDependenciesAsync(
45  projectRoot: string,
46  scheme: string
47): Promise<Target> {
48  const applicationTargetName = await getApplicationTargetNameForSchemeAsync(projectRoot, scheme);
49  const project = getPbxproj(projectRoot);
50  const [, applicationTarget] = findNativeTargetByName(project, applicationTargetName);
51  const dependencies = getTargetDependencies(project, applicationTarget);
52  return {
53    name: trimQuotes(applicationTarget.name),
54    type: TargetType.APPLICATION,
55    dependencies,
56  };
57}
58
59function getTargetDependencies(
60  project: XcodeProject,
61  parentTarget: PBXNativeTarget
62): Target[] | undefined {
63  if (!parentTarget.dependencies || parentTarget.dependencies.length === 0) {
64    return undefined;
65  }
66  return parentTarget.dependencies.map(({ value }) => {
67    const { target: targetId } = project.getPBXGroupByKeyAndType(
68      value,
69      'PBXTargetDependency'
70    ) as PBXTargetDependency;
71
72    const [, target] = findNativeTargetById(project, targetId);
73
74    const type = isTargetOfType(target, TargetType.EXTENSION)
75      ? TargetType.EXTENSION
76      : TargetType.OTHER;
77    return {
78      name: trimQuotes(target.name),
79      type,
80      dependencies: getTargetDependencies(project, target),
81    };
82  });
83}
84
85export function isTargetOfType(target: PBXNativeTarget, targetType: TargetType): boolean {
86  return trimQuotes(target.productType) === targetType;
87}
88
89export function getNativeTargets(project: XcodeProject): NativeTargetSectionEntry[] {
90  const section = project.pbxNativeTargetSection();
91  return Object.entries(section).filter(isNotComment);
92}
93
94export function findSignableTargets(project: XcodeProject): NativeTargetSectionEntry[] {
95  const targets = getNativeTargets(project);
96
97  const signableTargetTypes = [
98    TargetType.APPLICATION,
99    TargetType.APP_CLIP,
100    TargetType.EXTENSION,
101    TargetType.WATCH,
102    TargetType.STICKER_PACK_EXTENSION,
103  ];
104
105  const applicationTargets = targets.filter(([, target]) => {
106    for (const targetType of signableTargetTypes) {
107      if (isTargetOfType(target, targetType)) {
108        return true;
109      }
110    }
111    return false;
112  });
113  if (applicationTargets.length === 0) {
114    throw new Error(`Could not find any signable targets in project.pbxproj`);
115  }
116  return applicationTargets;
117}
118
119export function findFirstNativeTarget(project: XcodeProject): NativeTargetSectionEntry {
120  const targets = getNativeTargets(project);
121  const applicationTargets = targets.filter(([, target]) =>
122    isTargetOfType(target, TargetType.APPLICATION)
123  );
124  if (applicationTargets.length === 0) {
125    throw new Error(`Could not find any application target in project.pbxproj`);
126  }
127  return applicationTargets[0];
128}
129
130export function findNativeTargetByName(
131  project: XcodeProject,
132  targetName: string
133): NativeTargetSectionEntry {
134  const nativeTargets = getNativeTargets(project);
135  const nativeTargetEntry = nativeTargets.find(([, i]) => trimQuotes(i.name) === targetName);
136  if (!nativeTargetEntry) {
137    throw new Error(`Could not find target '${targetName}' in project.pbxproj`);
138  }
139  return nativeTargetEntry;
140}
141
142function findNativeTargetById(project: XcodeProject, targetId: string): NativeTargetSectionEntry {
143  const nativeTargets = getNativeTargets(project);
144  const nativeTargetEntry = nativeTargets.find(([key]) => key === targetId);
145  if (!nativeTargetEntry) {
146    throw new Error(`Could not find target with id '${targetId}' in project.pbxproj`);
147  }
148  return nativeTargetEntry;
149}
150