1082815dcSEvan Baconimport { PBXNativeTarget, PBXTargetDependency, XCBuildConfiguration, XcodeProject } from 'xcode';
2082815dcSEvan Bacon
3082815dcSEvan Baconimport { getApplicationTargetNameForSchemeAsync } from './BuildScheme';
4082815dcSEvan Baconimport {
5082815dcSEvan Bacon  getBuildConfigurationForListIdAndName,
6082815dcSEvan Bacon  getPbxproj,
7082815dcSEvan Bacon  isNotComment,
8082815dcSEvan Bacon  NativeTargetSectionEntry,
9082815dcSEvan Bacon} from './utils/Xcodeproj';
10082815dcSEvan Baconimport { trimQuotes } from './utils/string';
11082815dcSEvan Bacon
12082815dcSEvan Baconexport enum TargetType {
13082815dcSEvan Bacon  APPLICATION = 'com.apple.product-type.application',
14082815dcSEvan Bacon  EXTENSION = 'com.apple.product-type.app-extension',
15082815dcSEvan Bacon  WATCH = 'com.apple.product-type.application.watchapp',
16082815dcSEvan Bacon  APP_CLIP = 'com.apple.product-type.application.on-demand-install-capable',
17082815dcSEvan Bacon  STICKER_PACK_EXTENSION = 'com.apple.product-type.app-extension.messages-sticker-pack',
18*f421f4ecSDominik Sokal  FRAMEWORK = 'com.apple.product-type.framework',
19082815dcSEvan Bacon  OTHER = 'other',
20082815dcSEvan Bacon}
21082815dcSEvan Bacon
22082815dcSEvan Baconexport interface Target {
23082815dcSEvan Bacon  name: string;
24082815dcSEvan Bacon  type: TargetType;
25*f421f4ecSDominik Sokal  signable: boolean;
26082815dcSEvan Bacon  dependencies?: Target[];
27082815dcSEvan Bacon}
28082815dcSEvan Bacon
29082815dcSEvan Baconexport function getXCBuildConfigurationFromPbxproj(
30082815dcSEvan Bacon  project: XcodeProject,
31082815dcSEvan Bacon  {
32082815dcSEvan Bacon    targetName,
33082815dcSEvan Bacon    buildConfiguration = 'Release',
34082815dcSEvan Bacon  }: { targetName?: string; buildConfiguration?: string } = {}
35082815dcSEvan Bacon): XCBuildConfiguration | null {
36082815dcSEvan Bacon  const [, nativeTarget] = targetName
37082815dcSEvan Bacon    ? findNativeTargetByName(project, targetName)
38082815dcSEvan Bacon    : findFirstNativeTarget(project);
39082815dcSEvan Bacon  const [, xcBuildConfiguration] = getBuildConfigurationForListIdAndName(project, {
40082815dcSEvan Bacon    configurationListId: nativeTarget.buildConfigurationList,
41082815dcSEvan Bacon    buildConfiguration,
42082815dcSEvan Bacon  });
43082815dcSEvan Bacon  return xcBuildConfiguration ?? null;
44082815dcSEvan Bacon}
45082815dcSEvan Bacon
46082815dcSEvan Baconexport async function findApplicationTargetWithDependenciesAsync(
47082815dcSEvan Bacon  projectRoot: string,
48082815dcSEvan Bacon  scheme: string
49082815dcSEvan Bacon): Promise<Target> {
50082815dcSEvan Bacon  const applicationTargetName = await getApplicationTargetNameForSchemeAsync(projectRoot, scheme);
51082815dcSEvan Bacon  const project = getPbxproj(projectRoot);
52082815dcSEvan Bacon  const [, applicationTarget] = findNativeTargetByName(project, applicationTargetName);
53082815dcSEvan Bacon  const dependencies = getTargetDependencies(project, applicationTarget);
54082815dcSEvan Bacon  return {
55082815dcSEvan Bacon    name: trimQuotes(applicationTarget.name),
56082815dcSEvan Bacon    type: TargetType.APPLICATION,
57*f421f4ecSDominik Sokal    signable: true,
58082815dcSEvan Bacon    dependencies,
59082815dcSEvan Bacon  };
60082815dcSEvan Bacon}
61082815dcSEvan Bacon
62082815dcSEvan Baconfunction getTargetDependencies(
63082815dcSEvan Bacon  project: XcodeProject,
64082815dcSEvan Bacon  parentTarget: PBXNativeTarget
65082815dcSEvan Bacon): Target[] | undefined {
66082815dcSEvan Bacon  if (!parentTarget.dependencies || parentTarget.dependencies.length === 0) {
67082815dcSEvan Bacon    return undefined;
68082815dcSEvan Bacon  }
69*f421f4ecSDominik Sokal
70*f421f4ecSDominik Sokal  const nonSignableTargetTypes: TargetType[] = [TargetType.FRAMEWORK];
71*f421f4ecSDominik Sokal
72082815dcSEvan Bacon  return parentTarget.dependencies.map(({ value }) => {
73082815dcSEvan Bacon    const { target: targetId } = project.getPBXGroupByKeyAndType(
74082815dcSEvan Bacon      value,
75082815dcSEvan Bacon      'PBXTargetDependency'
76082815dcSEvan Bacon    ) as PBXTargetDependency;
77082815dcSEvan Bacon
78082815dcSEvan Bacon    const [, target] = findNativeTargetById(project, targetId);
79082815dcSEvan Bacon
80082815dcSEvan Bacon    const type = isTargetOfType(target, TargetType.EXTENSION)
81082815dcSEvan Bacon      ? TargetType.EXTENSION
82082815dcSEvan Bacon      : TargetType.OTHER;
83082815dcSEvan Bacon    return {
84082815dcSEvan Bacon      name: trimQuotes(target.name),
85082815dcSEvan Bacon      type,
86*f421f4ecSDominik Sokal      signable: !nonSignableTargetTypes.some((signableTargetType) =>
87*f421f4ecSDominik Sokal        isTargetOfType(target, signableTargetType)
88*f421f4ecSDominik Sokal      ),
89082815dcSEvan Bacon      dependencies: getTargetDependencies(project, target),
90082815dcSEvan Bacon    };
91082815dcSEvan Bacon  });
92082815dcSEvan Bacon}
93082815dcSEvan Bacon
94082815dcSEvan Baconexport function isTargetOfType(target: PBXNativeTarget, targetType: TargetType): boolean {
95082815dcSEvan Bacon  return trimQuotes(target.productType) === targetType;
96082815dcSEvan Bacon}
97082815dcSEvan Bacon
98082815dcSEvan Baconexport function getNativeTargets(project: XcodeProject): NativeTargetSectionEntry[] {
99082815dcSEvan Bacon  const section = project.pbxNativeTargetSection();
100082815dcSEvan Bacon  return Object.entries(section).filter(isNotComment);
101082815dcSEvan Bacon}
102082815dcSEvan Bacon
103082815dcSEvan Baconexport function findSignableTargets(project: XcodeProject): NativeTargetSectionEntry[] {
104082815dcSEvan Bacon  const targets = getNativeTargets(project);
105082815dcSEvan Bacon
106*f421f4ecSDominik Sokal  const signableTargetTypes: TargetType[] = [
107082815dcSEvan Bacon    TargetType.APPLICATION,
108082815dcSEvan Bacon    TargetType.APP_CLIP,
109082815dcSEvan Bacon    TargetType.EXTENSION,
110082815dcSEvan Bacon    TargetType.WATCH,
111082815dcSEvan Bacon    TargetType.STICKER_PACK_EXTENSION,
112082815dcSEvan Bacon  ];
113082815dcSEvan Bacon
114082815dcSEvan Bacon  const applicationTargets = targets.filter(([, target]) => {
115082815dcSEvan Bacon    for (const targetType of signableTargetTypes) {
116082815dcSEvan Bacon      if (isTargetOfType(target, targetType)) {
117082815dcSEvan Bacon        return true;
118082815dcSEvan Bacon      }
119082815dcSEvan Bacon    }
120082815dcSEvan Bacon    return false;
121082815dcSEvan Bacon  });
122082815dcSEvan Bacon  if (applicationTargets.length === 0) {
123082815dcSEvan Bacon    throw new Error(`Could not find any signable targets in project.pbxproj`);
124082815dcSEvan Bacon  }
125082815dcSEvan Bacon  return applicationTargets;
126082815dcSEvan Bacon}
127082815dcSEvan Bacon
128082815dcSEvan Baconexport function findFirstNativeTarget(project: XcodeProject): NativeTargetSectionEntry {
129082815dcSEvan Bacon  const targets = getNativeTargets(project);
130082815dcSEvan Bacon  const applicationTargets = targets.filter(([, target]) =>
131082815dcSEvan Bacon    isTargetOfType(target, TargetType.APPLICATION)
132082815dcSEvan Bacon  );
133082815dcSEvan Bacon  if (applicationTargets.length === 0) {
134082815dcSEvan Bacon    throw new Error(`Could not find any application target in project.pbxproj`);
135082815dcSEvan Bacon  }
136082815dcSEvan Bacon  return applicationTargets[0];
137082815dcSEvan Bacon}
138082815dcSEvan Bacon
139082815dcSEvan Baconexport function findNativeTargetByName(
140082815dcSEvan Bacon  project: XcodeProject,
141082815dcSEvan Bacon  targetName: string
142082815dcSEvan Bacon): NativeTargetSectionEntry {
143082815dcSEvan Bacon  const nativeTargets = getNativeTargets(project);
144082815dcSEvan Bacon  const nativeTargetEntry = nativeTargets.find(([, i]) => trimQuotes(i.name) === targetName);
145082815dcSEvan Bacon  if (!nativeTargetEntry) {
146082815dcSEvan Bacon    throw new Error(`Could not find target '${targetName}' in project.pbxproj`);
147082815dcSEvan Bacon  }
148082815dcSEvan Bacon  return nativeTargetEntry;
149082815dcSEvan Bacon}
150082815dcSEvan Bacon
151082815dcSEvan Baconfunction findNativeTargetById(project: XcodeProject, targetId: string): NativeTargetSectionEntry {
152082815dcSEvan Bacon  const nativeTargets = getNativeTargets(project);
153082815dcSEvan Bacon  const nativeTargetEntry = nativeTargets.find(([key]) => key === targetId);
154082815dcSEvan Bacon  if (!nativeTargetEntry) {
155082815dcSEvan Bacon    throw new Error(`Could not find target with id '${targetId}' in project.pbxproj`);
156082815dcSEvan Bacon  }
157082815dcSEvan Bacon  return nativeTargetEntry;
158082815dcSEvan Bacon}
159