I am soon starting to maintain a line of products containing variants of the same embedded software. Since I\'ve been playing with git for one year and appreciate it very much,
I'm facing the same issue. I want to share source not just among targets, but across platforms (e.g Mono vs. Visual studio. It seems like there isn't any wway to easily do that with just the version the branching/tagging provide by Git. Because ideally, you'd like to maintain a common code branch, and host/target specific branches and just merge the common code in and out of those branches. But I don't know how to do that. Maybe a combination of branching and tagging? Anybody?
Obviously, I would use condition compilation if it was needed where I could, so that there would only be one source file under version control, but for the other stuff files that should only appear in one host/target branch, or where the contents would be completely different, a different mechanism is needed. If you can use OO techniques to side step the problem that too would be desirable.
So the answer seems to be you need some sort of configuration/build management mechanism on top of git to manage this. But that would seem to add a lot of complexity, so maybe cherry picking merges into a common branch wouldn't be a bad way to go. Especially if you use other techniques to keep the source variants to a minimum. (Could tagging help automate this?).
You are in for a world of hurt!
Whatever you do, you need an automatic build environment. At the very least you need some automatic way of building all the different versions of your firmware. I've had issues of fixing a bug in one version and breaking the build of a different version.
Ideally you would be able to load the different targets and run some smoke tests.
If you go the #define route, I would put the following someplace where the variant is checked:
#else
#error You MUST specify a variant!
#endif
This will make sure all the files are built for the same variant during the build process.
I'd try to go for #define
s as much as possible. With a proper code you can minimize the impact on readability and repetitions.
But at the same time #define
approach can safely be combined with splitting and branching, application of which depends on the nature of the codebase.
You should strive to, as much as possible, keep each variant's custom code in its own set of files. Then your build system (Makefile or whatever) selects which sources to use based on which variant you are building.
The advantage to this is that when working on a particular variant, you see all of its code together, without other variants' code in there to confuse things. Readability is also much better than littering the source with #ifdef, #elif, #endif, etc.
Branches work best when you know that in the future you will want to merge all of the code from the branch into the master branch (or other branches). It doesn't work as well for only merging some changes from branch to branch (although it can certainly be done). So keeping separate branches for each variant will probably not yield good results.
If you use the aforementioned approach, you don't need to try to use such tricks in your version control to support your code's organization.
I am not sure if that's "best practice", but the Scintilla project uses something for years that is still quite manageable. It has only one branch common to all platforms (mostly Windows/GTK+/Mac, but with variants for VMS, Fox, etc.).
Somehow, it uses the first option which is quite common: it uses defines to manage little platform specific portions inside the sources, where it is impractical or impossible to put common code.
Note that this option is not possible with some languages (eg. Java).
But the main mechanism for portability is using OO: it abstracts some operations (drawing, showing context menu, etc.) and uses a Platform file (or several) per target, providing the concrete implementation.
The makefile compiles only the proper file(s) and uses linking to get the proper code.
I think that the appropriate answer depends in part on how radically different the variants are.
If there are small portions that are different, using conditional compilation on a single source file is reasonable. If the variant implementations are only consistent at the call interface, then it may be better to use separate files. You can include radically variant implementations in a single file with conditional compilation; how messy that is depends on the volume of variant code. If it is, say, four variants of about 100 lines each, maybe one file is OK. If it is four variants of 100, 300, 500 and 900 lines, then one file is probably a bad idea.
You don't necessarily need the variants on separate branches; indeed, you should only use branches when necessary (but do use them when they are necessary!). You can have the four files, say, all on a common branch, always visible. You can arrange for the compilation to pick up the correct variant. One possibility (there are many others) is compiling a single source file that knows which source variant to include given the current compilation environment:
#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