1import { IOSConfig, XcodeProject } from '@expo/config-plugins';
2import fs from 'fs';
3
4export type CodeSigningInfo = Record<
5  string,
6  {
7    developmentTeams: string[];
8    provisioningProfiles: string[];
9  }
10>;
11
12/** Find the development team and provisioning profile that's currently in use by the Xcode project. */
13export function getCodeSigningInfoForPbxproj(projectRoot: string): CodeSigningInfo {
14  const project = IOSConfig.XcodeUtils.getPbxproj(projectRoot);
15  const targets = IOSConfig.Target.findSignableTargets(project);
16
17  const signingInfo: CodeSigningInfo = {};
18  for (const [nativeTargetId, nativeTarget] of targets) {
19    const developmentTeams: string[] = [];
20    const provisioningProfiles: string[] = [];
21
22    IOSConfig.XcodeUtils.getBuildConfigurationsForListId(
23      project,
24      nativeTarget.buildConfigurationList
25    )
26      .filter(
27        ([, item]: IOSConfig.XcodeUtils.ConfigurationSectionEntry) =>
28          item.buildSettings.PRODUCT_NAME
29      )
30      .forEach(([, item]: IOSConfig.XcodeUtils.ConfigurationSectionEntry) => {
31        const { DEVELOPMENT_TEAM, PROVISIONING_PROFILE } = item.buildSettings;
32        if (
33          typeof DEVELOPMENT_TEAM === 'string' &&
34          // If the user selects "Team: none" in Xcode, it'll be an empty string.
35          !!DEVELOPMENT_TEAM &&
36          // xcode package sometimes reads an empty string as a quoted empty string.
37          DEVELOPMENT_TEAM !== '""'
38        ) {
39          developmentTeams.push(DEVELOPMENT_TEAM);
40        }
41        if (typeof PROVISIONING_PROFILE === 'string' && !!PROVISIONING_PROFILE) {
42          provisioningProfiles.push(PROVISIONING_PROFILE);
43        }
44      });
45    signingInfo[nativeTargetId] = {
46      developmentTeams,
47      provisioningProfiles,
48    };
49  }
50
51  return signingInfo;
52}
53
54/**
55 * Set the development team and configure the Xcode project for automatic code signing,
56 * this helps us resolve the code signing on subsequent runs and emulates Xcode behavior.
57 *
58 * @param props.project xcode project object from `xcode` package.
59 * @param props.appleTeamId Apple Team ID to use for code signing.
60 */
61export function mutateXcodeProjectWithAutoCodeSigningInfo({
62  project,
63  appleTeamId,
64}: {
65  project: XcodeProject;
66  appleTeamId: string;
67}): XcodeProject {
68  const targets = IOSConfig.Target.findSignableTargets(project);
69
70  const quotedAppleTeamId = ensureQuotes(appleTeamId);
71
72  for (const [nativeTargetId, nativeTarget] of targets) {
73    IOSConfig.XcodeUtils.getBuildConfigurationsForListId(
74      project,
75      nativeTarget.buildConfigurationList
76    )
77      .filter(
78        ([, item]: IOSConfig.XcodeUtils.ConfigurationSectionEntry) =>
79          item.buildSettings.PRODUCT_NAME
80      )
81      .forEach(([, item]: IOSConfig.XcodeUtils.ConfigurationSectionEntry) => {
82        item.buildSettings.DEVELOPMENT_TEAM = quotedAppleTeamId;
83        item.buildSettings.CODE_SIGN_IDENTITY = '"Apple Development"';
84        item.buildSettings.CODE_SIGN_STYLE = 'Automatic';
85      });
86
87    Object.entries(IOSConfig.XcodeUtils.getProjectSection(project))
88      .filter(IOSConfig.XcodeUtils.isNotComment)
89      .forEach(([, item]: IOSConfig.XcodeUtils.ProjectSectionEntry) => {
90        if (!item.attributes.TargetAttributes) {
91          item.attributes.TargetAttributes = {};
92        }
93
94        if (!item.attributes.TargetAttributes[nativeTargetId]) {
95          item.attributes.TargetAttributes[nativeTargetId] = {};
96        }
97
98        item.attributes.TargetAttributes[nativeTargetId].DevelopmentTeam = quotedAppleTeamId;
99        item.attributes.TargetAttributes[nativeTargetId].ProvisioningStyle = 'Automatic';
100      });
101  }
102
103  return project;
104}
105
106/**
107 * Configures the Xcode project for automatic code signing and persists the results.
108 */
109export function setAutoCodeSigningInfoForPbxproj(
110  projectRoot: string,
111  { appleTeamId }: { appleTeamId: string }
112): void {
113  const project = IOSConfig.XcodeUtils.getPbxproj(projectRoot);
114  mutateXcodeProjectWithAutoCodeSigningInfo({ project, appleTeamId });
115
116  fs.writeFileSync(project.filepath, project.writeSync());
117}
118
119const ensureQuotes = (value: string) => {
120  if (!value.match(/^['"]/)) {
121    return `"${value}"`;
122  }
123  return value;
124};
125