Modern Programming: Object Oriented Programming and Best Practices
上QQ阅读APP看书,第一时间看更新

Build Management

I wrote in the previous section that a benefit of adopting CI is that it forces you to simplify the building of your project (by which I mean compiling sources, translating assets, creating packages, and anything else that takes the inputs created by the project team and converts them into a product that will be used by customers). Indeed, to use CI you will have to condense the build down until an automated process can complete it given any revision of your source code.

There's no need to write a script or an other program to do this work, because plenty of build management tools already exist. At a high level, they all do the same thing: they take a collection of input files, a collection of output files, and some information about the transformations needed to get from one to the other. How they do that, of course, varies from product to product.

Convention or Configuration

Some build systems, like make and ant, need the developer to tell them nearly everything about a project before they can do anything. As an example, while make has an implicit rule for converting C source files into object files, it won't actually execute that rule until you tell it that you need the object file for something.

Conversely, other tools (including Maven) make certain assumptions about a project. Maven assumes that every .java file in a folder called src/main/java must be compiled into a class that will be part of the product.

The configuration approach has the advantage that it's discoverable even to someone who knows little about the system. Someone armed with a collection of source files, grep, and a little patience could work out from a Makefile or Xcode project which files were built as which targets, and how. Because there's a full (or near full) specification of how everything's built, you can find what you need to change to make it act differently, too.

The downside to that discoverability is that you have to specify all that stuff. You can't just tell Xcode that any .m file in a folder called Classes should be passed to the Objective-C compiler; you have to give it a big list of all of them. Add a new file, and you must change the list.

With a convention-based build system, this situation is exactly reversed. If you follow the conventions, everything's automatic. However, if you don't know the conventions, they can be hard to discover. I once had a situation on a Rails project where the folder that static resources (such as images) were saved in changed between two releases. On launching the app, none of my images were being used and it wasn't clear why. Of course, for someone who does know the conventions, there's no learning curve associated with transferring between different projects.

On balance, I'd prefer a convention-led approach, provided the conventions are well-documented somewhere so it's easy to find out what's going on and how to override it if you need to. The benefit of reduced effort and increased consistency, for me, outweighs the occasional surprise that's encountered.

Build Systems That Generate Other Build Systems

Some build procedures get so complicated that they spawn another build system that configures the build environment for the target system before building. An archetypal example is GNU Autotools, – which actually has a three-level build system. Typically, developers will run autoconf, a tool that examines a project to find out what questions the subsequent step should ask and generates a script called configure. The user downloads the source package and runs configure, which inspects the compilation environment and uses a collection of macros to create a Makefile. The Makefile can then compile the source code to (finally!) create the product.

As argued by Poul-Henning Kamphttp://queue.acm.org/detail.cfm?id=2349257), this is a bad architecture that adds layers of cruft to work around code that has not been written to be portable to the environments it will be used in. Software written to be built with tools like these is hard to read, because you must read multiple languages just to understand how one line of code works.

Consider a bug reported in a particular C function in your project. You open that function to find two different implementations, chosen by a #ifdef/#else/#endif preprocessor block. You search for the macro used by that block and find it in config.h, so you must read the configure script to find out how it's set. To discover whether that test is doing the right thing, you need to look at the configure.ac file to find out how the test is generated.

About the only justification for using such a convoluted process is that it's thought of as conventional and expected by your target users, but even then, I'd question whether that expectation is driven by a technical need or by Stockholm syndromehttp://en.wikipedia.org/wiki/Stockholm_syndrome. If your product doesn't need to be portable, then there's no need to add all that complexity – and even if it does, there may be better ways to solve the problem that'll work for your product. One obvious approach is to target a portable platform such as Mono or Python.