top | item 34885077

Launch HN: Moonrepo (YC W23) – Open-source build system

200 points| mileswjohnson | 3 years ago

Hey HN, Miles and James here from Moonrepo (https://moonrepo.dev). Are you struggling with large codebases? Well look no further! We built Moonrepo to simplify repository management, project ownership, task running, and everyday developer and productivity workflows.

If you’ve used Bazel (or another “enterprise” build system) in the past, you’re probably aware of how complex they can be to setup, configure, and use. Let alone the cognitive overhead required by developers on a day to day basis. After more than a decade in the industry, with many of those years working on infrastructure and developer tooling related products, we set out to build Moon, a language agnostic build system.

Existing systems focused solely on runtime logistics (faster builds, concurrency), while we want to also focus on the developer experience. We do this by automating workflows as much as possible, in an effort to reduce manual work. We constantly sync and verify configuration, so that the repository stays in a healthy state. We also infer/detect as much as we can from the environment/repository/codebase, so pieces "just work".

We wanted our system to be enjoyable to use and easy to understand, but also solve the same problems as existing systems. For example, configuration is in YAML, not a proprietary syntax. Tasks are defined and run as if you were running them in the terminal; no more abstractions like BUILD files. Unlike Bazel, we don’t hide or heavily rewrite terminal output, so the feedback loop is what you expect. We manage a toolchain, ensuring the correct version of languages is used (no more “works on my machine”). And lastly, our foundation is built on Rust and Tokio, so performance is first-class, the runtime is reliable, and memory safety is guaranteed.

We follow the open core model. Moon is open source, but we’re also working on a few subscription-based services for monitoring and improving your continuous integration pipelines, a registry of project and code ownership, a continuous deployment/delivery board, auxiliary application systems, and more. We haven't finalized the subscription model yet, so there's no pricing information on the website. However, we do have a starter/free tier that everyone can use by registering on https://moonrepo.app. In the future, we will offer on-prem as well.

Although Moonrepo is relatively new, we’re already feature-packed, stable, and used in production. We’re big fans of honest feedback, and look forward to your comments!

171 comments

order
[+] haberman|3 years ago|reply
> Tasks are defined and run as if you were running them in the terminal; no more abstractions like BUILD files.

But abstractions are a good thing! They let you separate a system into layers, so that things programmed at a higher layer don't need to know all the details of the lower layers. It's the way we separate interface from implementation.

Let me give an example from Bazel. Suppose you are compiling some C or C++, and you need to define a preprocessor symbol. There are two ways of doing this, "defines" and "local_defines":

    cc_library(
        name = "my_lib",
        srcs = ["my_lib.cc"],
        hdrs = ["my_lib.h"],
        # This will pass -DFOO when compiling this library
        # *or* any library that depends on it.
        defines = ["FOO"],
    )

    cc_library(
        name = "my_lib",
        srcs = ["my_lib.cc"],
        hdrs = ["my_lib.h"],
        # This will pass -DFOO when compiling this library
        # only.  Libraries that depend on us will *not* get
        # the symbol.
        local_defines = ["FOO"],
    )
You can use "defines" when you have #ifdef statements in headers and "local_defines" when you only test the symbols in your .cc files.

I did not have to think about how defines will propagate to the libraries that depend on me, I just specified what I need and the build system handles the rest. I did not have to define my own CXXFLAGS variable and handle concatenating all of the right things together. The lower layer takes care of that.

What Bazel lets you do is create abstraction boundaries within the build system. People can write rules that define a self-contained set of build logic, then other people can use those abstractions without knowing all the details of how they are implemented.

Bazel is not perfect, and I have found myself banging my head against the wall many times. But overall the ability to define abstractions in a build system is, I think, one of the biggest things it gets right.

Disclosure: I work for Google, but not on Bazel.

[+] jcalder|3 years ago|reply
Also, MIGRATIONS!

Interfaces are VERY GOOD for migrations.

If you decide that you want to stop using some compiler flag, or maybe use a different compiler, or change your python version, or...

You can right a regex to go over all your shell invocations, change them, then test, or you can do something like:

``` def build_thing(name, srcs, hdrs, migration=False): if migration: ...

build_thing( name = "my_lib", srcs = ["my_lib.cc"], hdrs = ["my_lib.h"], ) ```

You can then manually flip that to test stuff, write a script to flip it for every team, sending out a PR (because your targets can have oncalls) and they can land it, and then at the end you can flip the default and manually add a False to the holdouts.

All this stuff gives you the ability to do hard stuff at scale.

[+] underdeserver|3 years ago|reply
If I understand this correctly -

This means that #ifdef FOO will be evaluate to true _inside the .cc and .h files listed in srcs_, when building this target or dependent targets, but might be something else when downstream targets' object files (.o) are built.

[+] Aeolun|3 years ago|reply
Maybe that’s more relevant for the non-js ecosystem? I never find myself wanting to modify my source while it is being built.
[+] jcalder|3 years ago|reply
> We wanted our system to be enjoyable to use and easy to understand, but also solve the same problems as existing systems. For example, configuration is in YAML, not a proprietary syntax

I'm incredibly skeptical of this.

I'm ex-meta and have worked a lot with the enterprise solutions you're talking about and the choice of starlark (originally python) as the build definition language is one of the killer features of the systems.

People want to create macros, target generators, etc. It's a common use case for a lot of engineers and IMO is a pretty killer feature.

Being able to say "This is an X86 Python Binary", "This is an M1 python binary" and then bundle those into "this is how you build either of those binaries based on inputs" without ever touching the internals or anything other than (more or less) a blob of python is why those tools scale organizationally.

It allows the teams that need to do weird stuff to unblock themselves without drowning the tools org. Sure, it has draw backs. Super deep macro layers are kinda a crime against humanity and debugging/evolving them can be quite expensive, but I think that's just the cost of software.

If that logic isn't in the build definitions it'll expand into a meta layer that generates configuration (I've seen giant "Translate this definition into 30 configs to run stuff" systems time and time again).

I may just be super biased from past mistakes and wins, but I think what you're doing is just moving the complexity out of your tool into neighboring tools and selling it as a win isn't really true, it's shuffling complexity around not removing it.

[+] mileswjohnson|3 years ago|reply
Based on everyone's feedback about YAML (we didn't expect this much), we'll probably reconsider this!
[+] jpgvm|3 years ago|reply
Bazel has a heavy focus on correctness (pretty much to the exclusion of everything else). Where does Moon fall on the correctness gradient? Does it enforce hermecity or deterministic builds or give me tools to accomplish it?

In the same vein as those questions how does caching work? Is it content based like Bazel or mtime like Nx et al? If there is no sandboxing does it do input tracking or is there manual "cache key" shenanigans?

If the configuration language is YAML how am I expected to implement new behavior? Is that in Rust? Is there a plugin architecture? Do I need to rebuild the tool itself and redistribute it to my build machines and developers? The main appeal of Starlark/Python in build systems is ability to create macros or in many cases define entirely new rulesets to support new tools and languages without needing to modify the complex and performance sensitive core.

Sorry for the skeptisicm but build systems are very complex beasts and new entrants like Nx don't measure up to tools like Bazel very well.

[+] thundergolfer|3 years ago|reply
To be clear, Bazel focuses on correctness because it's essential to acheiving performance at scale.

If you don't have correct caching of intermediate build artifacts, a system can't handle the compile and test requirements of large codebases.

[+] hamandcheese|3 years ago|reply
I think it's hilarious to say bazel focuses on correctness when it doesn't ship with any hermetic toolchains by default. Properly setting up hermetic toolchains is poorly documented and left as an exercise to the reader.

I say this as someone who wants to love bazel... I just can't understand why it picks up impure toolchains from the system at all.

[+] mileswjohnson|3 years ago|reply
> Does it enforce hermecity or deterministic builds or give me tools to accomplish it?

I wouldn't say moon is hermetic, nor are we trying to be. We don't use the sandbox approach for tasks, and run against the original files. For the languages we support, this works best.

As for deterministic, we try to be. We have a "toolchain" where we download languages/tools in the background, and run tasks using these tools. This ensures, at minimum, that the same version/variant of a tool is used across machines.

> In the same vein as those questions how does caching work?

It's content based. We also hash other kinds of inputs depending on the language that is running.

> If the configuration language is YAML how am I expected to implement new behavior? (and other questions)

Our focus right now is on non-compiled languages, primarily web languages, where custom behavior (like Starlark) is not necessary. In the future, this may change.

[+] andrew_|3 years ago|reply
Bazel as a taskrunner/build system is to most monorepos as a Pile Driver [1] is to a floorboard nail. It will do everything but it's massive overkill. We are agreed that Nx doesn't measure up, but I wouldn't compare it to Bazel.

[1] https://en.wikipedia.org/wiki/Pile_driver

[+] jedberg|3 years ago|reply
My advice to anyone making a new build system:

Most likely you did this because you felt all the other ones are too complicated.

But the reason the “enterprise” ones are so complicated is to serve their enterprise customers, who need “just this one feature” so they can use it. But those customers pay the bills.

So basically you have to choose complexity and profit or simplicity and less (or no) profit.

Good luck! But make sure you’re ready to make that choice.

[+] nitsky|3 years ago|reply
Are you sure you can't have your cake and eat it too? You can have many configuration options, but give each one a sane default.
[+] lhorie|3 years ago|reply
IMHO, if you're targeting the Javascript ecosystem, this area is already fairly crowded, with Turborepo, Nx and various open source tools providing various degrees of functionality (Bazel, Pants, Lerna, etc) already competing in the space.

I'm a tech lead for the web monorepo at Uber. We talked to the Turborepo guy a few years ago, and he admitted that he wasn't sure if it could handle our scale in terms of all the bells and whistles that a repo of our size leverages - and his is one of the more feature packed commercial offerings in this space.

As a random example: we see thousands of commits a week, so invalidating the whole build graph when the lockfile changes is a complete non-starter, yet most turn-key solutions target small teams and are not equipped to cope with this problem. Phantom dependencies[0] are a problem with disjointed build graphs. Etc.

As someone who's evaluated a ton of these systems, I would love to see where your experience in this space is coming from. A new kid in the block will have a lot to prove.

[0] https://rushjs.io/pages/advanced/phantom_deps/

[+] mileswjohnson|3 years ago|reply
We agree. We weren't happy with all of the current solutions, at least in the JavaScript space.

In regards to build graph invalidation, we attempt to hash pieces at the granular level. This includes per file content hashing, and for dependencies (those in `package.json`), we parse the lockfile and extract the resolved version/integrity hashes. We can probably improve this further, but this has been working great so far.

As for phantom dependencies, this isn't something moon solves directly. We encourage users to use pnpm or yarn 3, where these phantom problems are less likely. We don't believe moon should solve this, and instead, the package managers should.

If you have any other questions or concerns, would love to hear them.

[+] palmdeezy|3 years ago|reply
Turborepo author here…

We do not invalidate the whole graph anymore for a lockfile change. We now have a sophisticated parser that can calculate if a change within the lockfile should actually alter the hash of a given target. In addition to higher cache hit rates, this is what powers our `turbo prune` command which allows teams to create slices of their monorepo for a target and its dependencies…useful for those building in Docker.

Prune docs: https://turbo.build/repo/docs/reference/command-line-referen...

Turborepo is much more scalable now than when we spoke pre-Vercel acquisition. It now powers the core web codebases at Netflix, Snap, Disney Streaming, Hearst, Plex and thousands of other high-performance teams. You can see a full list of users here: https://turbo.build/showcase

Would be happy to reconnect about Uber’s web monorepo sometime.

[+] andrew_|3 years ago|reply
Nx is hot garbage imo. Buggy, overly verbose, inflexible, poorly documented, and I'm dubious about their peer review process.

If I don't need build caching I'm not using any tool but PNPM and its workspace toolset - that's literally all most people need for a monorepo. I've looked into Turborepo, and its simplicity versus Nx is a strength. However, it's not the taskrunner that I want.

I now work in a monorepo where build caching is required, so I'm excited about moon and keeping a watchful eye on the project's progress. From my evaluation so far, it fixes all of my gripes about Nx and I'm keen on it not trying to do too much, while allowing me to make it as flexible and extensible as I need. Extending configs is chefs kiss

[+] IceWreck|3 years ago|reply
You're building something like buck/blaze and replacing starlark with YAML ? I want a full programming language when defining complex rules. Build systems do a lot more than execute commands.

You lost me there. Buck/Blaze aren't perfect but configuration was never an issue.

Moon like it is currently is a glorified task runner.

[+] astral303|3 years ago|reply
Configuration is code, so the YAML written in your tool is still code, code that developers are consuming and versioning. Making it YAML doesn’t make it not code. Your YAML configuration files are proprietary syntax, it is a proprietary code syntax written in a language (YAML) lacks basic abilities for logic or control flow. In a system of any worthwhile complexity, inevitably custom build logic will have to be expressed, and I can’t imagine it being good when it is being cooked up in a proprietary configuration language based on YAML.

How easy will it be for layers of abstractions to emerge in this system? That’s the ultimate limitation of markup-language-based / configuration-based build systems.

Look up the difference between external DSLs and internal DSLs. The YAML config file is an external DSL. I prefer to express configuration using internal DSLs.

[+] SCLeo|3 years ago|reply
Why? Why YAML? Why? YAML is absolutely terrible!

Aside from the classic `country_code: NO`, the other day I ran into issues with scientific notations. Now, guess, which of the following are strings, and which are numbers:

- 1e+10

- 1.e+10

- 1.0e10

[+] beisner|3 years ago|reply
What's the motivation for using YAML instead of Starlark (Bazel, Buck) or something closer to Python (Pants, please.build)? Seems as though much of the other monorepo tools have (kinda) standardized on this.

As I understand it, the primary reason these build systems leverage these Python-variants is so that the build rules, toolchains, constraints, and build definitions can all be written in the same language (since build rules often require some programmatic behavior). Perhaps with a future vision of them being totally interoperable across build systems.

[+] Denzel|3 years ago|reply
Not to mention using an actual language aids readability, extensibility, and static analysis, unlike a data exchange format. Starlark is a benefit, not a con, so YAML feels like a major step backwards.

I'm generally happy with Blaze/Bazel, so I'm not necessarily in the target market for Moonrepo, I guess.

EDIT: This isn't really competing with Blaze/Bazel either when I look at the execution model. It goes back to imperatively defined tasks instead of declaratively defined dependencies, which feels more spirtually aligned with Make than Blaze/Bazel.

[+] mileswjohnson|3 years ago|reply
We chose YAML for a few reasons. The first being that we wanted a format that is basically universally supported everywhere. This filtered down to JSON, TOML, and YAML. JSON is an awful configuration format, so that was a no go. TOML is pretty great, but is also not very ubiquitous. That left us with YAML.

The second reason is we wanted something language agnostic and not proprietary. It also helps that many other tools, like GitHub actions, are written in YAML.

And lastly, we wanted a format that developers are familiar with, and won't need to spend time learning. Requiring developers to understand a special syntax simply to define a task to run is something we want to avoid.

[+] andrew_|3 years ago|reply
I much prefer YAML rather than having to write more code for a build system.
[+] tbezman|3 years ago|reply
Came in here to say that Miles is an absolute fucking unit of an engineer / thinker. We just adopted Moonrepo at Gallery and it's been excellent. He's felt the pain of other tools (bazel, nx, turborepo). So happy to see this launch on the front page
[+] arnorhs|3 years ago|reply
Awesome, congrats. I've been an early trier of moon repo and I really fell for the slickness of the website and the name, when evaluating build tools.

I've primarily worked in typescript codebases and have used raw yarn workspaces, lerna, nx and recently evaluated moon and turbo.

The funky part is I eventually simply went with nx, not just because I've used it before, but also because I felt like the configuration is just simpler and more lightweight. Esp. since you can pretty much roll with it without defining much more than single simple config file - while moon required some 2-3 separate config files, plus config files in each project (I understand the per-project config is not required, I don't remember why I needed it - something to do with my build tasks)..

