What roles do continuous integration and binary caches play in software development? How does Nix facilitate their implementation?
What are continuous integration and binary caching?
Continuous integration (CI) is the automation of testing, debugging, and merging when a change is upstreamed to a development team's codebase. It prioritizes daily, small-scale code pushes over infrequent, major ones. In work environments distributed remotely across cities and continents rather than office floors, CI is especially useful for coordinating efforts and limiting the cost of testing a team's version-managed repositories. Configured correctly, it can enable nearly real-time updates across machines.
The automation begins after a developer requests to merge some work. Rather than have another developer test the code before it is incorporated, a CI system will run a suite of tests on the code and merge it after it passes these. Most CI systems will then build that code, and some will then deliver that build to a repository, though not in every case without human input. They provide general reporting on the process as well as build statistics.
A binary cache (BC) stores binary artifacts and their attendant metadata in a shareable location. There is a difference between a binary cache and a build cache. A build is a user-triggered collection of jobs that, accomplished, compiles a program. Binaries are the output artifacts of a build; they can be run without compiling. This distinction points to why binary caches have begun to widely complement build caches: Without a shared binary cache, a given build's transitive closure can be hard to complete across machines.
The transitive closure of a build is the map of its dependencies' requirements of other dependencies, their requirements, and so on. Misaligned needs (e.g., a necessary dependency's incompatibility within a different operating system) can result in build failures despite that same code compiling successfully on its local machine. A BC helps complete transitive closure across a network because dependencies a non-local machine can't compile are made available to it.
A BC makes running another developer's code less liable to fail. It both accelerates continuous integration and provides it performance guarantees. Working with CI and BC in tandem, a developer only needs to push code for it to be tested, merged, built, cached, and shareable with others, who can then reliably reproduce their results on another machine.
How does CI/BC work with Nix?
Although CI/BC is technology-agnostic, Nix is particularly well-suited for its implementation. Nix is a package manager and functional language for Linux systems. It stands out in this case because it has been designed with reproducibility in mind, which is the fundamental principle behind CI/BC.
Reproducibility is the measure of a build's likelihood to produce the same output on different machines when the inputs haven't changed. As a design principle, it surfaces in the careful management of dependencies. Across build environments in a continuously integrated network, the variability of dependencies (due to their being updated, deleted, or requiring something else that is unavailable) is far more likely to cause failure than changes made to tested code. This has historically posed a problem to continuous integration that Nix has been poised to answer.
The grain of Nix's design naturally aligns with these practices. Explaining why Nix is well-adapted to a CI workflow in their article, "Introduction to NixOS", the Stelligent team writes, "NixOS, and declarative immutable systems, are a great fit for CI/CD pipelines. With the entire system in code, ensuring and auditing reproducible environments becomes easy. Applications can also be 'nixified,' so both system and application are fully declarative and in version control." By implementing purely functional principles, Nix can reliably promise that a build output's will not vary if its inputs do not change. Deterministic, automated CI frees projects from the mesh of machines they're built on.
Due to its focus on function as a tool to orchestrate reproducible builds, Nix is primed to make use of binary caches. Although most Nix packages today are acquired via build derivations, the Nix store does already house binary artifacts for users. NixOS (an operating system built in/around Nix) makes extensive use of its binary cache to speed up development. The real-world value of these mainline implementations have led to the development of Nix-centric CI/BC services to supplement them. In a Nix environment, these benefit because they extend core functionality rather than invent it.
How might CI/BC in Nix change?
While Nix implementations of CI/BC have been highly successful, there is room for improvement that the next few years could satisfy.
For example, some Nix CI/BC systems will rebuild already-built items, costing time and computation. Robust reporting about both per-derivation build statistics and insights into the dependency tree could become more standard.
The use-value of CI/BC only stands to grow more apparent as remote or hybrid workforces continue to adapt to distributed teams. For many developers building the infrastructure to implement these practices, Nix has offered a path proven to work.
This has been a brief introduction to continuous integration and binary caching and a look at these concepts' relationship with Nix. We hope it's been helpful. Find us on Twitter & let us know your thoughts.