Custom Cordova Plugin: Add framework to Embedded Binaries
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
Cordova Plugin: Add custom framework using weak linking
Important note
Better use "Embedded Frameworks" functionality instead of this solution because dlopen
is forbidden since iOS 8.0 on non-mac/simulator devices (real iPhones/iPads).
Take a look at Custom Cordova Plugin: Add framework to "Embedded Binaries"
END OF Important note
I ended up doing something a bit different, Instead of declaring the .framework
as a <framework ... />
tag, I did the following.
I created a plugin hook that adds the plugin dir to the FRAMEWORK_SEARCH_PATHS Xcode build property.
<hook type="after_platform_add" src="hooks/addPluginDirToFrameworkSearchPaths/hook.js" />
Hook Code:
module.exports = function(context) {
const includesiOS = context.opts.platforms.indexOf('ios') != -1;
if(!includesiOS) return;
const
deferral = context.requireCordovaModule('q').defer(),
pluginId = context.opts.plugin.id;
const xcode = require('xcode'),
fs = require('fs'),
path = require('path');
function fromDir(startPath,filter, rec) {
if (!fs.existsSync(startPath)){
console.log("no dir ", startPath);
return;
}
const files=fs.readdirSync(startPath);
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) {
return filename;
}
}
}
const xcodeProjPath = fromDir('platforms/ios','.xcodeproj', false);
const projectPath = xcodeProjPath + '/project.pbxproj';
const myProj = xcode.project(projectPath);
function unquote(str) {
if (str) return str.replace(/^"(.*)"$/, "$1");
}
function getProjectName(myProj) {
var projectName = myProj.getFirstTarget().firstTarget.name;
projectName = unquote(projectName);
return projectName;
}
function set_FRAMEWORK_SEARCH_PATHS(proj) {
const lineToAdd = '"\\"' + getProjectName(proj) + '/Plugins/' + pluginId + '\\""'
const FRAMEWORK_SEARCH_PATHS = proj.getBuildProperty("FRAMEWORK_SEARCH_PATHS");
if(FRAMEWORK_SEARCH_PATHS != null) {
const isArray = typeof FRAMEWORK_SEARCH_PATHS != 'string';
if(isArray) {
for(var entry of FRAMEWORK_SEARCH_PATHS) {
if(entry.indexOf(pluginId) != -1) {
return false; // already exists, no need to do anything.
}
}
} else { // string
if(FRAMEWORK_SEARCH_PATHS.indexOf(pluginId) != -1) {
return false; // already exists, no need to do anything.
}
}
var newValueArray = isArray?FRAMEWORK_SEARCH_PATHS:[FRAMEWORK_SEARCH_PATHS];
newValueArray.push(lineToAdd);
proj.updateBuildProperty("FRAMEWORK_SEARCH_PATHS", newValueArray);
} else {
proj.addBuildProperty("FRAMEWORK_SEARCH_PATHS", lineToAdd);
}
return true;
}
myProj.parse(function (err) {
if(err) {
deferral.reject('Error while parsing project');
}
if(set_FRAMEWORK_SEARCH_PATHS(myProj)) {
fs.writeFileSync(projectPath, myProj.writeSync());
console.log('Added Framework Search Path for ' + pluginId);
} else {
console.log('Framework Search Path was already added for ' + pluginId);
}
deferral.resolve();
});
return deferral.promise;
};
Note: the hook depends on an NPM dependency named "xcode" so do npm i xcode --save
before (no need to edit the hook code).
Now the way we declare in plugin.xml to import the .framework
content to our project is the following:
<source-file src="ios/libs/CameraWizard.framework" />
<resource-file src="ios/libs/CameraWizard.framework/CameraWizard" />
We use the source-file
tag simply to import the .framework
because we only want it to be copied to the iOS platform plugins directory, we do not wish to have it "strongly" linked, the reason we need it there is only for it's Headers
, not it's binary. Our hook will add the correct framework search path for it.
Then we use resource-file
to import only the shared library file inside the .framework
directory, we add it as a resource so that when the app starts and we call dlopen(...)
, the shared library will be found in runtime.
Finally, Now to use the shared library in your plugin code, do the following:
#import <dlfcn.h>
(also import your.framework
's headers).Under
-(void)pluginInitialize
method, load the shared lib:NSString* resourcePath = [[NSBundle mainBundle] resourcePath];
NSString* dlPath = [NSString stringWithFormat: @"%@/FrameworkFileNameInResourceTag", resourcePath];
const char* cdlpath = [dlPath UTF8String];
dlopen(cdlpath, RTLD_LAZY|RTLD_GLOBAL);Now to use a class from the shared library:
SomeClassInFramework someInstance = [(SomeClassInFramework)[NSClassFromString(@"SomeClassInFramework") alloc] init];
How to Add a Third Party Framework to a Cordova 3.0 Custom Plugin?
Custom frameworks work with Cordova 3.4
<framework src="relative/path/to/my.framework" custom="true" />
Related Topics
Uitextview: Disable Selection, Allow Links
@Published Property Wrapper Not Working on Subclass of Observableobject
How to Manage Cookies with Uiwebview in Swift
Blocks on Swift (Animatewithduration:Animations:Completion:)
Didupdatelocations Instead of Didupdatetolocation
Swift Protocol Get Only Settable
iOS - Integrating Credit Card Payments
How to Create Static Library and Can Add Just .A File on Any Project in iOS
Converting Nsdictionary Object to Nsdata Object and Vice-Versa
Custom Url to Launch Facebook Messenger on iOS
How to Directly Rotate Cvimagebuffer Image in iOS 4 Without Converting to Uiimage
Using Cabasicanimation to Rotate a Uiimageview More Than Once