In any case I intend to give it another shot soon.

As for the whole config in yaml vs json vs toml or whatever, not a deal breaker since most of the time these sorts of configs are perhaps not something you end up interacting with programmatically - I know a lot of people enjoy editing yaml more than json files

[+] matesz|3 years ago|reply
I burned few months evaluating and for trying out some monorepo options, for a cross-platform typescript project. Eventually I picked Nx, the main reason being having single package.json.

Besides, yes, nx comes with a single configuration file for each project, but alongside of it are jest, eslint, babel and app/lib/spec/ide tsconfig configs - that’s a lot!

All of this shouldn’t be visible to the developer. Initially, when trying to find myself in this mess, I thought that the solution lies in autogenerated ide workspaces - for vscode and sublime. But in practice it wasn’t that helpful, because there is always something which is not handled by autogenerate multiroot structure but is needed, so one needs to have multiple windows open anyways.

Hopefully typescript 5.0 is going to help reduce some of this boilerplate with multiple tsconfig base classes, so lib/app/spec/ide tsconfigs will be able to extend from common base, which currently is not possible.

The worst part of Nx is that there is lack of ssr support, so I had to patch nx with patch-package to generate obfuscated css classes in prod, because currently it’s not possible with their webpack executor! Recently they tried to simplify it a little bit, reducing this webpack boilerplate, which introduced few bugs but it's better than not doing anything!

