Skip to content

Commit

Permalink
feat!: Better Catalyst build support (#1313)
Browse files Browse the repository at this point in the history
* feat!: Support for Catalyst builds

`cordova build ios --device --target=mac ...`

Output is to `build/Debug-maccatalyst`

* fix: Disable iPad-on-Mac builds in favour of Catalyst

Xcode warns that only one can be enabled at a time, so we need to pick
one, and Catalyst seems like the better option for a proper macOS app
experience.

* feat!: Enable compiling for Apple Vision platform

* chore(ci): Add unit tests for run and Catalyst stuff

* fix(build): Don't check for ios-deploy at build time

If neither `--device` nor `--emulator` are specified for the build
command, it will check for a connected device and assume `--device` if
one is found. However, it was also checking for the availability of the
ios-deploy tool which is used to deploy to a connected device.

If we're just building, we don't need to check for a deploy tool. The
run command already has this check to ensure that ios-deploy is
available before actually trying to deploy.

Closes GH-420.
Closes GH-677.
  • Loading branch information
dpogue authored Aug 30, 2024
1 parent adcc1fe commit f81013b
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 174 deletions.
20 changes: 16 additions & 4 deletions CordovaLib/CordovaLib.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,10 @@
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
PUBLIC_HEADERS_FOLDER_PATH = include/Cordova;
TARGETED_DEVICE_FAMILY = "1,2";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
TARGETED_DEVICE_FAMILY = "1,2,7";
XROS_DEPLOYMENT_TARGET = 1.0;
};
name = Debug;
};
Expand All @@ -649,7 +652,10 @@
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
PUBLIC_HEADERS_FOLDER_PATH = include/Cordova;
TARGETED_DEVICE_FAMILY = "1,2";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
TARGETED_DEVICE_FAMILY = "1,2,7";
XROS_DEPLOYMENT_TARGET = 1.0;
};
name = Release;
};
Expand Down Expand Up @@ -809,9 +815,12 @@
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
PRODUCT_BUNDLE_IDENTIFIER = org.apache.cordova.Cordova;
SKIP_INSTALL = NO;
TARGETED_DEVICE_FAMILY = "1,2";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
TARGETED_DEVICE_FAMILY = "1,2,7";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
XROS_DEPLOYMENT_TARGET = 1.0;
};
name = Debug;
};
Expand All @@ -832,9 +841,12 @@
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
PRODUCT_BUNDLE_IDENTIFIER = org.apache.cordova.Cordova;
SKIP_INSTALL = NO;
TARGETED_DEVICE_FAMILY = "1,2";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
TARGETED_DEVICE_FAMILY = "1,2,7";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
XROS_DEPLOYMENT_TARGET = 1.0;
};
name = Release;
};
Expand Down
51 changes: 36 additions & 15 deletions lib/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function getDefaultSimulatorTarget () {
/** @returns {Promise<void>} */
module.exports.run = function (buildOpts) {
const projectPath = this.root;
let emulatorTarget = '';
let emulatorTarget = 'iOS Device';
let projectName = '';

buildOpts = buildOpts || {};
Expand All @@ -115,6 +115,14 @@ module.exports.run = function (buildOpts) {
return Promise.reject(new CordovaError('Cannot specify "device" and "emulator" options together.'));
}

if (buildOpts.target && buildOpts.target.match(/mac/i)) {
buildOpts.catalyst = true;
buildOpts.device = true;
buildOpts.emulator = false;

emulatorTarget = 'macOS Catalyst';
}

if (buildOpts.buildConfig) {
if (!fs.existsSync(buildOpts.buildConfig)) {
return Promise.reject(new CordovaError(`Build config file does not exist: ${buildOpts.buildConfig}`));
Expand All @@ -133,15 +141,18 @@ module.exports.run = function (buildOpts) {
}
}

return require('./listDevices').run()
.then(devices => {
if (devices.length > 0 && !(buildOpts.emulator)) {
// we also explicitly set device flag in options as we pass
// those parameters to other api (build as an example)
buildOpts.device = true;
return check_reqs.check_ios_deploy();
return Promise.resolve()
.then(() => {
if (!buildOpts.emulator && !buildOpts.catalyst) {
return require('./listDevices').run().then(devices => {
if (devices.length > 0) {
// we explicitly set device flag in options
buildOpts.device = true;
}
});
}
}).then(() => {
})
.then(() => {
// CB-12287: Determine the device we should target when building for a simulator
if (!buildOpts.device) {
let newTarget = buildOpts.target || '';
Expand Down Expand Up @@ -175,8 +186,8 @@ module.exports.run = function (buildOpts) {
let extraConfig = '';
if (buildOpts.codeSignIdentity) {
extraConfig += `CODE_SIGN_IDENTITY = ${buildOpts.codeSignIdentity}\n`;
extraConfig += `CODE_SIGN_IDENTITY[sdk=iphoneos*] = ${buildOpts.codeSignIdentity}\n`;
}

if (buildOpts.provisioningProfile) {
if (typeof buildOpts.provisioningProfile === 'string') {
extraConfig += `PROVISIONING_PROFILE_SPECIFIER = ${buildOpts.provisioningProfile}\n`;
Expand Down Expand Up @@ -225,7 +236,7 @@ module.exports.run = function (buildOpts) {
const xcodebuildArgs = getXcodeBuildArgs(projectName, projectPath, configuration, emulatorTarget, buildOpts);
return execa('xcodebuild', xcodebuildArgs, { cwd: projectPath, stdio: 'inherit' });
}).then(() => {
if (!buildOpts.device || buildOpts.noSign) {
if (!buildOpts.device || buildOpts.catalyst || buildOpts.noSign) {
return;
}

Expand Down Expand Up @@ -336,7 +347,7 @@ function getXcodeBuildArgs (projectName, projectPath, configuration, emulatorTar
}
}

if (buildConfig.device) {
if (buildConfig.device && !buildConfig.catalyst) {
options = [
'-workspace', customArgs.workspace || `${projectName}.xcworkspace`,
'-scheme', customArgs.scheme || projectName,
Expand Down Expand Up @@ -377,10 +388,20 @@ function getXcodeBuildArgs (projectName, projectPath, configuration, emulatorTar
options = [
'-workspace', customArgs.workspace || `${projectName}.xcworkspace`,
'-scheme', customArgs.scheme || projectName,
'-configuration', customArgs.configuration || configuration,
'-sdk', customArgs.sdk || 'iphonesimulator',
'-destination', customArgs.destination || `platform=iOS Simulator,name=${emulatorTarget}`
'-configuration', customArgs.configuration || configuration
];

if (buildConfig.catalyst) {
options = options.concat([
'-destination', customArgs.destination || 'generic/platform=macOS,variant=Mac Catalyst'
]);
} else {
options = options.concat([
'-sdk', customArgs.sdk || 'iphonesimulator',
'-destination', customArgs.destination || `platform=iOS Simulator,name=${emulatorTarget}`
]);
}

buildActions = ['build'];
settings = [`SYMROOT=${path.join(projectPath, 'build')}`];

Expand Down
82 changes: 57 additions & 25 deletions lib/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,32 +41,38 @@ module.exports.run = function (runOptions) {
return module.exports.listDevices().then(() => module.exports.listEmulators());
}

let useDevice = !!runOptions.device;
const useCatalyst = runOptions.target && runOptions.target.match(/mac/i);
let useDevice = !!runOptions.device && !useCatalyst;
const configuration = runOptions.release ? 'Release' : 'Debug';

return require('./listDevices').run()
.then(devices => {
if (devices.length > 0 && !(runOptions.emulator)) {
useDevice = true;
// we also explicitly set device flag in options as we pass
// those parameters to other api (build as an example)
runOptions.device = true;
return check_reqs.check_ios_deploy();
return Promise.resolve()
.then(() => {
if (!runOptions.emulator && !useCatalyst) {
return module.exports.execListDevices().then(devices => {
if (devices.length > 0) {
useDevice = true;

// we also explicitly set device flag in options as we pass
// those parameters to other api (build as an example)
runOptions.device = true;
return check_reqs.check_ios_deploy();
}
});
}
}).then(() => {
})
.then(() => {
if (!runOptions.nobuild) {
return build.run(runOptions);
} else {
return Promise.resolve();
}
}).then(() => build.findXCodeProjectIn(projectPath))
})
.then(() => build.findXCodeProjectIn(projectPath))
.then(projectName => {
let appPath = path.join(projectPath, 'build', `${configuration}-iphonesimulator`, `${projectName}.app`);
const buildOutputDir = path.join(projectPath, 'build', `${configuration}-iphoneos`);

// select command to run and arguments depending whether
// we're running on device/emulator
// we're running on device/catalyst/emulator
if (useDevice) {
const buildOutputDir = path.join(projectPath, 'build', `${configuration}-iphoneos`);
const appPath = path.join(buildOutputDir, `${projectName}.app`);

return module.exports.checkDeviceConnected()
.then(() => {
// Unpack IPA
Expand All @@ -78,21 +84,19 @@ module.exports.run = function (runOptions) {
.then(() => {
// Uncompress IPA (zip file)
const appFileInflated = path.join(buildOutputDir, 'Payload', `${projectName}.app`);
const appFile = path.join(buildOutputDir, `${projectName}.app`);
const payloadFolder = path.join(buildOutputDir, 'Payload');

// delete the existing platform/ios/build/device/appname.app
fs.rmSync(appFile, { recursive: true, force: true });
fs.rmSync(appPath, { recursive: true, force: true });
// move the platform/ios/build/device/Payload/appname.app to parent
fs.renameSync(appFileInflated, appFile);
fs.renameSync(appFileInflated, appPath);
// delete the platform/ios/build/device/Payload folder
fs.rmSync(payloadFolder, { recursive: true, force: true });

return null;
})
.then(
() => {
appPath = path.join(projectPath, 'build', `${configuration}-iphoneos`, `${projectName}.app`);
let extraArgs = [];
if (runOptions.argv) {
// argv.slice(2) removes node and run.js, filterSupportedArgs removes the run.js args
Expand All @@ -101,9 +105,14 @@ module.exports.run = function (runOptions) {
return module.exports.deployToDevice(appPath, runOptions.target, extraArgs);
},
// if device connection check failed use emulator then
// This might fail due to being the wrong type of app bundle
() => module.exports.deployToSim(appPath, runOptions.target)
);
} else if (useCatalyst) {
const appPath = path.join(projectPath, 'build', `${configuration}-maccatalyst`, `${projectName}.app`);
return module.exports.deployToMac(appPath);
} else {
const appPath = path.join(projectPath, 'build', `${configuration}-iphonesimulator`, `${projectName}.app`);
return module.exports.deployToSim(appPath, runOptions.target);
}
})
Expand All @@ -113,10 +122,13 @@ module.exports.run = function (runOptions) {
module.exports.filterSupportedArgs = filterSupportedArgs;
module.exports.checkDeviceConnected = checkDeviceConnected;
module.exports.deployToDevice = deployToDevice;
module.exports.deployToMac = deployToMac;
module.exports.deployToSim = deployToSim;
module.exports.startSim = startSim;
module.exports.listDevices = listDevices;
module.exports.listEmulators = listEmulators;
module.exports.execListDevices = execListDevices;
module.exports.execListEmulatorTargets = execListEmulatorTargets;

/**
* Filters the args array and removes supported args for the 'run' command.
Expand Down Expand Up @@ -164,6 +176,16 @@ function deployToDevice (appPath, target, extraArgs) {
return execa('ios-deploy', args.concat(extraArgs), { stdio: 'inherit' });
}

/**
* Runs specified app package on the local macOS system.
* @param {String} appPath Path to application package
* @return {Promise} Resolves when deploy succeeds otherwise rejects
*/
function deployToMac (appPath) {
events.emit('log', 'Deploying to local macOS system');
return execa('open', [appPath], { stdio: 'inherit' });
}

/**
* Deploy specified app package to ios-sim simulator
* @param {String} appPath Path to application package
Expand All @@ -175,13 +197,13 @@ async function deployToSim (appPath, target) {

if (!target) {
// Select target device for emulator (preferring iPhone Emulators)
const emulators = await require('./listEmulatorImages').run();
const emulators = await module.exports.execListEmulatorTargets();
const iPhoneEmus = emulators.filter(emulator => emulator.startsWith('iPhone'));
target = iPhoneEmus.concat(emulators)[0];
events.emit('log', `No target specified for emulator. Deploying to "${target}" simulator.`);
}

return startSim(appPath, target);
return module.exports.startSim(appPath, target);
}

function startSim (appPath, target) {
Expand Down Expand Up @@ -210,8 +232,18 @@ function startSim (appPath, target) {
return subprocess;
}

/* istanbul ignore next */
function execListDevices () {
return require('./listDevices').run();
}

/* istanbul ignore next */
function execListEmulatorTargets () {
return require('./listEmulatorTargets').run();
}

function listDevices () {
return require('./listDevices').run()
return module.exports.execListDevices()
.then(devices => {
events.emit('log', 'Available iOS Devices:');
devices.forEach(device => {
Expand All @@ -221,7 +253,7 @@ function listDevices () {
}

function listEmulators () {
return require('./listEmulatorImages').run()
return module.exports.execListEmulatorTargets()
.then(emulators => {
events.emit('log', 'Available iOS Simulators:');
emulators.forEach(emulator => {
Expand Down
8 changes: 7 additions & 1 deletion templates/project/__PROJECT_NAME__.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
objects = {

/* Begin PBXBuildFile section */
902AE2142C6C059A0041150F /* Cordova.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 907F985F2C06B8DE00D2D242 /* Cordova.framework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
902AE2142C6C059A0041150F /* Cordova.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 907F985F2C06B8DE00D2D242 /* Cordova.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
907F98562C06B87200D2D242 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 907F98552C06B87200D2D242 /* PrivacyInfo.xcprivacy */; };
907F98662C06BC1B00D2D242 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 907F98652C06BC1B00D2D242 /* config.xml */; };
907F986A2C06BCD300D2D242 /* www in Resources */ = {isa = PBXBuildFile; fileRef = 907F98692C06BCD300D2D242 /* www */; };
Expand Down Expand Up @@ -388,8 +388,11 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_WORKSPACE = NO;
};
name = Debug;
};
Expand Down Expand Up @@ -445,8 +448,11 @@
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_COMPILATION_MODE = wholemodule;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VALIDATE_WORKSPACE = NO;
};
name = Release;
};
Expand Down
Loading

0 comments on commit f81013b

Please sign in to comment.