【问题标题】:Keeping track of source code variants跟踪源代码变体
【发布时间】:2010-11-30 01:32:01
【问题描述】:

我很快就开始维护一系列包含相同嵌入式软件变体的产品。由于我玩了 git 一年了,非常欣赏它,我很可能会用它来做源码控制。

我可以看到几个用于维护固件变体的选项,但没有一个让我很满意。您为自己的工作应用了哪些最佳实践?

我能想到的替代方案:

  • 定义。预处理。 优点:一切都始终存在于源代码中,很难错过其中一种产品的更新。 缺点:更难阅读。当我们只有两个变体时可能没问题,当它变成四个或更多时会很痛苦。此外,应用 DRY 原则(不要重复自己)似乎更难。

  • 每个产品变体一个分支。 当包含适用于所有产品的更改时,必须将更改合并到其他产品。 缺点:如果提交既包含所有产品的更改,又包含特定变体的更改,则会有麻烦。当然,您可以确保提交只包含一种更改:this-product-changes 或 the-whole-family-changes。但是尝试将其强加给团队?再加上合并是行不通的,我们应该挑选樱桃。对吧?

  • 作为子模块的核心存储库。 使所有包含核心功能的文件都成为自己的存储库。 所有产品都包含一个版本的核心存储库作为子模块。 缺点:我看不出最终不会有核心子模块的变体。然后我们又遇到麻烦了,然后我们又会使用定义或其他不好的东西。 带有分支的核心存储库?然后我们回到之前的替代方案:必须合并适用于所有分支的更改,但合并还包括特定于产品的内容。

  • 创建每个模块的存储库。 例如,显示驱动程序的存储库,电源管理硬件的另一个存储库,用户输入界面的另一个存储库,...... 优点:良好的模块化。只需将您需要的模块作为子模块来制作新产品!所有子模块都可能有分支,例如,如果一个变体以不同的方式使用硬件。 缺点:很多很多模块,每个模块都跟踪几个文件(一个包含文件和一个源文件)。一个麻烦。 有人在某些模块中进行了重要更新?然后,如果合适的话,有人需要在这个模块的其他分支中包含更改。然后有人还必须更新每个产品存储库中的子模块。 相当多的工作,我们有点失去了 git 的快照方面。

你是怎么做的,它是如何工作的?或者你会怎么做?

我觉得我应该体验一下樱桃采摘。

【问题讨论】:

  • 每个产品解决方案只有一个分支,您始终可以在 swperate 分支上进行开发,然后将该分支合并到变体(每个产品)分支中。

标签: git embedded workflow branch


【解决方案1】:

我也面临同样的问题。我想不仅在目标之间共享源代码,而且还想跨平台共享源代码(例如 Mono 与 Visual Studio。似乎只有 Git 提供的分支/标记版本没有任何方法可以轻松做到这一点。因为理想情况下,你'想要维护一个公共代码分支,并托管/定位特定分支,然后将公共代码合并进出这些分支。 但我不知道该怎么做。也许是分支和标记的组合?有人吗?

显然,如果需要,我会在可能的情况下使用条件编译,这样只有一个源文件受版本控制,但其他东西文件应该只出现在一个主机/目标分支中,或者内容会完全不同,需要不同的机制。如果您可以使用 OO 技术来回避问题,那也是可取的。

所以答案似乎是您需要某种配置/构建管理机制 git 来管理这个。但这似乎会增加很多复杂性,所以也许樱桃采摘 合并到一个公共分支不会是一个坏方法。特别是如果您使用其他技术将源变量保持在最低限度。 (标记可以帮助自动执行此操作吗?)。

【讨论】:

    【解决方案2】:

    你正处于一个受伤的世界!

    无论您做什么,都需要一个自动构建环境。至少您需要某种自动方式来构建所有不同版本的固件。我遇到了修复一个版本中的错误并破坏不同版本的构建的问题。

    理想情况下,您应该能够加载不同的目标并运行一些冒烟测试。

    如果你走 #define 路线,我会将以下内容放在检查变体的地方:

    #else 
        #error You MUST specify a variant!
    #endif
    

    这将确保在构建过程中为相同的变体构建所有文件。

    【讨论】:

    • 当然,#error。甚至,如果定义了多个变体定义,则除一个外都未定义(或发送另一个#error)。
    【解决方案3】:

    您应该尽可能将每个变体的自定义代码保存在自己的文件集中。然后您的构建系统(Makefile 或其他)根据您正在构建的变体选择要使用的源。

    这样做的好处是,在处理特定变体时,您可以将其所有代码放在一起,而没有其他变体的代码来混淆事物。可读性也比在源代码中乱扔#ifdef、#elif、#endif 等要好得多。

    当您知道将来您希望将分支中的所有代码合并到主分支(或其他分支)时,分支的工作效果最好。仅合并从一个分支到另一个分支的 some 更改并不适用(尽管它当然可以完成)。因此,为每个变体保留单独的分支可能不会产生好的结果。

    如果您使用上述方法,则无需尝试在版本控制中使用此类技巧来支持您的代码组织。

    【讨论】:

    • +1 仅在计划再次合并时提供有关分支的建议(尽管有例外)。还要考虑使用 VCS 技巧来支持代码的风险。请参阅我对 PhiLho 关于 DRY 的评论以及此类自定义代码文件。
    • 在处理特定变体时,我经常想查看其他变体的代码。这对代码折叠很有用..
    【解决方案4】:

    我认为适当的答案部分取决于变体的根本差异。

    如果有小部分不同,对单个源文件使用条件编译是合理的。如果变体实现仅在调用接口处保持一致,那么使用单独的文件可能会更好。您可以通过条件编译在单个文件中包含根本变体的实现;这有多混乱取决于变体代码的数量。例如,如果是四个变体,每个变体大约 100 行,那么可能一个文件就可以了。如果是 100、300、500 和 900 行的四种变体,那么一个文件可能是个坏主意。

    您不一定需要不同分支上的变体;实际上,您应该只在必要时使用分支(但在必要时使用它们!)。例如,您可以将四个文件都放在一个公共分支上,并且始终可见。您可以安排编译以选择正确的变体。一种可能性(还有很多其他可能性)是编译单个源文件,该文件知道在给定当前编译环境的情况下要包含哪个源变量:

    #include "config.h"
    #if defined(USE_VARIANT_A)
    #include "variant_a.c"
    #elif defined(USE_VARIANT_B)
    #include "variant_b.c"
    #else
    #include "basecase.c"
    #endif
    

    【讨论】:

    • 我更愿意在 makefile 中执行此操作,我不喜欢在 #include 中包含 c 文件:它们不会出现在我的 makefile 中。我同意没有灵丹妙药,解决方案取决于变体的差异:) 我害怕的是从#defines 开始,然后看到产品系列增长到 10 个变体。
    • 如果您正在生成 makefile(而不是使用版本控制的 makefile),那么直接在 makefile 中列出正确的文件。当 makefile 不是动态生成时,这种方法可以正常工作(不一定推荐,但确实有效)。我的基本信息是“一个尺寸并不适合所有人”,不同的解决方案是相关的,具体取决于细节。如果您认为您可能有 10 个变体项目,那么设计您的系统以使用尽可能多的变体。您不会说是 10 个二元决策,还是 10 个备选方案中的 1 个,或者其他什么。
    • 到目前为止,我的意思是 10 个备选方案中的 1 个。感谢上帝 :) 我不明白为什么拥有版本控制的 makefile(每个变体一个)会使这种方法无效?
    • 这取决于你如何构建你的makefile。如果您根据需要的变体构建十个不同目标中的一个——并且每个变体都有自己构建它所需的文件列表——那么你就可以了。这还取决于您是总是要在同一个构建中构建所有十个变体,还是一次只构建一个。我在一个系统上工作,其中在 6 个或 7 个不同的平台上使用相同的 makefile(不费心计算 32 位与 64 位的差异),但在任何时候,它只为单个平台构建,并且每个平台都使用相同的目标...
    • ..在那里,让make 找出与每个平台相关的文件集是一件令人讨厌的事情——因此在某些地方使用了包含技术。我们还有一个使用#ifdef 的系统。通常,#ifdef 场景比#include 场景多得多。因此,正如他们所说,您的里程会有所不同。 [而且 cmets 的标记编辑器在 C 预处理器周围的反引号处理错误!擦它!!!要么那个,要么我连续两次犯同样的错误。]
    【解决方案5】:

    我不确定这是否是“最佳实践”,但 Scintilla 项目多年来使用的东西仍然非常易于管理。它只有一个所有平台通用的分支(主要是 Windows/GTK+/Mac,但也有 VMS、Fox 等的变体)。

    不知何故,它使用了第一个非常常见的选项:它使用定义来管理源代码中特定于平台的小部分,在这些部分中放置通用代码是不切实际或不可能的。
    请注意,此选项不适用于某些语言(例如 Java)。

    但可移植性的主要机制是使用 OO:它抽象了一些操作(绘图、显示上下文菜单等)并为每个目标使用一个(或多个)平台文件,提供具体实现。
    makefile 只编译正确的文件并使用链接来获取正确的代码。

    【讨论】:

    • OO 会很好,但这是汇编程序中的嵌入代码,甚至没有可用的 C 编译器。我知道无论如何都可以应用 OO 的概念。仅组装和链接必要的文件是一种选择,但我相信两个这样的文件(每个变体一个)会非常相似,因此会相互重复。
    • 拥有“兼容”文件,无论是一个,还是每个独特平台一个,我认为是个好主意。
    • Java 的解决方法是使用附加插件,例如 Antenna for Eclipse。不完美,但很有帮助。
    【解决方案6】:

    我会尽量选择#defines。使用适当的代码,您可以最大限度地减少对可读性和重复性的影响。

    但同时#define 方法可以安全地与拆分和分支相结合,其应用取决于代码库的性质。

    【讨论】:

    • 定义是最糟糕的解决方案。它们使您在编辑器中看到的源代码与编译器看到的源代码不同。
    • ifs 使您在编辑器中看到的源代码与执行的源代码不同。整个事情就叫做编程。
    • 高级语言的全部意义在于让人类更容易编程。必须在编译时在脑海中解释 IF 并在运行时解释 IF,这使得理解程序的功能变得更加困难和容易出错。
    • 是的,复杂性通常伴随着复杂性。编译时 ifs 和运行时 ifs 一样可能使您的代码难以阅读。是的,在使用之前必须三思而后行。
    猜你喜欢
    • 2011-12-05
    • 1970-01-01
    • 1970-01-01
    • 2013-10-02
    • 2020-10-25
    • 1970-01-01
    • 1970-01-01
    • 2018-05-20
    • 1970-01-01
    相关资源
    最近更新 更多