自定义 Cordova 插件:将框架添加到“嵌入式二进制文件”中

16
在自定义的 Cordova 插件中,如何在 plugin.xml 中配置一个特定的 .framework 文件,以便将其添加到 Xcode 的“嵌入式二进制文件”部分?如果目前无法直接在 plugin.xml 中实现此操作,我愿意接受替代建议。

为什么必须使用嵌入式二进制文件而不是链接的框架和库?您能告诉我您正在尝试使用哪个框架吗? - jcesarmobile
这是一个定制的框架,没有源代码,也不是公共框架。它是由第三方提供给我们公司的,必须嵌入而不是链接,否则在启动时会出现运行时异常“图像未找到”。 - Alon Amir
5个回答

34

我已经实现了一种解决方案,直到 Cordova 的 plugin.xml 支持它。希望将来在这样的条目中添加一个 embed 属性将具有相同的效果:<framework embed="true" src="..." />。但目前,该属性没有帮助,因此需要以下解决方法。

以下方案适用于 Cordova 版本 5.3.3。

首先,请确保在 plugin.xml 中添加框架条目:

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

embed="true"目前无法使用,但仍需添加。

我们将创建一个钩子,在您的plugin.xml文件中声明:

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

接下来,在我们的钩子代码中需要使用一个特定的节点模块,这个模块是node-xcode

安装 node-xcode(必须是0.8.7版本或更高版本):

npm i xcode

最后,挂钩本身的代码 -

addEmbedded.js 文件:

'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);
};

这个钩子实际上是做什么的:

  1. 创建一个名为您插件 ID 的“Build Phase”,配置为“复制文件”,将复制的目标设置为“Frameworks”。
  2. 查找并将您的 .framework 文件添加到上述 Build Phase 中,依次嵌入它。
  3. 设置 Xcode 构建属性 LD_RUNPATH_SEARCH_PATHS,也要在 "@executable_path/Frameworks" 中查找嵌入式框架(该嵌入式框架将在“复制文件” -> “Frameworks” Build Phase 后复制到其中)。
  4. 通过设置 "CodeSignOnCopy" 和 "RemoveHeadersOnCopy" 为您的 .framework 文件,来配置 ATTRIBUTES 键。
  5. 从 FrameworksBuildPhase 中删除您的 .framework 文件,并以新的分离 PBXBuildFiles 方式重新添加它们到 FrameworksBuildPhase 中 (相同的 PBXFileReference),必须这样做才能使 "CodeSignOnCopy" 有意义, 如果没有删除它,如果使用 Xcode 打开项目,您将找不到指示其将进行签名的构建阶段中的选中标记。

更新1:钩子代码修改:

  1. 钩子会自动查找您的 .framework 文件,无需编辑钩子。
  2. 增加了一个重要的修改,为您的 .framework 文件设置 "CodeSignOnCopy" 和 "RemoveHeadersOnCopy" 属性。
  3. 改进了钩子,使其能够在多个插件使用此钩子的情况下工作。

更新2:

  1. 由于我的pull request已被接受,因此不再需要安装我自己的分支。
  2. 改进了钩子代码。

更新3 (19/09/2016)

根据 Max Whaler 的建议修改了钩子脚本,因为我在 Xcode 8 上遇到了相同的问题。

最后注意:

一旦您将应用上传到 AppStore,如果由于不受支持的架构(i386 等)而导致验证失败,请尝试使用以下 Cordova 插件(仅有钩子,没有本机代码):zcordova-plugin-archtrim


