News for hackers. Stuff that matters.
Re:We built the fastest CI in the world and it failed by alexvitkov on Tuesday September 12, 2023 @10:46AM (#37482115)
What does fast CI even mean?

CI is an overgrown shell script running your build and telling you when it fails. In general build tooling has gotten so slow that the cost of whatever CI runner relative to it should be nil.

If you want fast CI you need fast tsc, clang, rustc, etc... not a faster program that calls exec on them.

A bit more on topic, if you're selling CI and your business fails it's because people you're providing no value - people can run their build scripts fine without you.

Re:We built the fastest CI in the world and it failed by jcranmer on Tuesday September 12, 2023 @10:51AM (#37482190)
Perusing this article quickly, it means a CI that is automatically handling things like caching build artifacts so that you don't need to recompile your entire repository every single commit. It's not about a faster program to call exec; it's about a program that knows it need not even call exec.
Re:We built the fastest CI in the world and it failed by alexvitkov on Tuesday September 12, 2023 @11:03AM (#37482364)
Incremental builds are something even enterprise build systems fail at.

I've had Visual Studio and MSBuild fail on me multiple times by deciding not to recompile a certain .cpp file, because they got the dependency graph wrong. This results in a running but inconsistent executable that's a bitch to debug.

If you're selling me another tool that duplicates this work, but that has even less context on my project than the build system, I'll pass.

Re:We built the fastest CI in the world and it failed by no_wizard on Tuesday September 12, 2023 @12:37PM (#37483887)
One thing these tools should do automatically is at the end log to the console (or whatever system) what was compiled and what wasn't.

This made debugging so much easier for these issues. They're bound to happen, because we're all human and bugs are a thing. Automatically being able to audit the build as step 1 makes this easy to spot

Re:We built the fastest CI in the world and it failed by jcparkyn on Wednesday September 13, 2023 @01:19AM (#37492502)
Visual studio does give a fairly detailed explanation of what was compiled each time, in the output>build view.
Re:We built the fastest CI in the world and it failed by pharmakom on Tuesday September 12, 2023 @01:14PM (#37484420)
Bazel does this.

I have to be honest - MS Build does not scale well. Companies use it because it’s the default.

Re:We built the fastest CI in the world and it failed by no_wizard on Tuesday September 12, 2023 @01:56PM (#37484959)
Seems they came up with a way to scale up build toolchains with BuildXL[0]

Doesn't seem fully baked yet though

[0]: https://github.com/microsoft/BuildXL

Re:We built the fastest CI in the world and it failed by CobsterLock on Tuesday September 12, 2023 @02:29PM (#37485388)
I don't have a ton of experience in Bazel, but from what I have experienced with Bazel I think it would be more accurate to say that you configure the dependency graph yourself in Bazel. Then Bazel can determine what to build from scratch and what to take from cache
Re:We built the fastest CI in the world and it failed by howinteresting on Tuesday September 12, 2023 @05:46PM (#37488941)
Yes, but Bazel requires an all-in commitment and also tons of work to fit the rest of the world into it.
Re:We built the fastest CI in the world and it failed by aidos on Tuesday September 12, 2023 @10:56AM (#37482261)
I looked at Earthly just a couple of weeks back. It’s a tool that makes perfect sense - especially when I was deep in a makefile wishing the caching was as automated as docker.

In the end I decided that I couldn’t face the complexity of dealing with someone else’s hosted tooling. Luckily in this case.

Re:We built the fastest CI in the world and it failed by adamgordonbell on Tuesday September 12, 2023 @11:26AM (#37482712)
Yes, exactly.

Earthly is explicit about inputs ( using COPY) and outputs of each build step. This let's it be 100% certain about whether it can cache parts of the build.

If you are familiar with docker layer-based caching and Bazel, you can imagine how Earthly works to eliminate rework in builds.

Here is a small monorepo example: https://github.com/earthly/earthly-solutions

Re:We built the fastest CI in the world and it failed by theamk on Tuesday September 12, 2023 @11:06AM (#37482417)
except not really.. their tutorial [0] includes no-compile languages like python and JS, with only non-trivial compilation being and Java... and how do they do it?

    RUN gradle build
At least in this case, Earthly has no insight into how repository is organized, and it _will_ recompile the entire repository with every single commit.

So what's the real value of it? this basically seems like a better "docker build" alternative, giving nicer inter-layer caching. Maybe will save some time on dependencies if they take some time to install and devs were too lazy to set up own workers and used ephemeral runners instead.

[0] https://docs.earthly.dev/basics/part-1-a-simple-earthfile

Re:We built the fastest CI in the world and it failed by danudey on Tuesday September 12, 2023 @12:12PM (#37483479)
That's step one, 'how to write a basic Earthfile that just does the thing', so they don't delve into the details of caching and how it works, but look at the whole block and not just that one line; it operates like a Dockerfile:

    build:
        COPY build.gradle ./
        COPY src src
        RUN gradle build
        RUN gradle install
        SAVE ARTIFACT build/install/java-example/bin /bin
        SAVE ARTIFACT build/install/java-example/lib /lib
The COPY phases copy files into the build context. If the files haven't changed, then the commands don't need to be run. In other words, if `build.gradle` and `src` are unchanged, it won't run `gradle build` or `gradle install`, and it'll just give you the artifacts from the previous build.

They have a golang example in part three[0], which they redesign to use caching effectively:

    build:
        # Download deps before copying code.
        COPY go.mod go.sum .
        RUN go mod download
        # Copy and build code.
        COPY main.go .
        RUN go build -o output/example main.go
        SAVE ARTIFACT output/example AS LOCAL local-output/go-example
They copy in go.mod and go.sum and then run `go mod download`. If the mod and sum files haven't changed, then `go mod download` doesn't need to be run.

Then they copy `main.go` in, and run `go build`. If the `main.go` file hasn't changed, `go build` doesn't need to be run.

[0] https://docs.earthly.dev/basics/part-3-adding-dependencies-w...

Re:We built the fastest CI in the world and it failed by brabel on Tuesday September 12, 2023 @12:51PM (#37484099)
Gradle can do the caching of tasks itself in a level of granularity no CI can. If you keep your build cache "live" in the CI, Gradle knows to only recompile what has changed, and to only run tests which changed (or tests something which changed). I am not sure if many other build systems do that, but this is something I would absolutely expect from the build system, not CI. Do you not expect caching when building locally??

The whole idea with CI doing this is wrong IMO. All I want CI to do is call a couple of commands after setting up the environment (which would include the Gradle cache or whatever to speed up things - no need for your revolutionary CI build system).

Re:We built the fastest CI in the world and it failed by nvm0n2 on Tuesday September 12, 2023 @02:01PM (#37485014)
A CI system like TeamCity will give you that sort of thing for free though, without needing an Earthfile. It just dedupes builds at the same commit, and keeps checkout directories between builds on the agents, so if the build tool has a cache then it's automatically reused. Works great.

Given that it's hard to understand what exactly this is for? Build systems normally try to avoid redundant work for local purposes anyway. Is it some hack for companies that insist on resetting build environments to zero on every single build, and then decide they want caching back? If so, why not just ... let the builds take place in directories containing previous builds, and fix the problems? That's what we do at my company and it works great (using gradle), there are only rarely problems with unclean builds and it's easy to rerun with a clean checkout if that's suspected to be an issue (the bugs are mostly in our own code which does a layer of caching above the build system anyway).

Re:We built the fastest CI in the world and it failed by oftenwrong on Tuesday September 12, 2023 @09:41PM (#37491157)
[delayed]
Re:We built the fastest CI in the world and it failed by nvm0n2 on Wednesday September 13, 2023 @04:05AM (#37493625)
So they claim. And yet ultimately they do it by caching things, same as every other system.
Re:We built the fastest CI in the world and it failed by theamk on Tuesday September 12, 2023 @03:14PM (#37486347)
sure, but in a java project, how likely you are to have a commit with no changes to src/? or for a go project, how likely is it that you want to duplicate you entire dependency tree in a clunky, non-parallel dependency description language, vs just doing "COPY * .; RUN go build" and letting golang figure it all out and build in parallel using all the cores?

Don't get me wrong, there is definitely value in caching dependencies, and I can see Earthly approach help if you have, say, a Python app, with 10 minutes of dependency install time, 0 build time, and 10 seconds of tests - you'll see huge improvement. But you'll also see almost the same improvement if you use Dockerfile on a persistent host instead, and as a bonus you won't have to learn new system.

One place Earthly might shine are monorepos with multiple independent projects that even use multiple build systems... but I am not sure how many of those exist _and_ don't have some other caching solution already.

Re:We built the fastest CI in the world and it failed by yencabulator on Wednesday September 13, 2023 @11:47AM (#37498296)
That only splits dependency-build from app-build. Any change to the app source code still causes a full rebuild, unless you figure out how to share the cache directory between builds (and then parallelism gets sketchy).

Generic CI just simply can't parallelize+cache on a level comparable to systems that understand the build itself (Bazel, sccache, etc).

Re:We built the fastest CI in the world and it failed by dgb23 on Tuesday September 12, 2023 @12:00PM (#37483278)
Isn't this a complex solution for a problem that shouldn't exist?
Re:We built the fastest CI in the world and it failed by jve on Tuesday September 12, 2023 @10:50AM (#37482176)
> If you want fast CI you need fast tsc, clang, rustc, etc... not a faster program that calls exec on them

Yes and no. Caching stuff and knowing when to run tsc/clang/rustc also improves performance.

Re:We built the fastest CI in the world and it failed by bluGill on Tuesday September 12, 2023 @11:15AM (#37482547)
Sure, but you have to be 100% perfect in guessing when the cache file must be rebuilt. If there is any situation at all where you use the cached file when you should have rebuilt your tool is not useful.

Note that you can go too far. If you rebuild a file on linux even though the only changes were in a "#ifdef freebsd" (windows, mac...) section that is a waste of time, but not harmful. However we already have tools that do that.

Re:We built the fastest CI in the world and it failed by esafak on Tuesday September 12, 2023 @11:28AM (#37482752)
This is a solved problem; modern build tools and workflow orchestrators face the same challenge. You declare your inputs for a given task and cache the output as long as the inputs are unchanged, as determined by their hash. So there is no wrong guessing, only incomplete an specification of inputs. It's an elegant solution, if I do say so.
Re:We built the fastest CI in the world and it failed by duped on Tuesday September 12, 2023 @11:53AM (#37483146)
It's not a solved problem, very few builds in the wild are deterministic and reproducible. You have to build up the universe from scratch (like guix and nix) for the caching to be sound enough to be reliable.

Just because an algorithm exists that can help doesn't mean that the universe of software can fit cleanly into a model where that algorithm is perfect.

The other fundamental problem with this model is that it cannot model cyclical dependencies (a degenerate case: glibc needs python which needs glibc). Since you can't hash the output until you've hashed the input, and the input hash depends on the output hash you need a way to break the dependency cycle. Tools have varying ways of mitigating this but it's not exactly "solved" in an elegant sense.

And finally, a lot of build steps are faster than hashing their input and outputs. So if you want the utmost performance you need heuristics or a model that allows for some things to be hashed and others to always be considered fresh.

Re:We built the fastest CI in the world and it failed by danudey on Tuesday September 12, 2023 @12:40PM (#37483920)
> very few builds in the wild are deterministic and reproducible

There's a difference between 'deterministic and reproducible' and 'predictable'. Sure I can (probably) build curl ten times and get ten technically different binaries, but the differences aren't relevant to the functioning of the actual result.

Assuming my build environment isn't changing out from underneath me (i.e. if I'm using the same versions of the same dependencies on the same platform with the same compiler toolchain, and I'm not passing in different arguments or environment variables) then the end result should be functionally identical.

> You have to build up the universe from scratch (like guix and nix) for the caching to be sound enough to be reliable.

This is a false assertion. If I'm building (again, as an example) curl, then I don't need to be rebuilding the kernel, glibc, kerberos, libssh, gnutls, brotli, etc. all in order to get a working curl build; I just need to make sure the libraries and toolchains I have installed are valid. If I create a docker image with all of the dependencies and toolchains preinstalled, and then I build the exact same curl code inside of that docker container on two separate occasions, then the end result is going to be indistinguishable without hashing the build artifacts.

> Just because an algorithm exists that can help doesn't mean that the universe of software can fit cleanly into a model where that algorithm is perfect.

It doesn't need to be perfect, it just needs to be correct for the vast majority of cases (which are not somehow inherently broken already and just haven't failed yet).

> a degenerate case: glibc needs python which needs glibc

Because you're not rebuilding the entire world (in 99% of cases), this doesn't actually matter. If you're building glibc use the existing build of python that you already have installed, which is the same identical version to the one you used last time you built glibc.

If I'm rebuilding glibc, do I also need to rebuild python? If the glibc code hasn't changed, but maybe python has so I get a different result? Well okay, has the python source changed? No? Okay, have the dependencies for python or glibc changed? No? Okay, well problem solved then.

I'm not sure why people have this philosophy of "this problem isn't solved because of some imaginary, unrealistic corner case that either shouldn't exist in the first place, isn't actually a real problem, or that no one will run into". In 99% of cases it works, and if it's not right for a particular circumstance then just don't use it in that circumstance. Sometimes makefiles aren't the right tool. Sometimes a GUI isn't the right tool. Let's not argue that we shouldn't have GUIs because sometimes running a command on the CLI is easier, and let's not argue that an extremely basic caching system that makes sane assumptions that are almost always valid isn't a good idea.

And the parent is right: if you correctly define your inputs and outputs, then the system works. What those inputs and outputs are is up to you to determine. Maybe for you, the outputs are a working build of glibc, and maybe the inputs are all of the tarballs necessary to build Linux from scratch and a working bootstrap environment, but if all of those inputs and that whole environment are identical to last time, what's the point of rebuilding any of them? Maybe there is one, but that's up to you to determine and model.

Re:We built the fastest CI in the world and it failed by Karellen on Tuesday September 12, 2023 @01:08PM (#37484349)
> Sure I can (probably) build curl ten times and get ten technically different binaries

I think assuming the same build environment, then building curl is actually deterministic and reproducible:

https://tests.reproducible-builds.org/debian/rb-pkg/unstable...

(It says reproducibility fails if CFLAGS is different, but I'd count that as a change in build environment?)

Re:We built the fastest CI in the world and it failed by duped on Tuesday September 12, 2023 @02:11PM (#37485143)
I agree with you, but my point is ultimately that content hashing inputs like GP is arguing "solves" the problem doesn't actually solve the problem.

Take your curl example. Its inputs are its dependencies. Now say you depend on libcurl and a more recent version of glibc than curl was compiled against. If you only track the hashes of inputs to determine if something needs to be rebuilt, then in this scenario you will have to rebuild libcurl.

What you're talking about (determining if your dependencies are "valid" and don't need to be rebuilt) involves tracking compatibility metadata out-of-band of the actual content of the build artifacts, which is not a solved problem.

The point about reproducible builds is that they're required for any kind of build system that depends on content hashing to determine if something needs to be rebuilt, because it's actually impossible to track that based on the content alone.

It gets hairier when you start abstracting what a build artifact that depends on another build artifact is, because there's more to builds than object files, dlls, and executables.

Re:We built the fastest CI in the world and it failed by IshKebab on Tuesday September 12, 2023 @05:38PM (#37488821)
It is a solved problem - you need to use a build system with sandboxing like Bazel, and also vendor all your tools.

Almost nobody does that because very few people realise it is the solution and even fewer realise they're going to need that before they set up their build system.

Re:We built the fastest CI in the world and it failed by smaudet on Tuesday September 12, 2023 @08:01PM (#37490385)
Switching to a build system like Bazel can be a significant cost, though.

There's no easy solution to making your builds go faster, if they aren't already. There's hardwork and artifact management (dependency hell).

Unless you are asserting Bazel is a direct competitor to Earthly, I don't think the assertion is useful at all.

Re:We built the fastest CI in the world and it failed by zelos on Tuesday September 12, 2023 @12:32PM (#37483791)
From my experience with Gradle, it's fairly tricky at the build tool level, trying to do it at the CI level must surely be harder.

If (for example) I change a value in gradle.properties at the top level, do I need to rerun every test in every sub project? You need detailed knowledge of how the build is structured to decide that.

Re:We built the fastest CI in the world and it failed by yencabulator on Wednesday September 13, 2023 @12:09PM (#37498602)
Where that world sucks is that it forces you to ignore or duplicate work from the native build system. E.g. Go or Rust know which source files are needed to build something, yet Bazel makes you write that out.

No, generating the file isn't "the fix" if you're still committing those generating files into the source control; that's still duplication and manual labor.

Re:We built the fastest CI in the world and it failed by danudey on Tuesday September 12, 2023 @12:24PM (#37483673)
> you have to be 100% perfect in guessing when the cache file must be rebuilt

Or, you make reasonable assumptions that are correct in most cases, and rely on the user to provide hints as to when the cache should be invalidated.

Assuming your code, environment variables, and `args` are all factored into invalidating the cache, there are few situations in typical builds where using the cache is a bad idea. The biggest ones I can think of are if your build system is going out and fetching data itself to decide what to do, e.g. if you're downloading source code (e.g. a shared library) from an external source which may have updated it.

That said, if you're just arbitrarily pulling down whatever the latest build of libcurl is or something, you're going to have a bad time eventually anyway, so you should either put a static version into an arg or just put the download into a separate step; at which point the cache will correctly invalidate when it's changed.

Maybe if a build is only valid for 90 days from the build date or something (e.g. a preview build) and that's built into the binary at compile time, but in that case you can pass that in as an arg anyway and then your cache is going to be invalid every morning regardless.

I'm not going to say it's never an issue, but the same code with the same environment and the same arguments, built on the same platform with the same versions of the same dependencies, using the same versions of the same tooling to run the same commands and copy the same artifacts to the same locations should very nearly never produce a valid output now which is not an equally valid output in the future unless it is fetching and acting on external information (which can be easily parameterized).

Re:We built the fastest CI in the world and it failed by jnwatson on Tuesday September 12, 2023 @12:44PM (#37483973)
The only part of the Earthly pitch that resonated with me was the ability to run the CI locally. When debugging your CI, it is just faster to find problems when you can run on your own box.

I have a gnarly GitHub Actions script that took forever to get right because the debug cycle was 10 minutes long.

Re:We built the fastest CI in the world and it failed by fireflash38 on Tuesday September 12, 2023 @05:20PM (#37488571)
That's one of my biggest complaints about gitlab-ci. Theres basically no way to debug complex rules without just running it. They've got a way now to simulate for default branch, but that's like bare minimum necessary. No way to simulate based on vars or anything else. And no way to try locally, just gotta fucking send it live.
Re:We built the fastest CI in the world and it failed by ianwalter on Tuesday September 12, 2023 @05:24PM (#37488633)
I feel you on that one. There is also Act (https://github.com/nektos/act) and Dagger.io.
Re:We built the fastest CI in the world and it failed by jnwatson on Wednesday September 13, 2023 @04:15AM (#37493696)
I have a very complicated script and Act didn't work on it.
Re:We built the fastest CI in the world and it failed by smaudet on Tuesday September 12, 2023 @07:52PM (#37490315)
Yeah, the faster but is also a bit of a non-seller, they have to integrate the world/deal with bad build times that can't easily be parallelized (what if there are no modular dependencies, that your tool can understand?) except by tools that have nothing to do with their product.

But there have been many times when debugging a CI has been necessary, having to figure out what has gone wrong on a remote system is often an issue...ofc often the remote system is itself the problem...

Re:We built the fastest CI in the world and it failed by crabbone on Tuesday September 12, 2023 @01:36PM (#37484691)
> CI is an overgrown shell script running your build and telling you when it fails.

Oh, I wish it were that simple...

But, I also don't know what they measured. But, I'll give you just one example. Jenkins default landing page is a disaster in terms of speed. It tries to display data on recent builds everywhere across your cluster. For even not so big clusters this can be hundreds to thousands of items to pull from individual nodes running those builds. I've managed to crash Jenkins more times than I could count by simply loading the landing page w/o a particular configuration added that prevents it from its default behavior.

CI server will have its own database, typically, managing all sorts of CI entities: jobs, artifacts, users, secretes... It could be quite big and you'd do well taking care of proper indexing etc.

CI will have its assortment of runners, which are often provisioned dynamically (think VM or Docker images that need to be deployed to runner nodes), distributing them across the cluster is also not an easy task, if you want that to work fast. Of course you also want to be able to distribute the artifacts across the cluster -- this also takes time and resources.

CI will have its own bookkeeping necessary to, essentially, do garbage collection, reporting and self-diagnostic.

In big enough clusters this stuff can have a very high latency, if you don't specifically work towards reducing that latency.

> If you want fast CI you need fast tsc, clang, rustc, etc... not a faster program that calls exec on them.

Ever heard of ccache?

But, really, this is also super-naive. To make builds fast in a distributed system that CI is you would also need to figure out how to distribute this cache, how to modularize the builds... You might have heard about how one of the most difficult things in programming is cache invalidation? -- That's only partly a joke.

Re:We built the fastest CI in the world and it failed by jrockway on Tuesday September 12, 2023 @01:59PM (#37484988)
The tricky part of CI is figuring out what work doesn't need to be done. That is how you save time.