Skip to content

Commit

Permalink
fix(ios): optionally build macos and/or ios arm64 sim
Browse files Browse the repository at this point in the history
- based on if all libraries support arm64 sim. If not, exclude arm64 at build time
- decide mac on basis of manifest (if available) or xcode target's SUPPORTS_MACCATALYST value
- read info.plist to decide the xcframework archs
  • Loading branch information
vijaysingh-axway authored and sgtcoolguy committed Sep 22, 2020
1 parent ca66820 commit d862d43
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 88 deletions.
36 changes: 20 additions & 16 deletions iphone/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const appc = require('node-appc'),
__n = i18n.__n,
parallel = appc.async.parallel,
series = appc.async.series,
plist = require('simple-plist'),
version = appc.version;
const platformsRegExp = new RegExp('^(' + ti.allPlatformNames.join('|') + ')$'); // eslint-disable-line security/detect-non-literal-regexp
const pemCertRegExp = /(^-----BEGIN CERTIFICATE-----)|(-----END CERTIFICATE-----.*$)|\n/g;
Expand Down Expand Up @@ -2225,29 +2226,32 @@ iOSBuilder.prototype.validate = function validate(logger, config, cli) {
module.libFile = path.join(module.modulePath, module.libName);
module.isFramework = true;

// TODO: read Info.plist to get the full scope of targets/arches supported!
let archDir = 'ios-arm64_i386_x86_64-simulator';
if (!fs.existsSync(path.join(module.libFile, archDir))) {
// Try XCode 11 dir w/o arm64 support
archDir = 'ios-i386_x86_64-simulator';
this.legacyModules.add(module.id);// Record that this won't support arm64 sim!
const xcFrameworkInfo = plist.readFileSync(path.join(module.libFile, 'Info.plist'));
for (const libInfo of xcFrameworkInfo.AvailableLibraries) {
if (libInfo.SupportedPlatformVariant === undefined) {
// Device library is used for hash calculation.
// TODO: Probably we want to add other varient's library as well.
nativeHashes.push(module.hash = this.hash(fs.readFileSync(path.join(module.libFile, libInfo.LibraryIdentifier, 'lib' + module.id.toLowerCase() + '.a'))));
} else if (libInfo.SupportedPlatformVariant === 'simulator' && !libInfo.SupportedArchitectures.includes('arm64')) {
this.legacyModules.add(module.id);// Record that this won't support arm64 sim!
}
}
// TODO: Change hash calculation
nativeHashes.push(module.hash = this.hash(fs.readFileSync(path.join(module.libFile, archDir, 'lib' + module.id.toLowerCase() + '.a'))));
} else if (fs.existsSync(path.join(module.modulePath, xcFrameworkOfFramework))) {
module.libName = xcFrameworkOfFramework;
module.libFile = path.join(module.modulePath, module.libName);
module.isFramework = true;

// TODO: read Info.plist to get the full scope of targets/arches supported!
let archDir = 'ios-arm64_i386_x86_64-simulator';
if (!fs.existsSync(path.join(module.libFile, archDir))) {
// Try XCode 11 dir w/o arm64 support
archDir = 'ios-i386_x86_64-simulator';
this.legacyModules.add(module.id);// Record that this won't support arm64 sim!
const xcFrameworkInfo = plist.readFileSync(path.join(module.libFile, 'Info.plist'));
const scrubbedModuleId = this.scrubbedModuleId(module.id);
for (const libInfo of xcFrameworkInfo.AvailableLibraries) {
if (libInfo.SupportedPlatformVariant === undefined) {
// Device library is used for hash calculation.
// TODO: Probably we want to add other varient's library as well.
nativeHashes.push(module.hash = this.hash(fs.readFileSync(path.join(module.libFile, libInfo.LibraryIdentifier, scrubbedModuleId + '.framework', scrubbedModuleId))));
} else if (libInfo.SupportedPlatformVariant === 'simulator' && !libInfo.SupportedArchitectures.includes('arm64')) {
this.legacyModules.add(module.id);// Record that this won't support arm64 sim!
}
}
// TODO: Change hash calculation
nativeHashes.push(module.hash = this.hash(fs.readFileSync(path.join(module.libFile, archDir, this.scrubbedModuleId(module.id) + '.framework', this.scrubbedModuleId(module.id)))));
} else {
this.logger.error(__('Module %s (%s) is missing library or framework file.', module.id.cyan, (module.manifest.version || 'latest').cyan) + '\n');
this.logger.error(__('Please validate that your module has been packaged correctly and try it again.'));
Expand Down
225 changes: 153 additions & 72 deletions iphone/cli/commands/_buildModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

'use strict';

const { exec, spawn } = require('child_process'); // eslint-disable-line security/detect-child-process
const { spawn } = require('child_process'); // eslint-disable-line security/detect-child-process

const appc = require('node-appc'),
archiver = require('archiver'),
Expand All @@ -29,7 +29,11 @@ const appc = require('node-appc'),
temp = require('temp'),
util = require('util'),
__ = appc.i18n(__dirname).__,
series = appc.async.series;
series = appc.async.series,
xcode = require('xcode');

const plist = require('simple-plist');
const parsePlist = util.promisify(plist.readFile);

function iOSModuleBuilder() {
Builder.apply(this, arguments);
Expand All @@ -47,6 +51,7 @@ iOSModuleBuilder.prototype.validate = function validate(logger, config, cli) {
this.moduleName = cli.manifest.name;
this.moduleIdAsIdentifier = this.scrubbedName(this.moduleId);
this.moduleVersion = cli.manifest.version;
this.isMacOSEnabled = this.detectMacOSTarget();
this.moduleGuid = cli.manifest.guid;
this.isFramework = fs.existsSync(path.join(this.projectDir, 'Info.plist')); // TODO: There MUST be a better way to determine if it's a framework (Swift)

Expand Down Expand Up @@ -86,6 +91,32 @@ iOSModuleBuilder.prototype.validate = function validate(logger, config, cli) {
}.bind(this);
};

iOSModuleBuilder.prototype.detectMacOSTarget = function detectMacOSTarget() {
if (this.manifest.mac !== undefined) {
return this.manifest.mac === 'true';
}
const pbxFilePath = path.join(this.projectDir, `${this.moduleName}.xcodeproj`, 'project.pbxproj');

if (!fs.existsSync(pbxFilePath)) {
this.logger.warn(__(`The Xcode project does not contain the default naming scheme. This is required to detect macOS targets in your module.\nPlease rename your Xcode project to "${this.moduleName}.xcodeproj"`));
return true;
}

let isMacOSEnabled = true;
const proj = xcode.project(pbxFilePath).parseSync();
const configurations = proj.hash.project.objects.XCBuildConfiguration;

for (const key of Object.keys(proj.hash.project.objects.XCBuildConfiguration)) {
const configuration = configurations[key];
if (typeof configuration === 'object' && configuration.buildSettings.SUPPORTS_MACCATALYST) {
isMacOSEnabled = configuration.buildSettings.SUPPORTS_MACCATALYST === 'YES';
break;
}
}

return isMacOSEnabled;
};

iOSModuleBuilder.prototype.run = function run(logger, config, cli, finished) {
Builder.prototype.run.apply(this, arguments);

Expand All @@ -107,7 +138,9 @@ iOSModuleBuilder.prototype.run = function run(logger, config, cli, finished) {
'compileJS',
'buildModule',
'createUniversalBinary',
'verifyBuildArch',
function (next) {
this.verifyBuildArch().then(next, next);
},
'packageModule',
'runModule',

Expand Down Expand Up @@ -447,6 +480,9 @@ iOSModuleBuilder.prototype.buildModule = function buildModule(next) {
this.logger.error('[' + type + '] ' + line);
}, this);
this.logger.log();
if (type === 'xcode-macos') {
this.logger.info(__('To exclude mac target, set/add mac: false in manifest file.'));
}
process.exit(1);
}

Expand Down Expand Up @@ -476,10 +512,43 @@ iOSModuleBuilder.prototype.buildModule = function buildModule(next) {
args.push('SUPPORTS_MACCATALYST=YES');
}

if (target === 'iphonesimulator') {
// Search for third party framewrok included in module. If found any, exclude arm64 from simulator build.
// Assumption is that simulator arm64 architecture can only be supported via .xcframework.
const frameworksPath = path.join(this.projectDir, 'platform');
const legacyFrameworks = new Set();

fs.readdirSync(frameworksPath).forEach(filename => {
if (filename.endsWith('.framework')) {
legacyFrameworks.add(filename);
}
});
if (legacyFrameworks.size > 0) {
const pbxFilePath = path.join(this.projectDir, `${this.moduleName}.xcodeproj`, 'project.pbxproj');
const proj = xcode.project(pbxFilePath).parseSync();
const configurations = proj.hash.project.objects.XCBuildConfiguration;
let excludeArchs = 'EXCLUDED_ARCHS=arm64 ';

// Merge with excluded archs in xcode setting
for (const key of Object.keys(proj.hash.project.objects.XCBuildConfiguration)) {
const configuration = configurations[key];
if (typeof configuration === 'object' && configuration.name === 'Release' && configuration.buildSettings.EXCLUDED_ARCHS) {
let archs = configuration.buildSettings.EXCLUDED_ARCHS; // e.g. "i386 arm64 x86_64"
archs = archs.replace(/["]/g, '');
excludeArchs = excludeArchs.concat(archs);
break;
}
}

args.push(excludeArchs);
this.logger.warn(__(`The module is using frameworks (${Array.from(legacyFrameworks)}) that do not support simulator arm64. Excluding simulator arm64. The app using this module may fail if you're on an arm64 Apple Silicon device.`));
}
}

return args;
}.bind(this);

this.cli.env.getOSInfo(function (osInfo) {
this.cli.env.getOSInfo(osInfo => {
const macOsVersion = osInfo.osver;

// 1. Create a build for the simulator
Expand All @@ -488,8 +557,13 @@ iOSModuleBuilder.prototype.buildModule = function buildModule(next) {
xcodebuildHook(xcBuild, xcodeBuildArgumentsForTarget('iphoneos'), opts, 'xcode-dist', () => {
const osVersionParts = macOsVersion.split('.').map(n => parseInt(n));
if (osVersionParts[0] > 10 || (osVersionParts[0] === 10 && osVersionParts[1] >= 15)) {
// 3. Create a build for the maccatalyst
xcodebuildHook(xcBuild, xcodeBuildArgumentsForTarget('macosx'), opts, 'xcode-macos', next);
// 3. Create a build for the mac-catalyst if enabled
if (this.isMacOSEnabled) {
xcodebuildHook(xcBuild, xcodeBuildArgumentsForTarget('macosx'), opts, 'xcode-macos', next);
} else {
this.logger.info(__('macOS support disabled in Xcode project. Skipping …'));
next();
}
} else {
this.logger.warn(__('Ignoring build for mac as mac target is < 10.15'));
next();
Expand Down Expand Up @@ -558,15 +632,17 @@ iOSModuleBuilder.prototype.createUniversalBinary = function createUniversalBinar
args.push('-headers');
args.push(headerPath);
}
lib = findLib('macosx');
if (lib instanceof Error) {
this.logger.warn(__('The module is missing 64-bit support of macos. Ignoring mac target for this module...'));
} else {
args.push(buildType);
args.push(lib);
if (!this.isFramework) {
args.push('-headers');
args.push(headerPath);
if (this.isMacOSEnabled) {
lib = findLib('macosx');
if (lib instanceof Error) {
this.logger.warn(__('The module is missing macOS support. Ignoring mac target for this module...'));
} else {
args.push(buildType);
args.push(lib);
if (!this.isFramework) {
args.push('-headers');
args.push(headerPath);
}
}
}

Expand All @@ -592,84 +668,89 @@ iOSModuleBuilder.prototype.createUniversalBinary = function createUniversalBinar
}.bind(this));
};

iOSModuleBuilder.prototype.verifyBuildArch = function verifyBuildArch(next) {
iOSModuleBuilder.prototype.verifyBuildArch = async function verifyBuildArch() {
this.logger.info(__('Verifying universal library'));

// TODO: We want to verify the set of targets/arches built
// The manifest shouldn't matter any more?
// Can we just ensure it's an xcframework?

const findLib = function (dest) {
const moduleId = this.isFramework ? this.moduleIdAsIdentifier : this.moduleId;
const libName = this.isFramework ? this.moduleIdAsIdentifier + '.framework' : 'lib' + this.moduleId + '.a';
const lib = path.join(this.projectDir, 'build', moduleId + '.xcframework', dest, libName);
this.logger.info(__('Looking for ' + lib));
const moduleId = this.isFramework ? this.moduleIdAsIdentifier : this.moduleId;
const frameworkPath = path.join(this.projectDir, 'build', moduleId + '.xcframework');
// Make sure xcframework exists...
if (!(await fs.pathExists(frameworkPath))) {
throw new Error(__(`Unable to find "${moduleId}.xcframework"`));
}

if (!fs.existsSync(lib)) {
return new Error(__('Unable to find the built %s library', 'Release-' + dest));
// Confirm that the library/binary the Info.plist is pointing at actually exists on disk
const verifyLibraryExists = async (libInfo) => {
let libraryPath = path.join(frameworkPath, libInfo.LibraryIdentifier, libInfo.LibraryPath);
if (libraryPath.endsWith('.framework')) {
const frameworkName = path.basename(libraryPath, '.framework');
libraryPath = path.join(libraryPath, frameworkName);
}
return lib;
}.bind(this);
if (!(await fs.pathExists(libraryPath))) {
throw new Error(__('Unable to find the built library %s', libraryPath));
}
};

// Verify the architectures and platform variants
const buildArchs = new Set();
// TODO: Just read the Info.plist and take it's listing!
let lib = findLib('ios-arm64_armv7');
if (lib instanceof Error) {
// fallback to xcode 11 xcframework
lib = findLib('ios-armv7_arm64');
if (lib instanceof Error) {
this.logger.warn(__('The module is missing 64-bit support.'));
} else {
buildArchs.add('armv7');
buildArchs.add('arm64');
}
} else {
buildArchs.add('armv7');
buildArchs.add('arm64');

const xcFrameworkInfo = await parsePlist(path.join(frameworkPath, 'Info.plist'));

// Check iOS device
const iosDevice = xcFrameworkInfo.AvailableLibraries.find(l => l.SupportedPlatform === 'ios' && !l.SupportedPlatformVariant);
if (!iosDevice) {
throw new Error('The module is missing iOS device support.');
}
await verifyLibraryExists(iosDevice);
if (!iosDevice.SupportedArchitectures.includes('arm64')) {
this.logger.warn(__('The module is missing iOS device 64-bit support.'));
}
iosDevice.SupportedArchitectures.forEach(arch => buildArchs.add(arch));

lib = findLib('ios-arm64_i386_x86_64-simulator');
if (lib instanceof Error) {
// fall back to xcode 11 xcframework w/o arm64 sim support
lib = findLib('ios-i386_x86_64-simulator');
if (lib instanceof Error) {
// neither is available, so no sim support!
this.logger.warn(__('The module is missing ios simulator support.'));
} else {
this.logger.warn(__('The module is missing arm64 ios simulator support.'));
buildArchs.add('i386');
buildArchs.add('x86_64');
}
// Check iOS Simulator
const iosSim = xcFrameworkInfo.AvailableLibraries.find(l => l.SupportedPlatformVariant === 'simulator');
if (!iosSim) {
throw new Error(__('The module is missing iOS simulator support.'));
}
await verifyLibraryExists(iosSim);
if (!iosSim.SupportedArchitectures.includes('arm64')) {
this.logger.warn(__('The module is missing arm64 iOS simulator support.'));
}
iosSim.SupportedArchitectures.forEach(arch => buildArchs.add(arch));

// MacOS Catalyst support is optional
const macos = xcFrameworkInfo.AvailableLibraries.find(l => l.SupportedPlatformVariant === 'maccatalyst');
if (!macos) {
this.logger.warn(__('The module is missing macOS support.'));
} else {
buildArchs.add('i386');
buildArchs.add('x86_64');
// buildArchs.add('arm64'); // Don't include as we traditionally meant 'arm64' to be ios device arm64 (not sim!)
await verifyLibraryExists(macos);
if (!macos.SupportedArchitectures.includes('arm64')) {
this.logger.warn(__('The module is missing arm64 macOS support. This will not work on Apple Silicon devices (and is likley due to use of an Xcode that does not support arm64 maccatalyst).'));
}
}

lib = findLib('ios-arm64_x86_64-maccatalyst');
if (lib instanceof Error) {
// fall back to Xcode 12 GM variant (betas include arm64, but xcode 12 final does not)
lib = findLib('ios-x86_64-maccatalyst');
if (lib instanceof Error) {
this.logger.warn(__('The module is missing maccatalyst support.'));
// Spit out the platform variants and their architectures
for (const libInfo of xcFrameworkInfo.AvailableLibraries) {
let target;
if (libInfo.SupportedPlatformVariant === 'maccatalyst') {
target = 'Mac';
} else if (libInfo.SupportedPlatformVariant === 'simulator') {
target = 'Simulator';
} else {
this.logger.warn(__('The module is missing arm64 maccatalyst support. This will not work on Apple Silicon devices (and is likley due to use of an Xcode that does not support arm64 maccatalyst).'));
target = 'Device';
}
this.logger.info(__(`${target} has architectures: ${libInfo.SupportedArchitectures}`));
}

// Match against manifest
// TODO: Drop architectures from manifest altogether now?
const manifestArchs = new Set(this.manifest.architectures.split(' '));
if (buildArchs.size !== manifestArchs.size) {
this.logger.error(__('There is discrepancy between the architectures specified in module manifest and compiled binary.'));
this.logger.error(__('Architectures in manifest: %s', Array.from(manifestArchs)));
this.logger.error(__('Compiled binary architectures: %s', Array.from(buildArchs)));
this.logger.error(__('Please update manifest to match module binary architectures.') + '\n');
process.exit(1);
process.exit(1); // TODO: Just throw an Error!
}

if (!buildArchs.has('arm64')) {
this.logger.warn(__('The module is missing 64-bit support.'));
}
next();
};

iOSModuleBuilder.prototype.packageModule = function packageModule(next) {
Expand Down Expand Up @@ -894,7 +975,7 @@ iOSModuleBuilder.prototype.runModule = function runModule(next) {

// 5. unzip module to the tmp dir. Use native binary on macOS, as AdmZip doesn't support symlinks used in mac catalyst frameworks
const proc = spawn('unzip', [ '-o', this.moduleZipPath, '-d', tmpProjectDir ]);
proc.stdout.on('data', data => this.logger.info(data.toString().trimEnd()));
proc.stdout.on('data', data => this.logger.trace(data.toString().trimEnd()));
proc.stderr.on('data', data => this.logger.error(data.toString().trimEnd()));
proc.once('error', err => cb(err));
proc.on('exit', () => cb());
Expand Down
1 change: 1 addition & 0 deletions iphone/templates/module/default/template/ios/manifest.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
version: 1.0.0
apiversion: 2
architectures: armv7 arm64 i386 x86_64
mac: true
description: <%- moduleName %>
author: <%- author %>
license: Specify your license
Expand Down

0 comments on commit d862d43

Please sign in to comment.