However, saying all of that and as someone mentioned in previous comments, dx is not primarily improved by lack of config boilerplate, but by having semantic structure of the actual code, clearly knowing what depends on what and where one can find it and put it. Right now we are on our own, because there is lack of information on how to structure cross platform codebase properly. I strongly believe it will change though. Best of luck to the people at moon!

Ps. Nx has a nice blog post why they do t use bazel under the hood https://blog.nrwl.io/on-bazel-support-6be3b3ceba29

[+] mileswjohnson|3 years ago|reply
Thanks for the feedback. Too much configuration is something we keep thinking about, and are working to streamline. It's a bit involved since we support multiple languages.
[+] QuiiBz|3 years ago|reply
Congrats on the launch! I've been following Moon since a few months, seems like an interesting project.

Could you explain why any existing project using Turborepo/Nx should switch to Moonrepo? What are the advantages and disadvantages? The support for multiple languages seems like a big advantage.

[+] eric_khun|3 years ago|reply
We've started using Rush [1] at Buffer. Teams have been slowly migrating, and it has been great so far. We had (too many) repos with their own workflow and way too many different ways to build services, which has been annoying to maintain. I know teams use Rush at scale: Tiktok has ~450 projects, and Microsoft said they have ~700 projects.

To deploy the services locally, we use Tilt[2] (K8s for local). We want to be able to reproduce production as much as possible and remove developer overhead on how things work locally and in production.

