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