Cordova Custom Plugin: Add a Framework to the "Embedded Binary Files"

In a custom Cordova plugin, how can I customize a particular .framework file in the plugin.xml file so that it is added to the "Embedded Binaries" section in Xcode? If this is not currently possible directly in plugin.xml, I am open to alternative suggestions.

+11
ios xcode cordova cordova-plugins
source share
5 answers

I applied a workaround until it is supported by Cordova plugin.xml, I hope in the future, as soon as the embed property in such records will have the same effect: <framework embed="true" src="..." /> , at the moment this property does not help, therefore, the following workaround.

The following solution worked using Cordoba version 5.3.3.

First, be sure to add the frame entry in plugin.xml:

 <framework src="pointToYour/File.framework" embed="true" /> 

embed="true" does not work, but add it anyway.

We will create a hook, declare that in your plugin.xml:

 <hook type="after_platform_add" src="hooks/embedframework/addEmbedded.js" /> 

Next, there is a special node module that we will need in our hook code, this node-xcode module .

Install node -xcode (must be version 0.8.7 or higher):

 npm i xcode 

Finally, the hook code itself is

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 really does:

  • Creates a "build phase" named after your plugin ID configured to "Copy files", the purpose of this copy is "Frames".
  • Finds and adds your .framework files to the above build phase, in turn embedding it.
  • Sets the Xcode build property named LD_RUNPATH_SEARCH_PATHS to also search for built-in frameworks in "@executable_path/Frameworks" (This was the built-in infrastructure that will be copied after Copy Files → Frames Build Phase
  • Configures the ATTRIBUTES key by setting "CodeSignOnCopy" and "RemoveHeadersOnCopy" for your .framework files.
  • Removes your .framework files from FrameworksBuildPhase and re-adds them to FrameworksBuildPhase as new split PBXBuildFiles (Same PBXFileReference), this should be done so that "CodeSignOnCopy" means anything without deleting it if you open the project using Xcode, You will not find a checkmark in the build phase, which says that it will sign it.

Updated 1: hook code, changes:

  • The hook automatically finds your .framework files, there is no need to edit the hook.
  • An important modification has been added that sets the ATTRIBUTES "CodeSignOnCopy" and "RemoveHeadersOnCopy" for your .framework files.
  • Improved capture so that it works in the case when multiple plug-ins use this hook.

Update 2

  • Since my request has been accepted, you no longer need to install my own plug.
  • Improved hook code.

Update 3 (09/19/2016)

Changed script hook as suggested by Max Whaler, as I ran into the same issue compared to Xcode 8.

Final note

After you download the application in the AppStore, if the verification fails due to unsupported architectures (i386, etc.), try the following Cordova plugin (hook only, no native code): zcordova-plugin-archtrim

+28
source share

To get my build plugin with an Xcode 8.0 and cordova-ios 4.2 project, I had to run the hook in the after_build phase. Also, make sure that the node environment is using the latest version of xcode-node (^ 0.8.9) or you will get errors in the copy file phase.

<framework src="lib/myCustom.framework" custom="true" embed="true" /> <hook type="after_build" src="hooks/add_embedded.js" />

The .xml plugin needs custom="true" for Cordoba to copy the framework file, which ultimately contradicted the changes made to .pbxproj when this hook was launched in after_platform add or even after_prepare.

+7
source share

embed="true" supported by both cordova-ios 4.4.0 and cordova 7.0.0, which was released today. https://cordova.apache.org/docs/en/latest/plugin_ref/spec.html#framework https://issues.apache.org/jira/browse/CB-11233

+4
source share

To add libraries to the "Embedded Binaries" section in Xcode (Starting with cordova-ios 4.4.0 and cordova 7.0.0) put this in your plugin.xml file:

 <framework src="src/ios/XXX.framework" embed="true" custom="true" /> 

To add libraries to the Related Structures and Libraries section of Xcode, put this in your plugin.xml file:

 <source-file src="src/ios/XXX.framework" target-dir="lib" framework="true" /> 

Both of them can exist simultaneously. 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> 
+2
source share

@Alon Amir, thanks for sharing, it works beautifully! Although my application works fine in Debug, but not in Release mode. I realized that LD_RUNPATH_SEARCH_PATHS was only added to Debug mode, since proj.getBuildProperty without the build parameter takes the first result. I modified your code a bit to make it work in Debug, as well as in Release mode:

 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"); 
+1
source share

All Articles