Then come the issues with Docker and large node code bases:

1 challenge with large monorepos is the huge node_modules folder (rush among other tools put packages into a single large node_modules folder, and symlink every dependency there. It can contain millions of files and GBs, depending on how many 3rd-party libraries you use). On Linux, you can mount it without issues, but Mac has performance issues[3] with large folders.

We pre-build that huge node_modules into a "base" image, and each service in the monorepo pulls that base image and only mounts what's necessary (a few mb). So we can save that npm build time and don't need to copy all those files inside the containers. It is fine because package.json does not change that often. You need to do this pre-build locally then in your CI/CD -> dockerhub, so everyone can get it.

Another challenge is that you need to "watch" files to rebuild them. Watching all your files inside the monorepo isn't really viable. We use a dependency graph to know what services to watch and then copy the built files inside the Tilt containers.

Hope this can help people.

[1] https://rushjs.io

[2] https://tilt.dev/

[3] https://github.com/docker/roadmap/issues/7

[+] jpgvm|3 years ago|reply
This is where Bazel + rules_js ecosystem shines. First of all it lazily fetches dependencies which means if you are only building 1 out of X projects you are only getting your projects dependencies from npm, then it's only providing just those dependencies to the sandbox when you are doing node.js things like Typescript compilation. Finally you can assemble Docker/OCI images using rules_docker and js_image_layer which prevents the need for Dockerfiles at all, creates images with just exactly the dependencies needed (instead of the huge node_modules base layer which would need re-generating and downloading anytime a single dep changed) and better yet can be built pretty much instantly in most cases because it doesn't run Docker to do it.
[+] moffkalast|3 years ago|reply
It's built in Rust and does not have full Rust language support? ... how?
[+] throwawaaarrgh|3 years ago|reply
> For example, configuration is in YAML, not a proprietary syntax.

