Custom Cordova Plugin: Add framework to "Embedded Binaries"
To get my plugin to build with a project on XCode 8.0 and cordova-ios 4.2, I had to run the hook in the after_build
phase. Also, make sure that the node environment is using the latest verison of xcode-node (^0.8.9) or you will get bugs in the copy files phase.
<framework src="lib/myCustom.framework" custom="true" embed="true" />
<hook type="after_build" src="hooks/add_embedded.js" />
The plugin.xml needs custom="true"
for Cordova to copy the framework file, which ended up conflicting with the changes made to the .pbxproj when this hook ran in after_platform add or even after_prepare.
I've implemented a workaround until it's supported by Cordova's plugin.xml, hopefully, in the future, once an embed
property in such entries will have the same effect: <framework embed="true" src="..." />
, for now, this property does not help, hence the following workaround.
The following solution worked using Cordova version 5.3.3.
First, make sure to add the framework entry to plugin.xml:
<framework src="pointToYour/File.framework" embed="true" />
embed="true"
doesn't work for now, but add it anyway.
We're gonna create a hook, declare that in your plugin.xml:
<hook type="after_platform_add" src="hooks/embedframework/addEmbedded.js" />
Next, there's a specific node module we're gonna need in our hook's code, that module is node-xcode.
Install node-xcode (must be 0.8.7 version or above):
npm i xcode
Finally, the code for the hook itself -
addEmbedded.js file:
'use strict';
const xcode = require('xcode'),
fs = require('fs'),
path = require('path');
module.exports = function(context) {
if(process.length >=5 && process.argv[1].indexOf('cordova') == -1) {
if(process.argv[4] != 'ios') {
return; // plugin only meant to work for ios platform.
}
}
function fromDir(startPath,filter, rec, multiple){
if (!fs.existsSync(startPath)){
console.log("no dir ", startPath);
return;
}
const files=fs.readdirSync(startPath);
var resultFiles = []
for(var i=0;i<files.length;i++){
var filename=path.join(startPath,files[i]);
var stat = fs.lstatSync(filename);
if (stat.isDirectory() && rec){
fromDir(filename,filter); //recurse
}
if (filename.indexOf(filter)>=0) {
if (multiple) {
resultFiles.push(filename);
} else {
return filename;
}
}
}
if(multiple) {
return resultFiles;
}
}
function getFileIdAndRemoveFromFrameworks(myProj, fileBasename) {
var fileId = '';
const pbxFrameworksBuildPhaseObjFiles = myProj.pbxFrameworksBuildPhaseObj(myProj.getFirstTarget().uuid).files;
for(var i=0; i<pbxFrameworksBuildPhaseObjFiles.length;i++) {
var frameworkBuildPhaseFile = pbxFrameworksBuildPhaseObjFiles[i];
if(frameworkBuildPhaseFile.comment && frameworkBuildPhaseFile.comment.indexOf(fileBasename) != -1) {
fileId = frameworkBuildPhaseFile.value;
pbxFrameworksBuildPhaseObjFiles.splice(i,1); // MUST remove from frameworks build phase or else CodeSignOnCopy won't do anything.
break;
}
}
return fileId;
}
function getFileRefFromName(myProj, fName) {
const fileReferences = myProj.hash.project.objects['PBXFileReference'];
var fileRef = '';
for(var ref in fileReferences) {
if(ref.indexOf('_comment') == -1) {
var tmpFileRef = fileReferences[ref];
if(tmpFileRef.name && tmpFileRef.name.indexOf(fName) != -1) {
fileRef = ref;
break;
}
}
}
return fileRef;
}
const xcodeProjPath = fromDir('platforms/ios','.xcodeproj', false);
const projectPath = xcodeProjPath + '/project.pbxproj';
const myProj = xcode.project(projectPath);
function addRunpathSearchBuildProperty(proj, build) {
const LD_RUNPATH_SEARCH_PATHS = proj.getBuildProperty("LD_RUNPATH_SEARCH_PATHS", build);
if(!LD_RUNPATH_SEARCH_PATHS) {
proj.addBuildProperty("LD_RUNPATH_SEARCH_PATHS", "\"$(inherited) @executable_path/Frameworks\"", build);
} else if(LD_RUNPATH_SEARCH_PATHS.indexOf("@executable_path/Frameworks") == -1) {
var newValue = LD_RUNPATH_SEARCH_PATHS.substr(0,LD_RUNPATH_SEARCH_PATHS.length-1);
newValue += ' @executable_path/Frameworks\"';
proj.updateBuildProperty("LD_RUNPATH_SEARCH_PATHS", newValue, build);
}
}
myProj.parseSync();
addRunpathSearchBuildProperty(myProj, "Debug");
addRunpathSearchBuildProperty(myProj, "Release");
// unquote (remove trailing ")
var projectName = myProj.getFirstTarget().firstTarget.name.substr(1);
projectName = projectName.substr(0, projectName.length-1); //Removing the char " at beginning and the end.
const groupName = 'Embed Frameworks ' + context.opts.plugin.id;
const pluginPathInPlatformIosDir = projectName + '/Plugins/' + context.opts.plugin.id;
process.chdir('./platforms/ios');
const frameworkFilesToEmbed = fromDir(pluginPathInPlatformIosDir ,'.framework', false, true);
process.chdir('../../');
if(!frameworkFilesToEmbed.length) return;
myProj.addBuildPhase(frameworkFilesToEmbed, 'PBXCopyFilesBuildPhase', groupName, myProj.getFirstTarget().uuid, 'frameworks');
for(var frmFileFullPath of frameworkFilesToEmbed) {
var justFrameworkFile = path.basename(frmFileFullPath);
var fileRef = getFileRefFromName(myProj, justFrameworkFile);
var fileId = getFileIdAndRemoveFromFrameworks(myProj, justFrameworkFile);
// Adding PBXBuildFile for embedded frameworks
var file = {
uuid: fileId,
basename: justFrameworkFile,
settings: {
ATTRIBUTES: ["CodeSignOnCopy", "RemoveHeadersOnCopy"]
},
fileRef:fileRef,
group:groupName
};
myProj.addToPbxBuildFileSection(file);
// Adding to Frameworks as well (separate PBXBuildFile)
var newFrameworkFileEntry = {
uuid: myProj.generateUuid(),
basename: justFrameworkFile,
fileRef:fileRef,
group: "Frameworks"
};
myProj.addToPbxBuildFileSection(newFrameworkFileEntry);
myProj.addToPbxFrameworksBuildPhase(newFrameworkFileEntry);
}
fs.writeFileSync(projectPath, myProj.writeSync());
console.log('Embedded Frameworks In ' + context.opts.plugin.id);
};
What this hook actually does:
- Creates a "Build Phase" named after your plugin id, configured to "Copy Files", destination of that copy is "Frameworks".
- Finds and adds your .framework files to the above Build Phase, in turn, embedding it.
- Sets an Xcode build property named
LD_RUNPATH_SEARCH_PATHS
to also look for embedded frameworks in"@executable_path/Frameworks"
(That's were the embedded framework is going to be copied to after the "Copy Files"->"Frameworks" Build Phase - Configures the ATTRIBUTES key by setting "CodeSignOnCopy" and "RemoveHeadersOnCopy" for your .framework files.
- Removes your .framework files from the FrameworksBuildPhase and re-adds them to the FrameworksBuildPhase as new separated PBXBuildFiles (Same PBXFileReference), it has to be done in order for the "CodeSignOnCopy" to mean anything, without removing it, if you open the project with Xcode, you will not find a checkmark in the build phase that says it will sign it.
Updated 1: hook code, modifications:
- The hook automatically finds your .framework files, no need to edit the hook.
- Added an important modification, setting ATTRIBUTES "CodeSignOnCopy" and "RemoveHeadersOnCopy" for your .framework files.
- Improved the hook to allow it to work in such a case where multiple plugins use this hook.
Update 2
- Since my pull request has been accepted, there's no longer a need to install my own fork.
- Improved hook code.
Update 3 (19/09/2016)
Modified hook script according to Max Whaler's suggestion, as I experienced the same issue over Xcode 8.
Final Note
Once you upload your app to the AppStore, if validation fails due to unsupported architectures (i386, etc...), try the following Cordova plugin (only hook, no native code): zcordova-plugin-archtrim
For adding libraries to "Embedded Binaries" section in Xcode (Starting from cordova-ios 4.4.0 and cordova 7.0.0), put this in your plugin.xml:
<framework src="src/ios/XXX.framework" embed="true" custom="true" />
For adding libraries to "Linked Frameworks and Libraries" section in Xcode, put this in your plugin.xml:
<source-file src="src/ios/XXX.framework" target-dir="lib" framework="true" />
Both of them can exist at the same time. For example:
<!-- iOS Sample -->
<platform name="ios">
....
<source-file src="src/ios/XXX.m"/>
<source-file src="src/ios/XXX.framework" target-dir="lib" framework="true" />
<framework src="src/ios/XXX.framework" embed="true" custom="true" />
....
</platform>
<!-- Android Sample for your reference -->
<platform name="android">
....
<source-file src="src/android/XXX.java"/>
<framework src="src/android/build.gradle" custom="true" type="gradleReference" />
<resource-file src="src/android/SDK/libs/XXX.aar" target="libs/XXX.aar" />
....
</platform>