import { findSchemeNames, findSchemePaths } from './Paths'; import { findSignableTargets, TargetType } from './Target'; import { getPbxproj, unquote } from './utils/Xcodeproj'; import { readXMLAsync } from '../utils/XML'; interface SchemeXML { Scheme?: { BuildAction?: { BuildActionEntries?: { BuildActionEntry?: BuildActionEntryType[]; }[]; }[]; ArchiveAction?: { $?: { buildConfiguration?: string; }; }[]; }; } interface BuildActionEntryType { BuildableReference?: { $?: { BlueprintName?: string; BuildableName?: string; }; }[]; } export function getSchemesFromXcodeproj(projectRoot: string): string[] { return findSchemeNames(projectRoot); } export function getRunnableSchemesFromXcodeproj( projectRoot: string, { configuration = 'Debug' }: { configuration?: 'Debug' | 'Release' } = {} ): { name: string; osType: string; type: string }[] { const project = getPbxproj(projectRoot); return findSignableTargets(project).map(([, target]) => { let osType = 'iOS'; const type = unquote(target.productType); if (type === TargetType.WATCH) { osType = 'watchOS'; } else if ( // (apps) com.apple.product-type.application // (app clips) com.apple.product-type.application.on-demand-install-capable // NOTE(EvanBacon): This matches against `watchOS` as well so we check for watch first. type.startsWith(TargetType.APPLICATION) ) { // Attempt to resolve the platform SDK for each target so we can filter devices. const xcConfigurationList = project.hash.project.objects.XCConfigurationList[target.buildConfigurationList]; if (xcConfigurationList) { const buildConfiguration = xcConfigurationList.buildConfigurations.find( (value: { comment: string; value: string }) => value.comment === configuration ) || xcConfigurationList.buildConfigurations[0]; if (buildConfiguration?.value) { const xcBuildConfiguration = project.hash.project.objects.XCBuildConfiguration?.[buildConfiguration.value]; const buildSdkRoot = xcBuildConfiguration.buildSettings.SDKROOT; if ( buildSdkRoot === 'appletvos' || 'TVOS_DEPLOYMENT_TARGET' in xcBuildConfiguration.buildSettings ) { // Is a TV app... osType = 'tvOS'; } else if (buildSdkRoot === 'iphoneos') { osType = 'iOS'; } } } } return { name: unquote(target.name), osType, type: unquote(target.productType), }; }); } async function readSchemeAsync( projectRoot: string, scheme: string ): Promise { const allSchemePaths = findSchemePaths(projectRoot); const re = new RegExp(`/${scheme}.xcscheme`, 'i'); const schemePath = allSchemePaths.find((i) => re.exec(i)); if (schemePath) { return (await readXMLAsync({ path: schemePath })) as unknown as SchemeXML | undefined; } else { throw new Error(`scheme '${scheme}' does not exist, make sure it's marked as shared`); } } export async function getApplicationTargetNameForSchemeAsync( projectRoot: string, scheme: string ): Promise { const schemeXML = await readSchemeAsync(projectRoot, scheme); const buildActionEntry = schemeXML?.Scheme?.BuildAction?.[0]?.BuildActionEntries?.[0]?.BuildActionEntry; const targetName = buildActionEntry?.length === 1 ? getBlueprintName(buildActionEntry[0]) : getBlueprintName( buildActionEntry?.find((entry) => { return entry.BuildableReference?.[0]?.['$']?.BuildableName?.endsWith('.app'); }) ); if (!targetName) { throw new Error(`${scheme}.xcscheme seems to be corrupted`); } return targetName; } export async function getArchiveBuildConfigurationForSchemeAsync( projectRoot: string, scheme: string ): Promise { const schemeXML = await readSchemeAsync(projectRoot, scheme); const buildConfiguration = schemeXML?.Scheme?.ArchiveAction?.[0]?.['$']?.buildConfiguration; if (!buildConfiguration) { throw new Error(`${scheme}.xcscheme seems to be corrupted`); } return buildConfiguration; } function getBlueprintName(entry?: BuildActionEntryType): string | undefined { return entry?.BuildableReference?.[0]?.['$']?.BlueprintName; }