1import { findSchemeNames, findSchemePaths } from './Paths';
2import { findSignableTargets, TargetType } from './Target';
3import { getPbxproj, unquote } from './utils/Xcodeproj';
4import { readXMLAsync } from '../utils/XML';
5
6interface SchemeXML {
7  Scheme?: {
8    BuildAction?: {
9      BuildActionEntries?: {
10        BuildActionEntry?: BuildActionEntryType[];
11      }[];
12    }[];
13    ArchiveAction?: {
14      $?: {
15        buildConfiguration?: string;
16      };
17    }[];
18  };
19}
20
21interface BuildActionEntryType {
22  BuildableReference?: {
23    $?: {
24      BlueprintName?: string;
25      BuildableName?: string;
26    };
27  }[];
28}
29
30export function getSchemesFromXcodeproj(projectRoot: string): string[] {
31  return findSchemeNames(projectRoot);
32}
33
34export function getRunnableSchemesFromXcodeproj(
35  projectRoot: string,
36  { configuration = 'Debug' }: { configuration?: 'Debug' | 'Release' } = {}
37): { name: string; osType: string; type: string }[] {
38  const project = getPbxproj(projectRoot);
39
40  return findSignableTargets(project).map(([, target]) => {
41    let osType = 'iOS';
42    const type = unquote(target.productType);
43
44    if (type === TargetType.WATCH) {
45      osType = 'watchOS';
46    } else if (
47      // (apps) com.apple.product-type.application
48      // (app clips) com.apple.product-type.application.on-demand-install-capable
49      // NOTE(EvanBacon): This matches against `watchOS` as well so we check for watch first.
50      type.startsWith(TargetType.APPLICATION)
51    ) {
52      // Attempt to resolve the platform SDK for each target so we can filter devices.
53      const xcConfigurationList =
54        project.hash.project.objects.XCConfigurationList[target.buildConfigurationList];
55
56      if (xcConfigurationList) {
57        const buildConfiguration =
58          xcConfigurationList.buildConfigurations.find(
59            (value: { comment: string; value: string }) => value.comment === configuration
60          ) || xcConfigurationList.buildConfigurations[0];
61        if (buildConfiguration?.value) {
62          const xcBuildConfiguration =
63            project.hash.project.objects.XCBuildConfiguration?.[buildConfiguration.value];
64
65          const buildSdkRoot = xcBuildConfiguration.buildSettings.SDKROOT;
66          if (
67            buildSdkRoot === 'appletvos' ||
68            'TVOS_DEPLOYMENT_TARGET' in xcBuildConfiguration.buildSettings
69          ) {
70            // Is a TV app...
71            osType = 'tvOS';
72          } else if (buildSdkRoot === 'iphoneos') {
73            osType = 'iOS';
74          }
75        }
76      }
77    }
78
79    return {
80      name: unquote(target.name),
81      osType,
82      type: unquote(target.productType),
83    };
84  });
85}
86
87async function readSchemeAsync(
88  projectRoot: string,
89  scheme: string
90): Promise<SchemeXML | undefined> {
91  const allSchemePaths = findSchemePaths(projectRoot);
92  const re = new RegExp(`/${scheme}.xcscheme`, 'i');
93  const schemePath = allSchemePaths.find((i) => re.exec(i));
94  if (schemePath) {
95    return (await readXMLAsync({ path: schemePath })) as unknown as SchemeXML | undefined;
96  } else {
97    throw new Error(`scheme '${scheme}' does not exist, make sure it's marked as shared`);
98  }
99}
100
101export async function getApplicationTargetNameForSchemeAsync(
102  projectRoot: string,
103  scheme: string
104): Promise<string> {
105  const schemeXML = await readSchemeAsync(projectRoot, scheme);
106  const buildActionEntry =
107    schemeXML?.Scheme?.BuildAction?.[0]?.BuildActionEntries?.[0]?.BuildActionEntry;
108  const targetName =
109    buildActionEntry?.length === 1
110      ? getBlueprintName(buildActionEntry[0])
111      : getBlueprintName(
112          buildActionEntry?.find((entry) => {
113            return entry.BuildableReference?.[0]?.['$']?.BuildableName?.endsWith('.app');
114          })
115        );
116  if (!targetName) {
117    throw new Error(`${scheme}.xcscheme seems to be corrupted`);
118  }
119  return targetName;
120}
121
122export async function getArchiveBuildConfigurationForSchemeAsync(
123  projectRoot: string,
124  scheme: string
125): Promise<string> {
126  const schemeXML = await readSchemeAsync(projectRoot, scheme);
127  const buildConfiguration = schemeXML?.Scheme?.ArchiveAction?.[0]?.['$']?.buildConfiguration;
128  if (!buildConfiguration) {
129    throw new Error(`${scheme}.xcscheme seems to be corrupted`);
130  }
131  return buildConfiguration;
132}
133
134function getBlueprintName(entry?: BuildActionEntryType): string | undefined {
135  return entry?.BuildableReference?.[0]?.['$']?.BlueprintName;
136}
137