这个命令 - npm i xcode - 可以成功安装xcode模块。 - arjunattam
1
谢谢你的回答!但我有另一个问题:为了使其工作,我必须先添加插件,然后再添加iOS平台。框架仅在这些步骤中被复制到嵌入式系统。如果iOS平台已经安装,是否有什么我可以做的来让插件将框架复制到嵌入式系统? - Macaret
@jdixon04 谢谢,知道了!关于你的问题,确保在从框架导入头文件时使用#import语句与<>(而不是" )。 - Alon Amir
@AlonAmir 这不是我的插件,但是看源代码,是通过 #import <...> 导入的框架(https://github.com/Justin-Credible/cordova-plugin-braintree/blob/master/src/ios/BraintreePlugin.m)。我对 XCode 不是很熟悉,但是在“Frameworks”文件夹下确实可以看到与 Braintree 相关的框架。我不确定在查看“Symbol Navigator”时是否应该看到它们,但它们在那里没有显示出来。再次感谢。 - jdixon04
在添加嵌入式框架并启用“复制时代码签名”后,您是否仍能成功使用“cordova run ios --device”进行部署?我还不是很确定,但我认为现在遇到了代码签名问题,因为部署的应用程序立即崩溃。直接从XCode部署非常好。 - Shoerob
显示剩余3条评论

11

如果要将库添加到Xcode的“嵌入式二进制文件”部分(从cordova-ios 4.4.0和cordova 7.0.0开始),请在你的 plugin.xml 中放置以下代码:

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

如果想要将库添加到Xcode的“链接框架和库”部分,请在您的plugin.xml中添加以下内容:

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

它们两者可以同时存在。例如:

<!-- 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
在我按照您的指示操作后,“嵌入式二进制文件”和“链接的框架和库”部分都已添加该框架。但是在“链接的框架和库”部分,该框架似乎看起来像是禁用状态。即使在Framework组(在xcode的项目导航器下),该框架也呈现为红色(未链接状态)。请帮助我解决这个问题。非常感谢。 - GJDK
@GJDK 你尝试过在<framework>标签中去掉"embed="true""吗? - Joanne

7
为了让我的插件能够在XCode 8.0和cordova-ios 4.2的项目上构建,我不得不在after_build阶段运行钩子。此外,请确保node环境使用最新版本的xcode-node (^0.8.9),否则将在拷贝文件阶段中出现错误。
插件.xml需要使用custom="true",这样Cordova才能复制框架文件,但这会与当钩子在after_platform add或even after_prepare运行时对.pbxproj所做的更改产生冲突。

4

不准确的是针对嵌入式二进制文件的自定义iOS框架部分,详见https://issues.apache.org/jira/browse/CB-11233的持续讨论,即使在上个月(2017年4月)也是如此。请注意,它被标记为“cordova-7.0.0”。 - Alon Amir
嘿,我在想要加上“但是这对我还没有起作用。”;-) - grantpatterson
现在7.0.0版本已经发布,可以进行恢复删除操作。 - grantpatterson

1

@Alon Amir,谢谢分享,它运行得很好!虽然我的应用在Debug模式下运行完美,但在Release模式下却没有。我发现LD_RUNPATH_SEARCH_PATHS仅在Debug模式下添加,因为proj.getBuildProperty没有构建参数,所以它只获取第一个结果。我稍微修改了你的代码,使其可以在Debug模式和Release模式下工作:

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");

谢谢!很高兴知道它对你有帮助。尽管如果你使用updateBuildProperty或者addBuildProperty没有第三个参数(就像我的钩子一样),它应该已经应用于所有构建设置,这是从node-xcode库中的条件(build是第三个参数)if ( (build && config.name === build) || (!build) ) 。如果你的项目在“Release”中构建失败,请确保嵌入的框架支持“bitcode”,或在你自己的cordova项目中禁用“bitcode”(有插件+钩子可以做到)。 - Alon Amir
问题不在于updateBuildProperty或addBuildProperty,而是在于getBuildProperty:它首先进入Debug条目,在那里由于某种原因已经设置了LD_RUNPATH_SEARCHPATHS,所以“target”被设置为该值。之后,它到达Release条目。由于没有LD_RUNPATH_SEARCHPATHS条目,“target”将不会被更改!因此,addBuildProperty随后将不会被调用。我希望我能够稍微澄清一下我的情况(并且正确地做到了)。虽然位代码标志似乎不是一个问题,但是一旦我正确设置了搜索路径,它就可以完美运行。 - Max Wahler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接