@bacons/apple-targets
Version:
Generate Apple Targets with Expo Prebuild
490 lines (489 loc) • 25.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.withXcodeChanges = void 0;
const xcode_1 = require("@bacons/xcode");
const plist_1 = __importDefault(require("@expo/plist"));
const fs_1 = __importDefault(require("fs"));
const glob_1 = require("glob");
const path_1 = __importDefault(require("path"));
const target_1 = require("./target");
const entitlements_1 = require("./entitlements");
const with_bacons_xcode_1 = require("./with-bacons-xcode");
const assert_1 = __importDefault(require("assert"));
const configuration_list_1 = require("./configuration-list");
const util_1 = require("./util");
/**
* Checks if a target is a watchOS extension (NOT a watch app) by inspecting
* its build settings and product type. This includes modern WidgetKit-based
* watch widgets which use app-extension product type with WATCHOS_DEPLOYMENT_TARGET.
*/
function isWatchOSExtensionTarget(target) {
const buildSettings = target.getDefaultConfiguration().props.buildSettings;
const isWatchOS = buildSettings.SDKROOT === "watchos" ||
"WATCHOS_DEPLOYMENT_TARGET" in buildSettings;
// Watch apps are detected by isWatchOSTarget() - extensions are watchOS but NOT watch apps
return isWatchOS && !target.isWatchOSTarget();
}
const withXcodeChanges = (config, props) => {
return (0, with_bacons_xcode_1.withXcodeProjectBeta)(config, async (config) => {
await applyXcodeChanges(config, config.modResults, props);
return config;
});
};
exports.withXcodeChanges = withXcodeChanges;
// function getMainMarketingVersion(project: XcodeProject) {
// const mainTarget = getMainAppTarget(project);
// const config = mainTarget.getDefaultConfiguration();
// const info = config.getInfoPlist();
// const version = info.CFBundleShortVersionString;
// // console.log('getMainMarketingVersion', mainTarget.getDisplayName(), version)
// if (!version || version === "$(MARKETING_VERSION)") {
// // console.log('getMainMarketingVersion.fallback', config.props.buildSettings.MARKETING_VERSION)
// return config.props.buildSettings.MARKETING_VERSION;
// }
// return version;
// }
async function applyXcodeChanges(config, project, props) {
var _a, _b, _c;
var _d;
const mainAppTarget = (0, target_1.getMainAppTarget)(project);
// Special setting for share extensions.
if ((0, target_1.needsEmbeddedSwift)(props.type)) {
// Add ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES to the main app target
mainAppTarget.setBuildSetting("ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
}
function getExtensionTargets() {
return project.rootObject.props.targets.filter((target) => {
return (xcode_1.PBXNativeTarget.is(target) && (0, target_1.isNativeTargetOfType)(target, props.type));
});
}
const targets = getExtensionTargets();
const productName = props.productName;
let targetToUpdate = (_a = targets.find((target) => target.props.productName === productName)) !== null && _a !== void 0 ? _a : targets[0];
if (targetToUpdate) {
(0, util_1.warnOnce)(`Target "${targetToUpdate.props.productName}" already exists, updating instead of creating a new one`);
}
const magicCwd = path_1.default.join(config._internal.projectRoot, "ios", props.cwd);
function applyDevelopmentTeamIdToTargets() {
var _a, _b;
var _c, _d, _e;
// Set to the provided value or any value.
const devTeamId = props.teamId ||
project.rootObject.props.targets
.map((target) => target.getDefaultBuildSetting("DEVELOPMENT_TEAM"))
.find(Boolean);
project.rootObject.props.targets.forEach((target) => {
if (devTeamId) {
target.setBuildSetting("DEVELOPMENT_TEAM", devTeamId);
}
else {
target.removeBuildSetting("DEVELOPMENT_TEAM");
}
});
for (const target of project.rootObject.props.targets) {
(_a = (_c = project.rootObject.props.attributes).TargetAttributes) !== null && _a !== void 0 ? _a : (_c.TargetAttributes = {});
// idk, attempting to prevent EAS Build from failing when it codesigns
(_b = (_d = project.rootObject.props.attributes.TargetAttributes)[_e = target.uuid]) !== null && _b !== void 0 ? _b : (_d[_e] = {
CreatedOnToolsVersion: "14.3",
ProvisioningStyle: "Automatic",
DevelopmentTeam: devTeamId,
});
}
}
function configureTargetWithKnownSettings(target) {
var _a, _b;
if ((_a = props.colors) === null || _a === void 0 ? void 0 : _a.$accent) {
target.setBuildSetting("ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME", "$accent");
}
else {
target.removeBuildSetting("ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME");
}
if ((_b = props.colors) === null || _b === void 0 ? void 0 : _b.$widgetBackground) {
target.setBuildSetting("ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME", "$widgetBackground");
}
else {
target.removeBuildSetting("ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME");
}
}
function configureTargetWithEntitlements(target) {
var _a, _b;
// Prefer a generated entitlements file under
// `ios/<TARGET_GENERATED_DIR>/<productName>/` (written by `with-widget.ts`
// when entitlements are defined in the config). Fall back to a hand-written
// `*.entitlements` file in the source target directory if no generated one
// exists.
const resolved = (0, entitlements_1.resolveEntitlementsForCodeSign)({
projectRoot: config._internal.projectRoot,
productName: props.productName,
cwd: props.cwd,
});
const entitlementsAbsolutePath = (_a = resolved === null || resolved === void 0 ? void 0 : resolved.absolutePath) !== null && _a !== void 0 ? _a : null;
const codeSignEntitlements = (_b = resolved === null || resolved === void 0 ? void 0 : resolved.codeSignEntitlements) !== null && _b !== void 0 ? _b : null;
let hasAppGroups = false;
if (codeSignEntitlements && entitlementsAbsolutePath) {
target.setBuildSetting("CODE_SIGN_ENTITLEMENTS", codeSignEntitlements);
// Check if entitlements contain app groups
try {
const content = plist_1.default.parse(fs_1.default.readFileSync(entitlementsAbsolutePath, "utf8"));
const appGroups = content["com.apple.security.application-groups"];
hasAppGroups = Array.isArray(appGroups) && appGroups.length > 0;
}
catch {
// If we can't read/parse, assume no app groups
}
}
else {
target.removeBuildSetting("CODE_SIGN_ENTITLEMENTS");
}
// Set REGISTER_APP_GROUPS if app groups are configured.
// This build setting is required for extensions to properly access app group containers.
// Without it, extensions may fail to access shared data even with matching entitlements.
// See: https://stackoverflow.com/questions/79792338
if (hasAppGroups) {
// @ts-expect-error - REGISTER_APP_GROUPS is a valid Xcode build setting not in types yet
target.setBuildSetting("REGISTER_APP_GROUPS", "YES");
}
else {
// @ts-expect-error - REGISTER_APP_GROUPS is a valid Xcode build setting not in types yet
target.removeBuildSetting("REGISTER_APP_GROUPS");
}
return entitlementsAbsolutePath
? [path_1.default.basename(entitlementsAbsolutePath)]
: [];
}
function syncMarketingVersions() {
var _a;
// In Expo managed projects, the version will always be overwritten.
// Because the overwrite happens after this script runs, we just set it to the config value.
const mainVersion = ((_a = config.ios) === null || _a === void 0 ? void 0 : _a.version) || config.version || "1.0.0";
// const mainVersion = getMainMarketingVersion(project);
project.rootObject.props.targets.forEach((target) => {
if (xcode_1.PBXNativeTarget.is(target)) {
target.setBuildSetting("MARKETING_VERSION", mainVersion);
}
});
}
function configureTargetWithPreview(target) {
const assets = (0, glob_1.globSync)("preview/*.xcassets", {
absolute: true,
cwd: magicCwd,
})[0];
if (assets) {
target.setBuildSetting("DEVELOPMENT_ASSET_PATHS", `"${props.cwd + "/preview"}"`);
}
else {
target.removeBuildSetting("DEVELOPMENT_ASSET_PATHS");
}
return assets;
}
function configureJsExport(target) {
if (props.exportJs) {
const shellScript = mainAppTarget.props.buildPhases.find((phase) => xcode_1.PBXShellScriptBuildPhase.is(phase) &&
phase.props.name === "Bundle React Native code and images");
if (!shellScript) {
console.warn('Failed to find the "Bundle React Native code and images" build phase in the main app target. Will not be able to configure: ' +
props.type);
return;
}
const currentShellScript = target.props.buildPhases.find((phase) => xcode_1.PBXShellScriptBuildPhase.is(phase) &&
phase.props.name === "Bundle React Native code and images");
if (!currentShellScript) {
// Link the same build script across targets to simplify updates.
target.props.buildPhases.push(shellScript);
// Alternatively, create a duplicate.
// target.createBuildPhase(PBXShellScriptBuildPhase, {
// ...shellScript.props,
// });
}
else {
// If there already is a bundler shell script and it's not the one from the main target, then update it.
if (currentShellScript.uuid !== shellScript.uuid) {
for (const key in shellScript.props) {
// @ts-expect-error
currentShellScript.props[key] = shellScript.props[key];
}
}
}
}
else {
// Remove the shell script build phase if it exists from a subsequent build.
const shellScript = target.props.buildPhases.findIndex((phase) => xcode_1.PBXShellScriptBuildPhase.is(phase) &&
phase.props.name === "Bundle React Native code and images");
if (shellScript !== -1) {
target.props.buildPhases.splice(shellScript, 1);
}
}
}
if (targetToUpdate) {
// Remove existing build phases
targetToUpdate.props.buildConfigurationList.props.buildConfigurations.forEach((config) => {
config.getReferrers().forEach((ref) => {
ref.removeReference(config.uuid);
});
config.removeFromProject();
});
// Remove existing build configuration list
targetToUpdate.props.buildConfigurationList
.getReferrers()
.forEach((ref) => {
ref.removeReference(targetToUpdate.props.buildConfigurationList.uuid);
});
targetToUpdate.props.buildConfigurationList.removeFromProject();
// Create new build phases
targetToUpdate.props.buildConfigurationList =
(0, configuration_list_1.createConfigurationListForType)(project, props);
}
else {
const productType = (0, target_1.productTypeForType)(props.type);
const isExtension = productType === "com.apple.product-type.app-extension";
const isExtensionKit = productType === "com.apple.product-type.extensionkit-extension";
const isApplication = productType === "com.apple.product-type.application";
// Determine the correct file type for the product reference
const explicitFileType = isExtensionKit
? "wrapper.extensionkit-extension"
: isExtension
? "wrapper.app-extension"
: isApplication
? "wrapper.application"
: "wrapper.app-extension";
const appExtensionBuildFile = xcode_1.PBXBuildFile.create(project, {
fileRef: xcode_1.PBXFileReference.create(project, {
explicitFileType: explicitFileType,
includeInIndex: 0,
path: props.name + (isExtension ? ".appex" : ".app"),
sourceTree: "BUILT_PRODUCTS_DIR",
}),
settings: {
ATTRIBUTES: ["RemoveHeadersOnCopy"],
},
});
project.rootObject.ensureProductGroup().props.children.push(
// @ts-expect-error
appExtensionBuildFile.props.fileRef);
targetToUpdate = project.rootObject.createNativeTarget({
buildConfigurationList: (0, configuration_list_1.createConfigurationListForType)(project, props),
name: props.name,
productName,
// @ts-expect-error
productReference: appExtensionBuildFile.props.fileRef /* alphaExtension.appex */,
productType: productType,
});
// For watchOS extensions (like watch-widget), embed in the watchOS app target
// instead of the main iOS app target.
if (isWatchOSExtensionTarget(targetToUpdate)) {
const watchTarget = project.rootObject.props.targets.find((t) => xcode_1.PBXNativeTarget.is(t) && t.isWatchOSTarget());
if (watchTarget) {
const existingPhase = watchTarget.props.buildPhases.find((phase) => xcode_1.PBXCopyFilesBuildPhase.is(phase) &&
phase.props.name === "Embed Foundation Extensions");
const embedPhase = existingPhase !== null && existingPhase !== void 0 ? existingPhase : watchTarget.createBuildPhase(xcode_1.PBXCopyFilesBuildPhase, {
name: "Embed Foundation Extensions",
dstSubfolderSpec: 13,
dstPath: "",
files: [],
});
if (!embedPhase.getBuildFile(appExtensionBuildFile.props.fileRef)) {
embedPhase.props.files.push(appExtensionBuildFile);
}
}
else {
console.warn("[apple-targets] watchOS extension: could not find watchOS app target, falling back to main app");
const copyPhase = mainAppTarget.getCopyBuildPhaseForTarget(targetToUpdate);
if (!copyPhase.getBuildFile(appExtensionBuildFile.props.fileRef)) {
copyPhase.props.files.push(appExtensionBuildFile);
}
}
}
else {
// For watch apps, getCopyBuildPhaseForTarget returns "Embed Watch Content".
// For regular extensions, it returns "Embed Foundation Extensions".
const copyPhase = mainAppTarget.getCopyBuildPhaseForTarget(targetToUpdate);
if (!copyPhase.getBuildFile(appExtensionBuildFile.props.fileRef)) {
copyPhase.props.files.push(appExtensionBuildFile);
}
}
}
configureTargetWithKnownSettings(targetToUpdate);
configureTargetWithEntitlements(targetToUpdate);
configureTargetWithPreview(targetToUpdate);
targetToUpdate.ensureFrameworks(props.frameworks);
targetToUpdate.getSourcesBuildPhase();
targetToUpdate.getResourcesBuildPhase();
configureJsExport(targetToUpdate);
// For watchOS extensions, the dependency belongs on the watchOS
// app target so Xcode builds the extension before the watch app.
// For watch apps, the dependency belongs on the main iOS app.
const dependencyParent = isWatchOSExtensionTarget(targetToUpdate)
? (_b = project.rootObject.props.targets.find((t) => xcode_1.PBXNativeTarget.is(t) && t.isWatchOSTarget())) !== null && _b !== void 0 ? _b : mainAppTarget
: mainAppTarget;
dependencyParent.addDependency(targetToUpdate);
const assetsDir = path_1.default.join(magicCwd, "assets");
// TODO: Maybe just limit this to Safari extensions?
const explicitFolders = !fs_1.default.existsSync(assetsDir)
? []
: fs_1.default
.readdirSync(assetsDir)
.filter((file) => file !== ".DS_Store" &&
fs_1.default.statSync(path_1.default.join(assetsDir, file)).isDirectory())
.map((file) => path_1.default.join("assets", file));
const protectedGroup = ensureProtectedGroup(project, path_1.default.dirname(props.cwd));
const sharedAssets = (0, glob_1.globSync)("_shared/*", {
absolute: false,
cwd: magicCwd,
});
// Also look for global shared assets in the parent targets/_shared directory
const targetsDir = path_1.default.dirname(magicCwd);
const globalSharedAssets = (0, glob_1.globSync)("_shared/*", {
absolute: false,
cwd: targetsDir,
});
let syncRootGroup = protectedGroup.props.children.find((child) => child.props.path === path_1.default.basename(props.cwd));
if (!syncRootGroup) {
syncRootGroup = xcode_1.PBXFileSystemSynchronizedRootGroup.create(project, {
path: path_1.default.basename(props.cwd),
exceptions: [
xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet.create(project, {
target: targetToUpdate,
membershipExceptions: [
// TODO: What other files belong here, why is this here?
"Info.plist",
// Exclude the config path
path_1.default.relative(magicCwd, props.configPath),
].sort(),
}),
],
explicitFileTypes: {},
explicitFolders: [
// Replaces the previous `lastKnownFileType: "folder",` system that's used in things like Safari extensions to include folders of assets.
// ex: `"Resources/_locales", "Resources/images"`
...explicitFolders,
],
sourceTree: "<group>",
});
if (!targetToUpdate.props.fileSystemSynchronizedGroups) {
targetToUpdate.props.fileSystemSynchronizedGroups = [];
}
targetToUpdate.props.fileSystemSynchronizedGroups.push(syncRootGroup);
protectedGroup.props.children.push(syncRootGroup);
}
// If there's a `_shared` folder, create a PBXFileSystemSynchronizedBuildFileExceptionSet and set the `target` to the main app target. Then add exceptions to the new target's PBXFileSystemSynchronizedRootGroup's exceptions. Finally, ensure the relative paths for each file in the _shared folder are added to the `membershipExceptions` array.
(0, assert_1.default)(syncRootGroup instanceof xcode_1.PBXFileSystemSynchronizedRootGroup);
(_c = (_d = syncRootGroup.props).exceptions) !== null && _c !== void 0 ? _c : (_d.exceptions = []);
const existingExceptionSet = syncRootGroup.props.exceptions.find((exception) => exception instanceof xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet &&
exception.props.target === mainAppTarget);
if (sharedAssets.length) {
const exceptionSet = existingExceptionSet ||
xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet.create(project, {
target: mainAppTarget,
});
exceptionSet.props.membershipExceptions = sharedAssets.sort();
syncRootGroup.props.exceptions.push(exceptionSet);
}
else {
// Remove the exception set if there are no shared assets.
existingExceptionSet === null || existingExceptionSet === void 0 ? void 0 : existingExceptionSet.removeFromProject();
}
function configureTargetWithGlobalSharedAssets(target) {
var _a;
var _b;
if (!globalSharedAssets.length)
return;
// Create or find the global shared synchronized root group
let globalSharedSyncGroup = protectedGroup.props.children.find((child) => child.props.path === "_shared" &&
child instanceof xcode_1.PBXFileSystemSynchronizedRootGroup);
if (!globalSharedSyncGroup) {
globalSharedSyncGroup = xcode_1.PBXFileSystemSynchronizedRootGroup.create(project, {
path: "_shared",
exceptions: [
// Create exception set for the main app target
xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet.create(project, {
target: mainAppTarget,
membershipExceptions: globalSharedAssets.sort(),
}),
// Create exception set for the extension target
xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet.create(project, {
target: target,
membershipExceptions: globalSharedAssets.sort(),
}),
],
explicitFileTypes: {},
explicitFolders: [],
sourceTree: "<group>",
});
// Add to both targets' fileSystemSynchronizedGroups
if (!mainAppTarget.props.fileSystemSynchronizedGroups) {
mainAppTarget.props.fileSystemSynchronizedGroups = [];
}
mainAppTarget.props.fileSystemSynchronizedGroups.push(globalSharedSyncGroup);
if (!target.props.fileSystemSynchronizedGroups) {
target.props.fileSystemSynchronizedGroups = [];
}
target.props.fileSystemSynchronizedGroups.push(globalSharedSyncGroup);
protectedGroup.props.children.push(globalSharedSyncGroup);
}
else {
// Update existing synchronized group with current global shared assets
(_a = (_b = globalSharedSyncGroup.props).exceptions) !== null && _a !== void 0 ? _a : (_b.exceptions = []);
// Update or create exception set for main app target
let mainAppExceptionSet = globalSharedSyncGroup.props.exceptions.find((exception) => exception instanceof xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet &&
exception.props.target === mainAppTarget);
if (!mainAppExceptionSet) {
mainAppExceptionSet =
xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet.create(project, {
target: mainAppTarget,
membershipExceptions: globalSharedAssets.sort(),
});
globalSharedSyncGroup.props.exceptions.push(mainAppExceptionSet);
}
else {
mainAppExceptionSet.props.membershipExceptions =
globalSharedAssets.sort();
}
// Update or create exception set for extension target
let extensionExceptionSet = globalSharedSyncGroup.props.exceptions.find((exception) => exception instanceof xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet &&
exception.props.target === target);
if (!extensionExceptionSet) {
extensionExceptionSet =
xcode_1.PBXFileSystemSynchronizedBuildFileExceptionSet.create(project, {
target: target,
membershipExceptions: globalSharedAssets.sort(),
});
globalSharedSyncGroup.props.exceptions.push(extensionExceptionSet);
}
else {
extensionExceptionSet.props.membershipExceptions =
globalSharedAssets.sort();
}
// Ensure the current target has the synchronized group in its fileSystemSynchronizedGroups
if (!target.props.fileSystemSynchronizedGroups) {
target.props.fileSystemSynchronizedGroups = [];
}
// Check if this target already has the synchronized group
const hasGroup = target.props.fileSystemSynchronizedGroups.some((group) => group === globalSharedSyncGroup);
if (!hasGroup) {
target.props.fileSystemSynchronizedGroups.push(globalSharedSyncGroup);
}
}
}
configureTargetWithGlobalSharedAssets(targetToUpdate);
applyDevelopmentTeamIdToTargets();
syncMarketingVersions();
return project;
}
const PROTECTED_GROUP_NAME = "expo:targets";
function ensureProtectedGroup(project, relativePath = "../targets") {
const hasProtectedGroup = project.rootObject.props.mainGroup
.getChildGroups()
.find((group) => group.getDisplayName() === PROTECTED_GROUP_NAME);
const protectedGroup = hasProtectedGroup !== null && hasProtectedGroup !== void 0 ? hasProtectedGroup : xcode_1.PBXGroup.create(project, {
name: PROTECTED_GROUP_NAME,
path: relativePath,
sourceTree: "<group>",
});
if (!hasProtectedGroup) {
project.rootObject.props.mainGroup.props.children.unshift(protectedGroup);
}
return protectedGroup;
}