Have you ever been working on a really large product?
And that's... That's a pain in the ass...
Really, more is needed to code the project, you are also coding how to build the project. These scripts tend to grow in size to the extent that it does not look like a simple collection of source file path sets but rather a framework with its own rules.
If the application is cross-platform things become much worse: rules are complicated, workarounds increase drastically, and eventually, your build system becomes a risky component of the whole product itself.
If you do not understand the pain, just check out an Unreal Engine build system which is a custom build system written in C#, or check an O3DE cmake build scripts, or, for example, Mozilla Build System (I like how they start writing in their documentation: "The Mozilla build system is a complicated collection of scripts and tools..". This sentence sounds to me like a red flag itself, that something must be done. Non-surprising that Rust programming language was born inside the Mozilla company.
Currently, I'm working on a Shatterline game project, and it is comparable in its complexity to those examples, so, yes, while these examples are extreme, they appear more often in real life. We use WAF for building both engine and the game written in C++, and, yeah..., it hurts.☝ The problem here is covered in the fact that the project structure is not related to the programming design in any way.
And this thought is IMHO the core of all evil.
What Build System MUST do Except Delivering Extra Complexity To My Everyday Job?
So what are the responsibilities of building systems, or, in other words, what are the stages of making a build? Here they are (one may discuss how much the list is comprehensive and/or granular, but I see these four as conceptually prevailing ones):- Compilation and Linking.
Compiling single modules/units, building single modules/compilation units into a whole component (libraries, executables). - Component-level dependency management.
Providing your code base with third-party components (like Qt, boost, Spring, etc..) - Build variants.
Providing different kinds of builds: for different platforms, debugging/release, etc. - Post-building facilities.
Deployment, Installation, etc, are often very customized with outer supporting scripts.
Well, the problem is actually partly solved
The designers of modern programming languages are much more careful with the fact that project structuring and source code management must not be such a complex task. They tend to put the logic of source code layout into the compiler design producing such beloved things as Rust and Nim.
These languages employ modularity with a notion of the module rather than compilation unit, the one you pass when invoking the compiler, like:
$ rustc main.rs
Even if your program has more modules, all of them are automatically resolved, and you don't have to pass them all like it is done in C++.
That's how the first two responsibilities are handled. That is, without any build system.
☝ The problem solved here is about not manually managing lists of thousands of source code files, especially if some components share sources and headers. That's the hell.
Let's go deeper talking about module package dependencies management. This is usually done with accompanying tools like Cargo in Rust, or Nimble in Nim. These are not parts of the language itself but are shipped along with the compiler, and allow to specify and download code dependencies.
But employing a module-based approach solves only the first two responsibilities, and the third one is as harsh as the previous ones.
Build Variants. The Tricky One
Why is that? Simple C++:
#ifdef WIN32
// Do windows stuff
#elif _linux_
// Do Linux stuff
#elif __macos__
// Do Mac OS stuff
#endif
Not that simple? Quit ugly stuff, I know. But it is usually even worse in C++:
#ifdef WIN32
// Do windows stuff
#if defined(NDEBUG)
// Do Release Stuff
#endif
#elif _linux_
// Do Linux stuff
#if defined(clang)
// Something that does not work like in GCC
#endif
#elif __macos__
// Do Mac OS stuff
#endif
const Builder = @import("std").build.Builder;
pub fn build(b: *Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("init-exe", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
The Power of Metaprogramming
when defined(windows):
// Do windows stuff
when (debug):
// Print something
elif defined(linux):
// Do linux stuff
elif defined (macos):
// Do macos staff
Comments
Post a Comment
Let's discuss the post