@sentry/wizard
Version:
Sentry wizard helping you to configure your project
1,023 lines (1,022 loc) • 52.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.XcodeProject = void 0;
/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
// @ts-expect-error - clack is ESM and TS complains about that. It works though
const clack = __importStar(require("@clack/prompts"));
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const debug_1 = require("../utils/debug");
const templates = __importStar(require("./templates"));
const xcode_1 = require("xcode");
const macos_system_helper_1 = require("./macos-system-helper");
function setDebugInformationFormatAndSandbox(proj, targetName) {
const xcObjects = proj.hash.project.objects;
if (!xcObjects.PBXNativeTarget) {
xcObjects.PBXNativeTarget = {};
}
const targetKey = Object.keys(xcObjects.PBXNativeTarget).filter((key) => {
const value = xcObjects.PBXNativeTarget?.[key];
return (!key.endsWith('_comment') &&
typeof value !== 'string' &&
value?.name === targetName);
})[0];
const target = xcObjects.PBXNativeTarget[targetKey];
if (!xcObjects.XCBuildConfiguration) {
xcObjects.XCBuildConfiguration = {};
}
if (!xcObjects.XCConfigurationList) {
xcObjects.XCConfigurationList = {};
}
const buildConfigurationListId = target?.buildConfigurationList ?? '';
const configurationList = xcObjects.XCConfigurationList?.[buildConfigurationListId];
const buildListConfigurationIds = configurationList?.buildConfigurations ?? [];
for (const buildListConfigId of buildListConfigurationIds) {
const config = xcObjects.XCBuildConfiguration[buildListConfigId.value] ?? {};
if (typeof config === 'string') {
// Ignore comments
continue;
}
const buildSettings = config.buildSettings ?? {};
buildSettings.DEBUG_INFORMATION_FORMAT = '"dwarf-with-dsym"';
buildSettings.ENABLE_USER_SCRIPT_SANDBOXING = '"NO"';
config.buildSettings = buildSettings;
xcObjects.XCBuildConfiguration[buildListConfigId.value] = config;
}
}
function addSentrySPM(proj, targetName) {
const xcObjects = proj.hash.project.objects;
const sentryFrameworkUUID = proj.generateUuid();
const sentrySPMUUID = proj.generateUuid();
// Check whether xcObjects already have sentry framework
if (xcObjects.PBXFrameworksBuildPhase) {
for (const key in xcObjects.PBXFrameworksBuildPhase || {}) {
const frameworkBuildPhase = xcObjects.PBXFrameworksBuildPhase[key];
if (key.endsWith('_comment') || typeof frameworkBuildPhase === 'string') {
// Ignore comments
continue;
}
for (const framework of frameworkBuildPhase.files ?? []) {
// We identify the Sentry framework by the comment "Sentry in Frameworks",
// which is set by this manager in previous runs.
if (framework.comment === 'Sentry in Frameworks') {
return;
}
}
}
}
if (!xcObjects.PBXBuildFile) {
xcObjects.PBXBuildFile = {};
}
xcObjects.PBXBuildFile[sentryFrameworkUUID] = {
isa: 'PBXBuildFile',
productRef: sentrySPMUUID,
productRef_comment: 'Sentry',
};
xcObjects.PBXBuildFile[`${sentryFrameworkUUID}_comment`] =
'Sentry in Frameworks';
if (!xcObjects.PBXFrameworksBuildPhase) {
xcObjects.PBXFrameworksBuildPhase = {};
}
for (const key in xcObjects.PBXFrameworksBuildPhase) {
const value = xcObjects.PBXFrameworksBuildPhase[key];
if (key.endsWith('_comment') || typeof value === 'string') {
// Ignore comments
continue;
}
const frameworks = value.files ?? [];
frameworks.push({
value: sentryFrameworkUUID,
comment: 'Sentry in Frameworks',
});
value.files = frameworks;
xcObjects.PBXFrameworksBuildPhase[key] = value;
}
if (!xcObjects.PBXNativeTarget) {
xcObjects.PBXNativeTarget = {};
}
const targetKey = Object.keys(xcObjects.PBXNativeTarget || {}).filter((key) => {
const value = xcObjects.PBXNativeTarget?.[key];
return (!key.endsWith('_comment') &&
typeof value !== 'string' &&
value?.name === targetName);
})[0];
const target = xcObjects.PBXNativeTarget[targetKey];
if (!target.packageProductDependencies) {
target.packageProductDependencies = [];
}
target.packageProductDependencies.push({
value: sentrySPMUUID,
comment: 'Sentry',
});
const sentrySwiftPackageUUID = proj.generateUuid();
const xcProject = proj.getFirstProject().firstProject;
if (!xcProject.packageReferences) {
xcProject.packageReferences = [];
}
xcProject.packageReferences.push({
value: sentrySwiftPackageUUID,
comment: 'XCRemoteSwiftPackageReference "sentry-cocoa"',
});
if (!xcObjects.XCRemoteSwiftPackageReference) {
xcObjects.XCRemoteSwiftPackageReference = {};
}
xcObjects.XCRemoteSwiftPackageReference[sentrySwiftPackageUUID] = {
isa: 'XCRemoteSwiftPackageReference',
repositoryURL: '"https://github.com/getsentry/sentry-cocoa/"',
requirement: {
kind: 'upToNextMajorVersion',
minimumVersion: '8.0.0',
},
};
xcObjects.XCRemoteSwiftPackageReference[`${sentrySwiftPackageUUID}_comment`] =
'XCRemoteSwiftPackageReference "sentry-cocoa"';
if (!xcObjects.XCSwiftPackageProductDependency) {
xcObjects.XCSwiftPackageProductDependency = {};
}
xcObjects.XCSwiftPackageProductDependency[sentrySPMUUID] = {
isa: 'XCSwiftPackageProductDependency',
package: sentrySwiftPackageUUID,
package_comment: 'XCRemoteSwiftPackageReference "sentry-cocoa"',
productName: 'Sentry',
};
xcObjects.XCSwiftPackageProductDependency[`${sentrySPMUUID}_comment`] =
'Sentry';
clack.log.step('Added Sentry SPM dependency to your project');
}
class XcodeProject {
/**
* The directory where the Xcode project is located.
*/
baseDir;
/**
* The path to the `<PROJECT>.xcodeproj` directory.
*/
xcodeprojPath;
/**
* The path to the `project.pbxproj` file.
*/
pbxprojPath;
/**
* The Xcode project object.
*/
project;
objects;
/**
* Creates a new XcodeProject instance, a wrapper around the Xcode project file `<PROJECT>.xcodeproj/project.pbxproj`.
*
* @param projectPath - The path to the Xcode project file
*/
constructor(projectPath) {
this.pbxprojPath = projectPath;
this.xcodeprojPath = path.dirname(projectPath);
this.baseDir = path.dirname(this.xcodeprojPath);
this.project = (0, xcode_1.project)(projectPath);
this.project.parseSync();
this.objects = this.project.hash.project.objects;
}
getAllTargets() {
const targets = this.objects.PBXNativeTarget ?? {};
return Object.keys(targets)
.filter((key) => {
const value = targets[key];
return (!key.endsWith('_comment') &&
typeof value !== 'string' &&
value.productType.startsWith('"com.apple.product-type.application'));
})
.map((key) => {
return targets[key].name;
});
}
updateXcodeProject(sentryProject, target, addSPMReference, uploadSource = true) {
this.addUploadSymbolsScript({
sentryProject,
targetName: target,
uploadSource,
});
if (uploadSource) {
setDebugInformationFormatAndSandbox(this.project, target);
}
if (addSPMReference) {
addSentrySPM(this.project, target);
}
const newContent = this.project.writeSync();
fs.writeFileSync(this.pbxprojPath, newContent);
}
addUploadSymbolsScript({ sentryProject, targetName, uploadSource, }) {
const xcObjects = this.project.hash.project.objects;
if (!xcObjects.PBXNativeTarget) {
xcObjects.PBXNativeTarget = {};
}
const targetKey = Object.keys(xcObjects.PBXNativeTarget).filter((key) => {
const value = xcObjects.PBXNativeTarget?.[key];
return (!key.endsWith('_comment') &&
typeof value !== 'string' &&
value?.name === targetName);
})[0];
const target = xcObjects.PBXNativeTarget[targetKey];
if (!target) {
(0, debug_1.debug)(`Target not found: ${targetName}`);
return;
}
// Generate the new script content
const isHomebrewInstalled = fs.existsSync('/opt/homebrew/bin/sentry-cli');
const shellScript = templates.getRunScriptTemplate(sentryProject.organization.slug, sentryProject.slug, uploadSource, isHomebrewInstalled);
if (!xcObjects.PBXShellScriptBuildPhase) {
xcObjects.PBXShellScriptBuildPhase = {};
}
// Look for existing Sentry build phase in the current target
let existingSentryBuildPhaseId;
let existingSentryBuildPhase;
// Check target's build phases for existing Sentry script by searching for a build phase that contains "sentry-cli" in the shell script
if (target.buildPhases) {
for (const phase of target.buildPhases) {
const buildPhase = xcObjects.PBXShellScriptBuildPhase[phase.value];
if (typeof buildPhase === 'object' &&
buildPhase.shellScript?.includes('sentry-cli')) {
existingSentryBuildPhaseId = phase.value;
existingSentryBuildPhase = buildPhase;
break;
}
}
}
// Clean up orphaned build phase references that may exist from previous runs
// Find all build phase IDs that are referenced in targets but don't exist in PBXShellScriptBuildPhase
const orphanedBuildPhaseIds = [];
for (const targetKey in xcObjects.PBXNativeTarget) {
const targetValue = xcObjects.PBXNativeTarget[targetKey];
if (typeof targetValue === 'object' && targetValue.buildPhases) {
for (const phase of targetValue.buildPhases) {
// Check if this is a shell script build phase that doesn't exist
if (!xcObjects.PBXShellScriptBuildPhase?.[phase.value] &&
phase.comment?.includes('Upload Debug Symbols to Sentry')) {
orphanedBuildPhaseIds.push(phase.value);
}
}
}
}
// Remove orphaned build phase references from all targets
if (orphanedBuildPhaseIds.length > 0) {
for (const targetKey in xcObjects.PBXNativeTarget) {
const targetValue = xcObjects.PBXNativeTarget[targetKey];
if (typeof targetValue === 'object' && targetValue.buildPhases) {
targetValue.buildPhases = targetValue.buildPhases.filter((phase) => {
return !orphanedBuildPhaseIds.includes(phase.value);
});
}
}
}
if (existingSentryBuildPhaseId && existingSentryBuildPhase) {
// Update existing build phase instead of adding a new one
// This call is idempotent, so it will not add a new build phase if it already exists
this.updateScriptBuildPhase(existingSentryBuildPhaseId, shellScript, [
templates.scriptInputPath,
]);
clack.log.step(`Updated existing Sentry upload script for "${targetName}" build phase`);
}
else {
// Add new build phase to the target
this.addScriptBuildPhase(targetKey, 'Upload Debug Symbols to Sentry', shellScript, [templates.scriptInputPath]);
clack.log.step(`Added Sentry upload script to "${targetName}" build phase`);
}
}
/**
* Retrieves all source files associated with a specific target in the Xcode project.
* This is used to find files where we can inject Sentry initialization code.
*
* @param targetName - The name of the target to get files for
* @returns An array of absolute file paths for the target's source files, or undefined if target not found
*/
getSourceFilesForTarget(targetName) {
// ## Summary how Xcode Projects are structured:
// - Every Xcode Project has exactly one main group of type `PBXGroup`
// - The main group contains a list of children identifiers
// - Each child can be a `PBXGroup`, a `PBXFileReference` or a `PBXFileSystemSynchronizedRootGroup`
// - Each `PBXGroup` has a list of children identifiers which again can be `PBXGroup`, `PBXFileReference` or `PBXFileSystemSynchronizedRootGroup`
// - The target defines the list of `fileSystemSynchronizedGroups` which are `PBXFileSystemSynchronizedRootGroup` to be included in the build phase
// - The `PBXFileSystemSynchronizedRootGroup` has a list of `exceptions` which are `PBXFileSystemSynchronizedBuildFileExceptionSet`
// - Each `PBXFileSystemSynchronizedBuildFileExceptionSet` represents a folder to be excluded from the build.
// - The `PBXFileSystemSynchronizedBuildFileExceptionSet` has a list of `membershipExceptions` which are files to be excluded from being excluded, therefore included in the build.
// - The Xcode project has a build phase `PBXSourcesBuildPhase` which has a list of `files` which are `PBXBuildFile`
// - A file which is not part of a `PBXFileSystemSynchronizedRootGroup` must be added to the `files` list of the `PBXSourcesBuildPhase` build phase
// - Nested subfolders in `fileSystemSynchronizedGroups` are not declared but recursively included
//
// Based on the findings above the files included in the build phase are:
// - All files in the `files` of the `PBXSourcesBuildPhase` build phase `Sources` of the target
// - All files in directories of the `fileSystemSynchronizedGroups` of the target
// - Excluding all files in the `exceptions` of the `PBXFileSystemSynchronizedRootGroup` of the target
// - Including all files in the `membershipExceptions` of the `PBXFileSystemSynchronizedBuildFileExceptionSet` of the target
const nativeTarget = this.findNativeTargetByName(targetName);
if (!nativeTarget) {
(0, debug_1.debug)('Target not found: ' + targetName);
return undefined;
}
const filesInBuildPhase = this.findFilesInSourceBuildPhase(nativeTarget);
(0, debug_1.debug)(`Found ${filesInBuildPhase.length} files in build phase for target: ${targetName}`);
const filesInSynchronizedRootGroups = this.findFilesInSynchronizedRootGroups(nativeTarget);
(0, debug_1.debug)(`Found ${filesInSynchronizedRootGroups.length} files in synchronized root groups for target: ${targetName}`);
return [...filesInBuildPhase, ...filesInSynchronizedRootGroups];
}
// ================================ TARGET HELPERS ================================
/**
* Finds a native target by name.
*
* @param targetName - The name of the target to find
* @returns The native target, or undefined if the target is not found
*/
findNativeTargetByName(targetName) {
(0, debug_1.debug)('Finding native target by name: ' + targetName);
if (!this.objects.PBXNativeTarget) {
(0, debug_1.debug)('No native targets found');
return undefined;
}
const nativeTargets = Object.entries(this.objects.PBXNativeTarget);
for (const [key, target] of nativeTargets) {
// Ignore comments
if (key.endsWith('_comment') || typeof target === 'string') {
continue;
}
// Ignore targets that are not the target we are looking for
if (target.name !== targetName) {
continue;
}
(0, debug_1.debug)('Found native target: ' + targetName);
return {
id: key,
obj: target,
};
}
(0, debug_1.debug)('Target not found: ' + targetName);
return undefined;
}
// ================================ BUILD PHASE HELPERS ================================
/**
* Finds the source build phase in a target.
*
* @param target - The target to find the source build phase in
* @returns The source build phase, or undefined if the target is not found or has no source build phase
*/
findSourceBuildPhaseInTarget(target) {
(0, debug_1.debug)(`Finding source build phase in target: ${target.name}`);
if (!target.buildPhases) {
(0, debug_1.debug)('No build phases found for target: ' + target.name);
return undefined;
}
for (const phase of target.buildPhases) {
const buildPhaseId = phase.value;
const buildPhase = this.objects.PBXSourcesBuildPhase?.[buildPhaseId];
if (typeof buildPhase !== 'object') {
// Ignore comments
continue;
}
(0, debug_1.debug)(`Found source build phase: ${buildPhaseId} for target: ${target.name}`);
return {
id: buildPhaseId,
obj: buildPhase,
};
}
(0, debug_1.debug)(`No source build phase found for target: ${target.name}`);
return undefined;
}
/**
* Adds a new script build phase to the specified target.
*
* @param targetKey - The key of the target to add the build phase to
* @param name - The name of the build phase
* @param script - The shell script content
* @param inputPaths - Array of input paths for the script
* @returns The UUID of the created build phase
*/
addScriptBuildPhase(targetKey, name, script, inputPaths = []) {
const buildPhaseUuid = this.project.generateUuid();
const escapedScript = script.replace(/"/g, '\\"');
// Create the shell script build phase object
const buildPhase = {
isa: 'PBXShellScriptBuildPhase',
buildActionMask: 2147483647,
files: [],
inputPaths,
outputPaths: [],
runOnlyForDeploymentPostprocessing: 0,
shellPath: '/bin/sh',
shellScript: `"${escapedScript}"`,
name: `"${name}"`,
};
// Add to PBXShellScriptBuildPhase section
if (!this.objects.PBXShellScriptBuildPhase) {
this.objects.PBXShellScriptBuildPhase = {};
}
this.objects.PBXShellScriptBuildPhase[buildPhaseUuid] = buildPhase;
this.objects.PBXShellScriptBuildPhase[`${buildPhaseUuid}_comment`] = name;
// Add to target's build phases
const target = this.objects.PBXNativeTarget?.[targetKey];
if (target?.buildPhases) {
target.buildPhases.push({
value: buildPhaseUuid,
comment: name,
});
}
return buildPhaseUuid;
}
/**
* Updates an existing script build phase.
*
* @param buildPhaseId - The UUID of the build phase to update
* @param script - The new shell script content
* @param inputPaths - Array of input paths for the script
*/
updateScriptBuildPhase(buildPhaseId, script, inputPaths = []) {
const buildPhase = this.objects.PBXShellScriptBuildPhase?.[buildPhaseId];
if (!buildPhase || typeof buildPhase === 'string') {
(0, debug_1.debug)(`Build phase not found: ${buildPhaseId}`);
return;
}
const escapedScript = script.replace(/"/g, '\\"');
buildPhase.shellScript = `"${escapedScript}"`;
buildPhase.inputPaths = inputPaths;
buildPhase.shellPath = '/bin/sh';
}
// ================================ FILE HELPERS ================================
/**
* Finds all files in the source build phase of a target.
*
* @param nativeTarget - The target to find the files in
* @returns The files in the source build phase of the target, or an empty array if the target is not found or has no source build phase
*/
findFilesInSourceBuildPhase(nativeTarget) {
(0, debug_1.debug)('Finding files in source build phase for target: ' +
nativeTarget.obj.name);
const buildPhase = this.findSourceBuildPhaseInTarget(nativeTarget.obj);
if (!buildPhase) {
(0, debug_1.debug)(`Sources build phase not found for target: ${nativeTarget.obj.name}`);
return [];
}
const buildPhaseFiles = buildPhase.obj.files;
if (!buildPhaseFiles) {
(0, debug_1.debug)(`No files found in sources build phase for target: ${nativeTarget.obj.name}`);
return [];
}
if (!this.objects.PBXBuildFile) {
(0, debug_1.debug)('PBXBuildFile is undefined');
return [];
}
if (!this.objects.PBXFileReference) {
(0, debug_1.debug)('PBXFileReference is undefined');
return [];
}
const result = [];
for (const file of buildPhaseFiles) {
(0, debug_1.debug)(`Resolving build phase file: ${file.value}`);
// Find the related build file object
const buildFileObj = this.objects.PBXBuildFile[file.value];
if (!buildFileObj || typeof buildFileObj !== 'object') {
(0, debug_1.debug)(`Build file object not found for file: ${file.value}`);
continue;
}
(0, debug_1.debug)(`Build file object found for file: ${file.value}`);
const buildFileRefId = buildFileObj.fileRef;
if (!buildFileRefId) {
(0, debug_1.debug)(`File reference not found for file: ${file.value}`);
continue;
}
(0, debug_1.debug)(`Build file reference found for file: ${file.value}`);
// Find the related file reference object
const buildFile = this.objects.PBXFileReference[buildFileRefId];
if (!buildFile || typeof buildFile !== 'object') {
(0, debug_1.debug)(`File not found in file dictionary for file: ${file.value}`);
continue;
}
(0, debug_1.debug)(`Build file found in file dictionary for file: ${file.value}`);
// Resolve the path of the file based on the `sourceTree` property
const resolvedFilePath = this.resolveAbsolutePathOfFileReference({
id: buildFileRefId,
obj: buildFile,
});
if (!resolvedFilePath) {
(0, debug_1.debug)(`Failed to resolve file path for file: ${file.value}`);
continue;
}
(0, debug_1.debug)(`Resolved file ${file.value} to path: ${resolvedFilePath}`);
result.push(resolvedFilePath);
}
(0, debug_1.debug)(`Resolved ${result.length} files for target: ${nativeTarget.obj.name}`);
return result;
}
/**
* Resolves the absolute path of a file reference.
*
* @param fileRef - The file reference to resolve the path of
* @returns The absolute path of the file reference, or undefined if the file reference is not found or has no path
*/
resolveAbsolutePathOfFileReference(fileRef) {
(0, debug_1.debug)(`Resolving path of file reference: ${fileRef.id} with path: ${fileRef.obj.path}`);
// File path is expected to be set, therefore typing is non-nullable.
// As the file is loaded from a project file, it is not guaranteed to be set,
// therefore we treat it as optional.
if (!fileRef.obj.path) {
(0, debug_1.debug)(`File reference path not found for file reference: ${fileRef.id}`);
return undefined;
}
// File references are resolved based on the `sourceTree` property
// which can have one of the following values:
// - '<absolute>': The file path is absolute
// - '<group>': The file path is relative to the parent group of the file reference
// - 'BUILT_PRODUCTS_DIR': The file path is relative to the built products directory, i.e. the build output directory in derived data
// - 'SOURCE_ROOT': The file path is relative to the source root, i.e. the directory where the Xcode project is located
// - 'SDKROOT': The file path is relative to the SDK root, i.e. the directory where the SDK is installed
// - 'DEVELOPER_DIR': The file path is relative to the developer directory, i.e. the directory where the Xcode command line tools are installed
// The default is '<group>'
const fileRefSourceTree = fileRef.obj.sourceTree?.replace(/"/g, '') ?? '';
switch (fileRefSourceTree) {
case '<absolute>':
return fileRef.obj.path.replace(/"/g, '');
case '<group>':
return this.resolveAbsoluteFilePathRelativeToGroup(fileRef);
case 'BUILT_PRODUCTS_DIR':
return this.resolveAbsoluteFilePathRelativeToBuiltProductsDir(fileRef);
case 'SOURCE_ROOT':
return this.resolveAbsoluteFilePathRelativeToSourceRoot(fileRef);
case 'SDKROOT':
return this.resolveAbsoluteFilePathRelativeToSdkRoot(fileRef);
case 'DEVELOPER_DIR':
return this.resolveAbsoluteFilePathRelativeToDeveloperDir(fileRef);
default:
(0, debug_1.debug)(`Unknown source tree '${fileRef.obj.sourceTree}' for build file: ${fileRef.obj.path}`);
return undefined;
}
}
/**
* Resolves the absolute path of a file reference relative to the parent group.
*
* @param fileRef - The file reference to resolve the path of
* @returns The absolute path of the file reference, or undefined if the file reference is not found or has no path
*/
resolveAbsoluteFilePathRelativeToGroup(fileRef) {
(0, debug_1.debug)(`Resolving absolute file path relative to group for file reference: ${fileRef.id} with path: ${fileRef.obj.path ?? ''}`);
const fileRefPath = fileRef.obj.path?.replace(/"/g, '');
if (!fileRefPath) {
(0, debug_1.debug)(`File reference path not found for file reference: ${fileRef.id}`);
return undefined;
}
// Find the parent group of the file reference by searching for the reverse relationship
const parentGroup = this.findParentGroupByChildId(fileRef.id);
if (!parentGroup) {
(0, debug_1.debug)(`Parent group not found for file reference: ${fileRef.id} at path: ${fileRefPath}`);
return undefined;
}
// Resolve the path of the parent group
const absoluteGroupPath = this.resolveAbsolutePathOfGroup(parentGroup);
if (!absoluteGroupPath) {
(0, debug_1.debug)(`Failed to resolve path of group: ${parentGroup.id}`);
return undefined;
}
return path.join(absoluteGroupPath, fileRefPath);
}
/**
* Resolves the absolute path of a file reference relative to the built products directory.
*
* @param buildFile - The file reference to resolve the path of
* @returns The absolute path of the file reference, or undefined if the file reference is not found or has no path
*/
resolveAbsoluteFilePathRelativeToBuiltProductsDir(buildFile) {
(0, debug_1.debug)(`Resolving absolute file path relative to built products directory for file reference: ${buildFile.id} with path: ${buildFile.obj.path ?? ''}`);
const builtProductsDir = this.getBuildProductsDirectoryPath();
if (!builtProductsDir) {
(0, debug_1.debug)(`Failed to resolve built products directory path`);
return undefined;
}
return path.join(builtProductsDir, buildFile.obj.path.replace(/"/g, ''));
}
/**
* Resolves the absolute path of a file reference relative to the source root.
*
* The source root is the directory where the `.xcodeproj` file is located.
*
* @param buildFile - The file reference to resolve the path of
* @returns The absolute path of the file reference, or undefined if the file reference is not found or has no path
*/
resolveAbsoluteFilePathRelativeToSourceRoot(buildFile) {
return path.join(this.baseDir, buildFile.obj.path.replace(/"/g, ''));
}
/**
* Resolves the absolute path of a file reference relative to the SDK root.
*
* @param buildFile - The file reference to resolve the path of
* @returns The absolute path of the file reference, or undefined if the file reference is not found or has no path
*/
resolveAbsoluteFilePathRelativeToSdkRoot(buildFile) {
(0, debug_1.debug)(`Resolving absolute file path relative to SDK root for file reference: ${buildFile.id} with path: ${buildFile.obj.path ?? ''}`);
const sdkRoot = macos_system_helper_1.MacOSSystemHelpers.findSDKRootDirectoryPath();
if (!sdkRoot) {
(0, debug_1.debug)(`Failed to resolve SDK root directory path`);
return undefined;
}
return path.join(sdkRoot, buildFile.obj.path.replace(/"/g, ''));
}
/**
* Resolves the absolute path of a file reference relative to the developer directory.
*
* @param buildFile - The file reference to resolve the path of
* @returns The absolute path of the file reference, or undefined if the file reference is not found or has no path
*/
resolveAbsoluteFilePathRelativeToDeveloperDir(buildFile) {
(0, debug_1.debug)(`Resolving absolute file path relative to developer directory for file reference: ${buildFile.id} with path: ${buildFile.obj.path ?? ''}`);
const developerDir = macos_system_helper_1.MacOSSystemHelpers.findDeveloperDirectoryPath();
if (!developerDir) {
(0, debug_1.debug)(`Failed to resolve developer directory path`);
return undefined;
}
return path.join(developerDir, buildFile.obj.path.replace(/"/g, ''));
}
/**
* Resolves the absolute path of a group.
*
* @param group - The group to resolve the path of
* @returns The absolute path of the group, or undefined if the group is not found or has no path
*/
resolveAbsolutePathOfGroup(group) {
(0, debug_1.debug)(`Resolving path of group: ${group.id} with path: ${group.obj.path ?? ''}`);
// Group paths are resolved based on the `sourceTree` property
// which can have one of the following values:
// - '<group>': The group path is relative to the parent group of the group
// - 'SOURCE_ROOT': The group path is relative to the source root, i.e. the directory where the Xcode project is located
// - 'BUILT_PRODUCTS_DIR': The group path is relative to the built products directory, i.e. the build output directory in derived data
// - 'SDKROOT': The group path is relative to the SDK root, i.e. the directory where the SDK is installed
// - 'DEVELOPER_DIR': The group path is relative to the developer directory, i.e. the directory where the Xcode command line tools are installed
// The default is '<group>'
const groupSourceTree = group.obj.sourceTree?.replace(/"/g, '') ?? '<group>';
switch (groupSourceTree) {
case '<group>':
return this.resolvePathOfGroupRelativeToGroup(group);
case 'SOURCE_ROOT':
return this.resolvePathOfGroupRelativeToSourceRoot(group);
case 'BUILT_PRODUCTS_DIR':
return this.resolvePathOfGroupRelativeToBuiltProductsDir(group);
case 'SDKROOT':
return this.resolvePathOfGroupRelativeToSdkRoot(group);
case 'DEVELOPER_DIR':
return this.resolvePathOfGroupRelativeToDeveloperDir(group);
default:
(0, debug_1.debug)(`Unknown source tree '${groupSourceTree}' for group: ${group.id}`);
return undefined;
}
}
/**
* Resolves the path of a group relative to the parent group.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the parent group, or undefined if the group is not found or has no path
*/
resolvePathOfGroupRelativeToGroup(group) {
const parentGroup = this.findParentGroupByChildId(group.id);
if (!parentGroup) {
(0, debug_1.debug)(`Parent group not found for group: ${group.id}`);
// If the parent group is not found, check if the group is the main group
// We assume the main group is at the root of the project
if (this.isMainGroup(group.id)) {
return this.baseDir;
}
return undefined;
}
const parentGroupPath = this.resolveAbsolutePathOfGroup(parentGroup);
if (!parentGroupPath) {
(0, debug_1.debug)(`Failed to resolve path of parent group: ${parentGroup.id}`);
return undefined;
}
const groupPath = group.obj.path?.replace(/"/g, '') ?? '';
if (!groupPath) {
(0, debug_1.debug)(`Group path not found for group: ${group.id}`);
return undefined;
}
return path.join(parentGroupPath, groupPath);
}
/**
* Resolves the path of a group relative to the source root.
*
* The source root is the directory where the `.xcodeproj` file is located.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the source root, or undefined if the group is not found or has no path
*/
resolvePathOfGroupRelativeToSourceRoot(group) {
const groupPath = group.obj.path?.replace(/"/g, '') ?? '';
if (!groupPath) {
(0, debug_1.debug)(`Group path not found for group: ${group.id}`);
return this.baseDir;
}
return path.join(this.baseDir, groupPath);
}
/**
* Resolves the path of a group relative to the built products directory.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the built products directory, or undefined if the group is not found or has no path
*/
resolvePathOfGroupRelativeToBuiltProductsDir(group) {
(0, debug_1.debug)(`Resolving path of group: ${group.id} relative to built products directory`);
const builtProductsDir = this.getBuildProductsDirectoryPath();
if (!builtProductsDir) {
(0, debug_1.debug)(`Failed to resolve built products directory path`);
return undefined;
}
return path.join(builtProductsDir, group.obj.path?.replace(/"/g, '') ?? '');
}
/**
* Resolves the path of a group relative to the SDK root.
*
* The SDK root is the directory where the SDK is installed.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the SDK root, or undefined if the group is not found or has no path
*/
resolvePathOfGroupRelativeToSdkRoot(group) {
(0, debug_1.debug)(`Resolving path of group: ${group.id} relative to SDK root`);
const sdkRoot = macos_system_helper_1.MacOSSystemHelpers.findSDKRootDirectoryPath();
if (!sdkRoot) {
(0, debug_1.debug)(`Failed to resolve SDK root directory path`);
return undefined;
}
return path.join(sdkRoot, group.obj.path?.replace(/"/g, '') ?? '');
}
/**
* Resolves the path of a group relative to the developer directory.
*
* The developer directory is the directory where the Xcode command line tools are installed.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the developer directory, or undefined if the group is not found or has no path
*/
resolvePathOfGroupRelativeToDeveloperDir(group) {
(0, debug_1.debug)(`Resolving path of group: ${group.id} relative to developer directory`);
const developerDir = macos_system_helper_1.MacOSSystemHelpers.findDeveloperDirectoryPath();
if (!developerDir) {
(0, debug_1.debug)(`Failed to resolve developer directory path`);
return undefined;
}
return path.join(developerDir, group.obj.path?.replace(/"/g, '') ?? '');
}
/**
* Resolves the absolute path of a group.
*
* @param group - The group to resolve the path of
* @returns The absolute path of the group, or undefined if the group is not found or has no path
*/
resolveAbsolutePathOfSynchronizedRootGroup(group) {
(0, debug_1.debug)(`Resolving path of synchronized root group: ${group.id} with path: ${group.obj.path ?? ''}`);
// Group paths are resolved based on the `sourceTree` property
// which can have one of the following values:
// - '<group>': The group path is relative to the parent group of the group
// - 'SOURCE_ROOT': The group path is relative to the source root, i.e. the directory where the Xcode project is located
// - 'BUILT_PRODUCTS_DIR': The group path is relative to the built products directory, i.e. the build output directory in derived data
// - 'SDKROOT': The group path is relative to the SDK root, i.e. the directory where the SDK is installed
// - 'DEVELOPER_DIR': The group path is relative to the developer directory, i.e. the directory where the Xcode command line tools are installed
// The default is '<group>'
const groupSourceTree = group.obj.sourceTree?.replace(/"/g, '') ?? '<group>';
switch (groupSourceTree) {
case '<group>':
return this.resolvePathOfSynchronizedRootGroupRelativeToGroup(group);
case 'SOURCE_ROOT':
return this.resolvePathOfSynchronizedRootGroupRelativeToSourceRoot(group);
case 'BUILT_PRODUCTS_DIR':
return this.resolvePathOfSynchronizedRootGroupRelativeToBuiltProductsDir(group);
case 'SDKROOT':
return this.resolvePathOfSynchronizedRootGroupRelativeToSdkRoot(group);
case 'DEVELOPER_DIR':
return this.resolvePathOfSynchronizedRootGroupRelativeToDeveloperDir(group);
default:
(0, debug_1.debug)(`Unknown source tree '${groupSourceTree}' for group: ${group.id}`);
return undefined;
}
}
/**
* Resolves the path of a group relative to the parent group.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the parent group, or undefined if the group is not found or has no path
*/
resolvePathOfSynchronizedRootGroupRelativeToGroup(group) {
const parentGroup = this.findParentGroupByChildId(group.id);
if (!parentGroup) {
(0, debug_1.debug)(`Parent group not found for group: ${group.id}`);
// If the parent group is not found, check if the group is the main group
// We assume the main group is at the root of the project
if (this.isMainGroup(group.id)) {
return this.baseDir;
}
return undefined;
}
const parentGroupPath = this.resolveAbsolutePathOfGroup(parentGroup);
if (!parentGroupPath) {
(0, debug_1.debug)(`Failed to resolve path of parent group: ${parentGroup.id}`);
return undefined;
}
const groupPath = group.obj.path?.replace(/"/g, '') ?? '';
if (!groupPath) {
(0, debug_1.debug)(`Group path not found for group: ${group.id}`);
return undefined;
}
return path.join(parentGroupPath, groupPath);
}
/**
* Resolves the path of a group relative to the source root.
*
* The source root is the directory where the `.xcodeproj` file is located.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the source root, or undefined if the group is not found or has no path
*/
resolvePathOfSynchronizedRootGroupRelativeToSourceRoot(group) {
const groupPath = group.obj.path?.replace(/"/g, '') ?? '';
if (!groupPath) {
(0, debug_1.debug)(`Group path not found for group: ${group.id}`);
return this.baseDir;
}
return path.join(this.baseDir, groupPath);
}
/**
* Resolves the path of a group relative to the built products directory.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the built products directory, or undefined if the group is not found or has no path
*/
resolvePathOfSynchronizedRootGroupRelativeToBuiltProductsDir(group) {
(0, debug_1.debug)(`Resolving path of synchronized root group: ${group.id} relative to built products directory`);
const builtProductsDir = this.getBuildProductsDirectoryPath();
if (!builtProductsDir) {
(0, debug_1.debug)(`Failed to resolve built products directory path`);
return undefined;
}
return path.join(builtProductsDir, group.obj.path?.replace(/"/g, '') ?? '');
}
/**
* Resolves the path of a group relative to the SDK root.
*
* The SDK root is the directory where the SDK is installed.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the SDK root, or undefined if the group is not found or has no path
*/
resolvePathOfSynchronizedRootGroupRelativeToSdkRoot(group) {
(0, debug_1.debug)(`Resolving path of group: ${group.id} relative to SDK root`);
const sdkRoot = macos_system_helper_1.MacOSSystemHelpers.findSDKRootDirectoryPath();
if (!sdkRoot) {
(0, debug_1.debug)(`Failed to resolve SDK root directory path`);
return undefined;
}
return path.join(sdkRoot, group.obj.path?.replace(/"/g, '') ?? '');
}
/**
* Resolves the path of a group relative to the developer directory.
*
* The developer directory is the directory where the Xcode command line tools are installed.
*
* @param group - The group to resolve the path of
* @returns The path of the group relative to the developer directory, or undefined if the group is not found or has no path
*/
resolvePathOfSynchronizedRootGroupRelativeToDeveloperDir(group) {
(0, debug_1.debug)(`Resolving path of synchronized root group: ${group.id} relative to developer directory`);
const developerDir = macos_system_helper_1.MacOSSystemHelpers.findDeveloperDirectoryPath();
if (!developerDir) {
(0, debug_1.debug)(`Failed to resolve developer directory path`);
return undefined;
}
return path.join(developerDir, group.obj.path?.replace(/"/g, '') ?? '');
}
/**
* Finds all files in the synchronized root groups of a target.
*
* @param nativeTarget - The target to find the files in
* @returns The files in the synchronized root groups of the target, or an empty array if the target is not found or has no synchronized root groups
*/
findFilesInSynchronizedRootGroups(nativeTarget) {
(0, debug_1.debug)(`Finding files in synchronized root groups for target: ${nativeTarget.obj.name}`);
const synchronizedRootGroups = nativeTarget.obj.fileSystemSynchronizedGroups ?? [];
const result = [];
for (const group of synchronizedRootGroups) {
const groupObj = this.objects.PBXFileSystemSynchronizedRootGroup?.[group.value];
if (!groupObj || typeof groupObj !== 'object') {
(0, debug_1.debug)(`Synchronized root group not found: ${group.value}`);
continue;
}
(0, debug_1.debug)(`Found synchronized root group: ${group.value}`);
const files = this.getFilesInSynchronizedRootGroup({
id: group.value,
obj: groupObj,
});
(0, debug_1.debug)(`Found ${files.length} files in synchronized root group: ${group.value}`);
result.push(...files.map((file) => file.path));
}
(0, debug_1.debug)(`Found ${result.length} files in synchronized root groups for target: ${nativeTarget.obj.name}`);
return result;
}
getFilesInSynchronizedRootGroup(group) {
// Group path is expected to be set, therefore typing is non-nullable.
// As the group is loaded from a project file, it is not guaranteed to be set,
// therefore we treat it as optional.
if (!group.obj.path) {
(0, debug_1.debug)(`Group path not found for group: ${group.id}`);
return [];
}
// Resolve the path of the synchronized root group
const absoluteGroupPath = this.resolveAbsolutePathOfSynchronizedRootGroup(group);
if (!absoluteGroupPath) {
(0, debug_1.debug)(`Failed to resolve path of synchronized root group: ${group.id}`);
return [];
}
// Build a list of all exception paths for the group
const exceptionSets = this.getExceptionSetsForGroup(group);
// Resolve a list of all files in the group
const files = this.getAbsoluteFilePathsInDirectoryTree(absoluteGroupPath);
// Filter out files that are excluded by the exception sets
const filteredFiles = this.filterFilesByExceptionSets(files, exceptionSets);
return filteredFiles;
}
/**
* Returns all files in a directory tree.
*
* @param dirPath - The path of the directory to get the files in
* @returns All files in the directory tree, or an empty array if the directory does not exist
*/
getAbsoluteFilePathsInDirectoryTree(dirPath) {
// If the directory does not exist, return an empty array
// This can happen if the group is not found in the project
if (!fs.existsSync(dirPath)) {
return [];
}
const result = [];
const files = fs.readdirSync(dirPath);
for (const file of files) {
// Ignore hidden files and directories
if (file.startsWith('.')) {
continue;
}
const filePath = path.join(dirPath, file);
// If the file is a directory, recursively get the files in the directory
if (fs.statSync(filePath).isDirectory()) {
result.push(...this.getAbsoluteFilePathsInDirectoryTree(filePath));
continue;
}
// If the file is a file, add it to the result
if (fs.statSync(filePath).isFile()) {
result.push({
name: file,
path: filePath,
});
continue;
}
}
return result;
}
filterFilesByExceptionSets(files, exceptionSets) {
// Iterate over all files and filter out files that are excluded by any exception sets
return files.filter((file) => {
return !exceptionSets.some((exceptionSet) => {
const membershipExceptions = exceptionSet.obj.membershipExceptions ?? [];
return membershipExceptions.some((path) => {
const unescapedPath = path.replace(/"/g, '');
return file.path.includes(unescapedPath);
});
});
});
}
// ================================ GROUP HELPERS ================================
/**
* Returns all groups that are PBXGroup.
*
* This is a helper method to avoid having to map and filter the groups manually.
*
* @returns All groups that are PBXGroup, excluding comments and non-object values.
*/
get groups() {
// Map and filter the groups to only include the groups that are PBXGroup
return Object.entries(this.objects.PBXGroup ?? {}).reduce((acc, [key, group]) => {
if (typeof group !== 'object') {
return acc;
}
return acc.concat([