【问题标题】:Custom Cordova Plugin: Add framework to "Embedded Binaries"自定义 Cordova 插件:将框架添加到“嵌入式二进制文件”
【发布时间】:2023-03-11 17:05:02
【问题描述】:

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

【问题讨论】:

  • 为什么必须在嵌入式二进制文件中,而不是在链接的框架和库中?你能说出你想使用哪个框架吗?
  • 它是一个自定义框架,没有它的源代码,不是公共框架,它是由第三方提供给我们公司的,它必须嵌入并且不能链接,否则我会遇到运行时异常启动时“找不到图片”。

标签: ios xcode cordova cordova-plugins


【解决方案1】:

要将库添加到 Xcode 中的“嵌入式二进制文件”部分(从 cordova-ios 4.4.0 和 cordova 7.0.0 开始),请将其放入您的 plugin.xml:

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

要在 Xcode 的“Linked Frameworks and Libraries”部分添加库, 把它放在你的 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>

【讨论】:

  • 按照您说的做之后,该框架已添加到“嵌入二进制文件”和“链接框架和库”部分。但在 Linked Frameworks & Libraries 部分,该框架看起来像是一个禁用的框架。即使在框架组中(在 xcode 的项目导航器下),框架似乎也处于红色(未链接状态)。请帮助我解决这个问题。非常感谢。
  • @GJDK 你试过在 标签中去掉“embed="true"”吗?
【解决方案2】:

embed="true" 从今天发布的cordova-ios 4.4.0 和cordova 7.0.0 开始支持。 https://cordova.apache.org/docs/en/latest/plugin_ref/spec.html#framework https://issues.apache.org/jira/browse/CB-11233

【讨论】:

  • 对于嵌入式二进制文件的自定义 iOS 框架部分不准确,请参阅上个月(2017 年 4 月)对issues.apache.org/jira/browse/CB-11233 的继续讨论。请注意,它被标记为“cordova-7.0.0”。
  • 嘿,对,我正在考虑添加,“但这对我来说还没有用。” ;-)
  • 7.0.0 发布后取消删除。
【解决方案3】:

在 Cordova 的 plugin.xml 支持之前,我已经实施了一种解决方法,希望将来,一旦此类条目中的 embed 属性将具有相同的效果:&lt;framework embed="true" src="..." /&gt;,目前,此属性没有帮助,因此有以下解决方法。

以下解决方案使用 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 命名的“构建阶段”,配置为“复制文件”,该副本的目标是“框架”。
  2. 查找您的 .framework 文件并将其添加到上述构建阶段,然后将其嵌入。
  3. 设置一个名为 LD_RUNPATH_SEARCH_PATHS 的 Xcode 构建属性以在 "@executable_path/Frameworks" 中查找嵌入式框架(这是嵌入式框架将在“复制文件”->“框架”构建阶段之后被复制到的位置
  4. 通过为您的 .framework 文件设置“CodeSignOnCopy”和“RemoveHeadersOnCopy”来配置 ATTRIBUTES 键。
  5. 从 FrameworksBuildPhase 中删除您的 .framework 文件并将它们重新添加到 FrameworksBuildPhase 作为新的分离的 PBXBuildFiles(相同的 PBXFileReference),必须这样做才能使“CodeSignOnCopy”具有任何含义,而不是删除它,如果您用 Xcode 打开项目,在构建阶段你不会发现它会签名的复选标记。

更新1:钩子代码,修改:

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

更新 2

  1. 由于我的pull request 已被接受,我不再需要安装我自己的 fork。
  2. 改进的挂钩代码。

更新 3 (19/09/2016)

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

最后说明

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

【讨论】:

  • 这 - npm i xcode - 用于安装 xcode 模块。
  • 感谢您的回答!但我还有另一个问题;为了让它工作,我必须先添加插件,然后再添加 iOS 平台。仅在这些步骤中将框架复制到嵌入。如果已经安装了 iOS 平台,我能做些什么让插件将框架复制到嵌入式?
  • @Macarat 如果您不使用 CLI 工作流程,那么您不需要任何这些,您只需打开 Xcode,并将您的框架文件添加到嵌入式二进制文件部分,与 Cordova 无关。
  • @AlonAmir 感谢您提供出色的解决方案!这种技术正在cordova-plugin-braintree 插件中使用,虽然看起来框架正在嵌入,但在构建时,它告诉我找不到标头。有什么建议?谢谢!
  • @jdixon04 谢谢,很高兴知道!关于您的问题,请确保在从框架中导入标头时使用带有 &lt;&gt;(而不是 ")的 #import 语句。
【解决方案4】:

要让我的插件在 XCode 8.0 和 cordova-ios 4.2 上构建项目,我必须在 after_build 阶段运行挂钩。另外,请确保 node 环境使用的是最新版本的 xcode-node (^0.8.9),否则您将在复制文件阶段遇到错误。

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

plugin.xml 需要custom="true" 以便 Cordova 复制框架文件,当此挂钩在 after_platform add 甚至 after_prepare 中运行时,最终与对 .pbxproj 所做的更改发生冲突。

【讨论】:

    【解决方案5】:

    @Alon Amir,感谢分享,效果很好!虽然,我的应用程序在调试模式下运行完美,但在发布模式下却没有。我发现 LD_RUNPATH_SEARCH_PATHS 仅添加到调试模式,因为没有构建参数的 proj.getBuildProperty 采用第一个结果。我稍微修改了您的代码,使其在调试和发布模式下都可以工作:

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

    【讨论】:

    • 谢谢!很高兴知道它对你有帮助。虽然如果你使用 updateBuildPropertyaddBuildProperty 没有第三个参数(如我的钩子),它应该已应用于所有构建设置,这是来自 node-xcode lib 的条件(build 是第三个参数) if ( (build &amp;&amp; config.name === build) || (!build) )。如果您的项目无法在“Release”中构建,请尝试确保您嵌入的框架支持“bitcode”,或者在您自己的cordova项目中禁用“bitcode”(有一个插件+挂钩)。
    • 问题不在于 updateBuildProperty 或 addBuildProperty,而在于 getBuildProperty:它首先出现在 Debug 条目,由于某种原因已经设置了 LD_RUNPATH_SEARCHPATHS,因此“target”设置为该值。之后,它到达 Release 条目。由于没有 LD_RUNPATH_SEARCHPATHS 条目,因此不会更改“目标”!因此, addBuildProperty 之后将不会被调用。我希望我能够稍微澄清一下我的情况(并且做得正确)。位码标志似乎不是问题,因为一旦我正确设置了搜索路径,它就会完美运行。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-06-20
    • 2019-04-19
    • 1970-01-01
    • 1970-01-01
    • 2016-09-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多