This makes me sad. A product designed to be developer friendly and automate things should create a parser and custom config language, or at least borrow one. The whole reason there are so many is they are built for purpose to make a specific task easier. If you need features like templating and logic, you can slap in existing solutions like Jinja2 and Go templates. YAML is not a configuration format, it's a data serialization format. If you don't want to write a parser, use TOML.

My final note is: don't reinvent the wheel. There is a fully open source alternative to Drone.io (I can't remember the name) that forked a while ago. Drone is the best CI system for developers, hands down. Build on the open source codebase, build some proprietary extensions and a new UI, sell your product there. It's proven (because Harness now owns it) and you can help build the existing open source product at the same time.

Otherwise, I have no need for yet another build system. All build systems are pretty much the same to me (except Jenkins, which is pretty much the tenth circle of Hell) and I'd much rather use a fully open source one that I could pay for support + extensions for at work. Maybe you could make a better Jenkins?

[+] duped|3 years ago|reply
I'm supremely disappointed to see another service using YAML to configure task running. I do in fact need a real programming language to do this, and copying others in this vein is inheriting mistakes and not picking a battle-tested solution.

What you will find is the vast majority of your configurations will invoke a make.sh script that does everything that you want to support in your system.

[+] Aeolun|3 years ago|reply
Yeah, I’m kind of aligned with this. Github workflow definitions would be much more pleasant to write if they were plain Typescript.

