Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/data/build-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class IOSBuildData extends BuildData implements IiOSBuildData {
public buildForAppStore: boolean;
public iCloudContainerEnvironment: string;
public hostProjectPath: string;
public catalyst: boolean;

constructor(projectDir: string, platform: string, data: any) {
super(projectDir, platform, data);
Expand All @@ -43,6 +44,7 @@ export class IOSBuildData extends BuildData implements IiOSBuildData {
this.buildForAppStore = data.buildForAppStore;
this.iCloudContainerEnvironment = data.iCloudContainerEnvironment;
this.hostProjectPath = data.hostProjectPath;
this.catalyst = data.catalyst;
}
}

Expand Down
10 changes: 10 additions & 0 deletions lib/definitions/ios.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ declare global {
projectData: IProjectData,
buildConfig: IBuildConfig,
): Promise<string>;
buildForCatalyst(
platformData: IPlatformData,
projectData: IProjectData,
buildConfig: IBuildConfig,
): Promise<void>;
}

interface IosSPMPackageBase {
Expand Down Expand Up @@ -110,6 +115,11 @@ declare global {
projectData: IProjectData,
buildConfig: IBuildConfig,
): Promise<string[]>;
getBuildForCatalystArgs(
platformData: IPlatformData,
projectData: IProjectData,
buildConfig: IBuildConfig,
): string[];
getXcodeProjectArgs(
platformData: IPlatformData,
projectData: IProjectData,
Expand Down
4 changes: 4 additions & 0 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,10 @@ interface IiOSBuildConfig
* Code sign identity used for build. If not set iPhone Developer is used as a default when building for device.
*/
codeSignIdentity?: string;
/**
* Build a native macOS app with Mac Catalyst instead of an iOS app.
*/
catalyst?: boolean;
}

/**
Expand Down
1 change: 1 addition & 0 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export class Options {
framework: { type: OptionType.String, hasSensitiveValue: false },
frameworkVersion: { type: OptionType.String, hasSensitiveValue: false },
forDevice: { type: OptionType.Boolean, hasSensitiveValue: false },
catalyst: { type: OptionType.Boolean, hasSensitiveValue: false },
iCloudContainerEnvironment: {
type: OptionType.String,
hasSensitiveValue: false,
Expand Down
23 changes: 22 additions & 1 deletion lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,20 @@ export const DevicePlatformSdkName = "iphoneos";
export const SimulatorPlatformSdkName = "iphonesimulator";
export const VisionDevicePlatformSdkName = "xros";
export const VisionSimulatorPlatformSdkName = "xrsimulator";
// Not an SDK name — Xcode names the Mac Catalyst products directory
// `<Configuration>-maccatalyst`, and the build output path is derived from it.
export const CatalystPlatformSdkName = "maccatalyst";

const FRAMEWORK_EXTENSIONS = [".framework", ".xcframework"];

const getPlatformSdkName = (buildData: IBuildData): string => {
const forDevice =
!buildData || buildData.buildForDevice || buildData.buildForAppStore;

if (buildData && (<{ catalyst?: boolean }>buildData).catalyst) {
return CatalystPlatformSdkName;
}

const isvisionOS = injector
.resolve("devicePlatformsConstants")
.isvisionOS(buildData.platform);
Expand Down Expand Up @@ -452,7 +460,20 @@ export class IOSProjectService
this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data);
};

if (buildData.buildForDevice) {
if (buildData.catalyst) {
// Signing is handled by `-allowProvisioningUpdates`: Mac Catalyst needs a
// macOS provisioning profile, which the iOS signing service cannot pick.
await attachAwaitDetach(
constants.BUILD_OUTPUT_EVENT_NAME,
this.$childProcess,
handler,
this.$xcodebuildService.buildForCatalyst(
platformData,
projectData,
<any>buildData,
),
);
} else if (buildData.buildForDevice) {
await this.$iOSSigningService.setupSigningForDevice(
projectRoot,
projectData,
Expand Down
74 changes: 74 additions & 0 deletions lib/services/ios/xcodebuild-args-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { IPlatformData } from "../../definitions/platform";
import { IFileSystem } from "../../common/declarations";
import { injector } from "../../common/yok";
import * as _ from "lodash";
import * as semver from "semver";

import {
DevicePlatformSdkName,
Expand All @@ -21,6 +22,8 @@ import {
} from "../ios-project-service";

export class XcodebuildArgsService implements IXcodebuildArgsService {
private static readonly MIN_CATALYST_DEPLOYMENT_TARGET = "13.1";

constructor(
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
private $devicesService: Mobile.IDevicesService,
Expand All @@ -30,6 +33,39 @@ export class XcodebuildArgsService implements IXcodebuildArgsService {
private $xcconfigService: IXcconfigService,
) {}

public getBuildForCatalystArgs(
platformData: IPlatformData,
projectData: IProjectData,
buildConfig: IBuildConfig,
): string[] {
// Mac Catalyst is a variant of the iOS platform rather than a platform of
// its own: the same target is rebuilt against the macOS SDK with the
// `-macabi` triple. `SUPPORTS_MACCATALYST` has to be forced because the
// runtime template only enables the legacy `SUPPORTS_UIKITFORMAC` alias.
return [
"-destination",
"platform=macOS,variant=Mac Catalyst",
"build",
"-configuration",
buildConfig.release ? Configurations.Release : Configurations.Debug,
"-allowProvisioningUpdates",
"SUPPORTS_MACCATALYST=YES",
// no `-sdk` here: the destination already selects macOS + the Mac Catalyst
// variant, and forcing an SDK on top of it makes xcodebuild pick iphoneos
"BUILD_DIR=" + path.join(platformData.projectRoot, constants.BUILD_DIR),
"SHARED_PRECOMPS_DIR=" +
path.join(platformData.projectRoot, constants.BUILD_DIR, "sharedpch"),
]
.concat(
// the deployment target is re-added below, clamped to what Catalyst supports
this
.getXcodeProjectArgs(platformData, projectData)
.filter((arg) => !arg.startsWith("IPHONEOS_DEPLOYMENT_TARGET=")),
)
.concat(this.getCatalystDeploymentTargetArgs(projectData))
.concat(this.getBuildLoggingArgs());
}

public async getBuildForSimulatorArgs(
platformData: IPlatformData,
projectData: IProjectData,
Expand Down Expand Up @@ -254,6 +290,44 @@ export class XcodebuildArgsService implements IXcodebuildArgsService {
return this.$logger.getLevel() === "INFO" ? ["-quiet"] : [];
}

/**
* Mac Catalyst starts at iOS 13.1, so a project that still targets an older iOS
* cannot be built as-is. Raise the deployment target for the Catalyst build only
* rather than failing — the iOS build keeps whatever the app has chosen.
* `MACCATALYST_DEPLOYMENT_TARGET` is passed alongside because the runtime's
* metadata generator reads it and older runtimes crash when it is unset.
*/
private getCatalystDeploymentTargetArgs(projectData: IProjectData): string[] {
const buildSettingsFilePath = path.join(
projectData.appResourcesDirectoryPath,
this.$devicePlatformsConstants.iOS,
constants.BUILD_XCCONFIG_FILE_NAME,
);
const projectDeploymentTarget = this.$xcconfigService.readPropertyValue(
buildSettingsFilePath,
"IPHONEOS_DEPLOYMENT_TARGET",
);
const minimum = XcodebuildArgsService.MIN_CATALYST_DEPLOYMENT_TARGET;
let deploymentTarget = projectDeploymentTarget;

if (
!deploymentTarget ||
semver.lt(semver.coerce(deploymentTarget), semver.coerce(minimum))
) {
if (deploymentTarget) {
this.$logger.warn(
`Mac Catalyst requires iOS ${minimum} or higher. Building the Mac Catalyst app with IPHONEOS_DEPLOYMENT_TARGET=${minimum} instead of the project's ${deploymentTarget}.`,
);
}
deploymentTarget = minimum;
}

return [
`IPHONEOS_DEPLOYMENT_TARGET=${deploymentTarget}`,
`MACCATALYST_DEPLOYMENT_TARGET=${deploymentTarget}`,
];
}

private getBuildCommonArgs(
platformData: IPlatformData,
projectData: IProjectData,
Expand Down
16 changes: 16 additions & 0 deletions lib/services/ios/xcodebuild-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ export class XcodebuildService implements IXcodebuildService {
});
}

public async buildForCatalyst(
platformData: IPlatformData,
projectData: IProjectData,
buildConfig: IBuildConfig
): Promise<void> {
const args = this.$xcodebuildArgsService.getBuildForCatalystArgs(
platformData,
projectData,
buildConfig
);
await this.$xcodebuildCommandService.executeCommand(args, {
cwd: platformData.projectRoot,
stdio: buildConfig.buildOutputStdio,
});
}

public async buildForAppStore(
platformData: IPlatformData,
projectData: IProjectData,
Expand Down
Loading