Search code examples
gitmakefile

How to declare dependencies from different git commits in a Makefile?


I am trying automate the bootstrapping of a compiler, from version n to version n+1. The problem is that the compiler is (partly) written in the language it compiles, so not only it is itself a dependency for building itself, but also modifying the source code of the compiler modifies both the syntax of the language (and hence the syntax of that source code) and the semantics of the produced code. Both could change when passing from version n to n+1.

I have looked at several possible solutions for this problem (mainly, how other compilers do it), and I have adopted the following scheme:

  • assume a binary of compiler version n is available (a binary of version 0 has been produced "by hand", and binaries have been rolled since then);
  • write a version n.5 of the compiler, which uses the syntax and semantics of the language defined by compiler version n, but itself parses syntax of language version n+1, and uses semantics of version n+1;
  • compile it using compiler version n to get a compiler version n.5;
  • write a version n+1 of the compiler, which uses the syntax and the semantics it defines itself;
  • use compiler version n.5 to compiler version n+1.

I now have a binary of compiler version n+1, and the source code is written in the language version n+1. I can now store this somewhere to be used to build version n+2, and so on.

Ideally, this could be done in a single pass:

  • compiler: version n; source code: version n.
  • I write compiler version n.5 -> compiler: version n; source code: version n.5.
  • I build the project with the available compiler -> compiler: version n.5; source code: version n.5.
  • I write compiler version n+1 -> compiler: version n.5; source code: version n+1.
  • I build the project with the available compiler -> compiler: version n+1; source code: version n+1.

But in reality, one might (silently) introduce bugs in the version n.5 of the source code, making the compiler version n.5 bugged. Then, we trying to compile version n+1, it fails, because the compiler is bugged (or worse, it doesn't fail, but introduces a bug in compiler version n+1 which makes it diverge from the source code version n+1, and so forth up to version n+1000 when you realize there is a bug...). This means that all the versions of the source code should be available independently, so that one can back up, patch the bug in version n.5, then rebuild everything.

To do so, while developing version n+1 from version n, assuming I'm standing at a commit of the compiler repo which corresponds to version n, I create two branches: one for version n.5, and one for version n+1. I write the code for both versions of the compiler, then build the project as mentioned before. This allows each branch to evolve as I need more features or patch more bugs, and once it is finished, I can merge everything to get version n+1.

My problem is: how to get make to understand that some dependencies are in an other commit than the current commit? In particular, the compiler version n+1 has, as dependency, compiler-vn.5, whose source code is in an other branch. Is this even possible?


Solution

  • make deals with dependencies that are files on the filesystem. That's it. It doesn't know about anything else. So if you can transform your branch requirements into files on the filesystem and represent their "out of date" / "up to date" qualities as timestamps on these files, make can help you with it. If not, it can't.

    For example, if you keep the SHA of the branch as of the time you last built it in a file, then you can have a rule with a recipe that checks to see if that is the current SHA of the head of the branch. if it is, the recipe does nothing (doesn't change the file--note it's not good enough to just write the same SHA into the file! That still changes the timestamp. You have to avoid touching the file at all) and the file's timestamp does not change and nothing is out of date. If the SHA is different you update the contents of the file with the new SHA, now the file is new and anything that depends on that file will be rebuilt.

    Or something like that.

    ETA

    I don't have time to write a completely working example, and StackOverflow is a place to ask specific questions about specific technical problems, not a place to get a completely working example from scratch. But this can get you started: this will get the SHA of the given branch into the variable my-branch_SHA:

    my-branch_SHA := $(shell git rev-parse my-branch)
    

    now you can write a rule like this:

    my-branch_FILE: FORCE
            test '$(my_branch_SHA)' = "$$(cat $@)" \
                || echo '$(my_branch_SHA)' > $@
    
    FORCE:;
    

    The FORCE trick is documented in the GNU Make manual; it basically gets make to always run that rule to check whether the file is out of date. But the recipe says "if the values are different, update the file" (which will change its timestamp); if the values are the same nothing happens so the timestamp is not updated.

    Now if you add my-branch_FILE as a prerequisite to some other target, that target will be considered out of date (and so its recipe will be run) if the branch SHA is different.

    If you have lots of these branches you might want to use various other tricks to avoid code duplication. You can probably get some useful ideas in this series of blog posts: https://make.mad-scientist.net/category/metaprogramming/ Definitely prefer the simplest ones that get the job done: everyone jumps immediately for eval but it is very tricky to use; the other options are much simpler.

    If you run into problems ask a new question showing your example code and asking specifically about the problem you have.