Feel like any data definition language eventually bolts on a shitty form of templates.

[+] mileswjohnson|3 years ago|reply
Can you speak to what kind of "functionality" you need a language for? Are you referring to Starlark-like files?
[+] andrew_|3 years ago|reply
Conversely, if moon required code instead of config, I wouldn't give it a second look. Ecosystem needs vary greatly.
[+] quickthrower2|3 years ago|reply
Apologies if this sounds reductive but can I think of it as an open source CircleCI?

The thing that annoys me most about CircleCI, TravisCI, Github Actions and Appveyor is that there is no simple way to run the same thing locally to debug or test workflows without creating either git history mess or temporary hacks like taking off branch restrictions in the yaml.

[+] aidanhs|3 years ago|reply
(for context - I'm not interested in first class node support)

This seems pretty cool. I particularly like how 'gradual' it seems to be relative to things like Bazel, i.e. you can take some shell scripts and migrate things over. I did have a play and hit an initial problem around project caching I think, which I raised at [0].

One comment, from the paranoid point of view of someone who has built distributed caching build systems before is that your caching is very pessimistic! I understand why you hash outputs by default (as well as inputs), but I think that will massively reduce hit rate a lot of the time when it may not be necessary? I raised [1].

Edit: for any future readers, I spotted an additional issue around the cache not being pessimistic enough [3]

As an aside, I do wish build systems moved beyond the 'file-based' approach to inputs/outputs to something more abstract/extensible. For example, when creating docker images I'd prefer to define an extension that informs the build system of the docker image hash, rather than create marker files on disk (the same is true of initiating rebuilds on environment variable change, which I see moon has some limited support for). It just feels like language agnostic build systems saw the file-based nature of Make and said 'good enough for us' (honorable mention to Shake, which is an exception [2]).

[0] https://github.com/moonrepo/moon/issues/637

[1] https://github.com/moonrepo/moon/issues/638

[2] https://shakebuild.com/why#expresses-many-types-of-build-rul...

[3] https://github.com/moonrepo/moon/issues/640

[+] mileswjohnson|3 years ago|reply
Thanks for the feedback and prototyping with it immediately! Always appreciated to get hands on feedback.
[+] yewenjie|3 years ago|reply
A few months ago I tried moonrepo and couldn't get it to work. IIRC, it tries to bring its own nodejs and has no option for using the system provided one - this breaks entirely in NixOS as Nix's nodejs is patched for the lack of FHS.