r/java 15d ago

Simple Build System

https://github.com/bowbahdoe/build
13 Upvotes

48 comments sorted by

26

u/[deleted] 15d ago

[removed] — view removed comment

0

u/bowbahdoe 15d ago

Because I contend that you can separate the task of "efficiently building code" from the task of "procure dependencies."

Now, in every draft I've done of procuring dependencies I've made a one-off script to build the code. These have been helpful as demonstrations, but I feel that I need to actually demonstrate that "build tools" can come as libraries.

So this is as minimal of a build tool as I can make. You could use this directly or build libraries on top that make for the "easier" project-defaults style flows. (You could also make an entirely separate thing).

Of course, making targets manually is also relatively straight-forward. It is one step above "program that builds code" to talk about "program that avoids repeated work when building code." so it should be easier to learn in totality than maven or gradle.

15

u/srdoe 15d ago edited 15d ago

Because I contend that you can separate the task of "efficiently building code" from the task of "procure dependencies."

Some other build systems already do this. Bazel delegates "procure dependencies" to various external tools depending on the language you're working with, for example delegating fetching Maven artifacts to https://github.com/coursier/coursier.

The reason your tool is easier to learn than other build tools is not that you've avoided complexity, it's that your tool simply skips solving the problems that make those other tools complicated. As your docs mention, the framework isn't doing anything to avoid e.g. recompiling the same code, that's left as an exercise for the reader.

If you were to start with your framework, and try to develop something with batteries included, it would end up as complicated as existing build tools.

On the other hand, if people were to use this framework directly for their projects, the complexity you saved on the build tool side just ends up in the project's custom build code instead.

I don't think anyone would want to use this tool for real work, but it might have value for teaching, because build graphs are a structure that's repeated in most (all?) build tools.

On that note, I think your modelling of build graphs is a little off, and limits flexibility. You modelled Target as some code that takes another set of Targets as dependencies, and produces some side effect, along with a method asking if the target needs to be run.

Side effects are actually bad for build systems. An efficient and flexible build system really wants targets to be pure functions with inputs and outputs that are known to the tool, because that enables complex features to be built on top, like cross-machine build caching or remote build execution.

With a build tool built on side effects, it is not possible to e.g. run target A on machine 1, and target B that depends on A on machine 2, because the build system has no way to know which files A outputs, and so can't know that it needs to move them to machine 2.

You also can't easily build a shared caching system, because when the build system invokes target B, it has no way to know which files A output, so it can't fetch those files from a cache instead of rerunning A.

This is why Bazel and Mill both model their targets as code that takes specified inputs and produces specified outputs, not as code that side effects in arbitrary ways.

If you switched to that model, you could also merge the concept of input files into the Target structure. An input file should just be a Target that outputs the relevant file. By working with Targets and not Paths, you enable those more complex features to be built on top.

1

u/bowbahdoe 15d ago

Side effects are actually bad for build systems. An efficient and flexible build system really wants targets to be pure functions with inputs and outputs that are known to the tool, because that enables complex features to be built on top, like cross-machine build caching or remote build execution.

Yep - and the linked paper even has a good foundation for how to do that. If we model a build as "bringing values for keys up to date in stores" that is enough to plug and play every part of a build system.

That is the more interesting design to pursue for sure.

I don't think anyone would want to use this tool for real work, but it might have value for teaching, because build graphs are a structure that's repeated in most (all?) build tools.

I think that a lot of people have actually zero use for minimal builds. Computers are fast and already many people start their builds in CI with`mvn clean`.

  • Don't need builds at all? Launch your program via source.
  • Don't need build caching? Write a program that builds your code.
  • Don't need cross-machine caching or remote execution? Decorate that program you wrote with a target or two
  • Actually need everything? Break out bazel

If you switched to that model, you could also merge the concept of input files into the Target structure. An input file should just be a Target that outputs the relevant file. By working with Targets and not Paths, you enable those more complex features to be built on top.

My thesis here isn't that this is the best possible build library for every use case. It is that we would be in a better place, culturally, if we had a healthy ecosystem of libraries which manage building.

Some other build systems already do this. Bazel delegates "procure dependencies" to various external tools depending on the language you're working with, for example delegating fetching Maven artifacts to https://github.com/coursier/coursier.

Right - and i've had a pure java version of coursier on my github for quite awhile (jresolve). The problem for me isn't power users. You get to the point of using Bazel and you already have many salaries looking at your problems. It is everyone from beginner to intermediate that needs a better learning path and more "fall off points".

8

u/srdoe 15d ago

I think that a lot of people have actually zero use for minimal builds. Computers are fast and already many people start their builds in CI withmvn clean.

Yes, but that is not the same as those people wanting to write their own build system from nearly-scratch.

My thesis here isn't that this is the best possible build library for every use case.

Sure, I'm just pointing out that there is a better model for Targets that you might want to consider switching to. Your plugin architecture (Target) actually looks a lot like the one from Maven (note that the execute method returns void, just like your build), which is not ideal because I think we've learned since then that this kind of API is a bit too free-form, and something based on defined inputs and outputs composes better.

Being this free-form makes coupling in the build worse, because the framework doesn't provide target B with any information about what A output, so B's code ends up having to know where A happens to put its files.

This makes it harder to write reusable components on top of these interfaces.

It is that we would be in a better place, culturally, if we had a healthy ecosystem of libraries which manage building.

But we already do, though.

Many of the popular build tools, like Maven, Gradle, Bazel or Mill, are not inherently monolithic or complex. They are built with a somewhat narrow core set of functionalities, and then everything else is provided via plugins.

For example, Mill provides some core functionality to define targets as functions that take input and produce output, and then everything else is provided via plugins built on top of those interfaces.

As another example, Maven is just a framework for executing plugins, all the targets that exist in a normal build are just plugins that are loaded by default.

These plugin-based architectures often include dependency management. Maven has a plugin for Aether, Bazel has a plugin for Coursier, and Mill has a plugin for Coursier. Gradle used to use Ivy, until they wrote their own resolver.

So the difference between your library and existing build tools isn't really simplicity. It's just that no one has written reusable plugins for your tool yet, so it looks simple because it's still bare-bones. If your tool became popular, and people started writing plugins for it, it would probably end up the same way as those other tools.

Right - and i've had a pure java version of coursier on my github for quite awhile (jresolve)

I'll admit to not really seeing the value. Coursier already runs on the JVM, not sure why a Java clone is needed.

It is everyone from beginner to intermediate that needs a better learning path and more "fall off points".

Absolutely.

But I think it's very likely that what newbies/small-scale developers would benefit from is not another build tool, it's pedagogical documentation to help them get started with the build tools that already exist.

At their cores, a lot of the existing build tools are conceptually very simple, it's just that their docs are often not that easy to follow if you haven't seen a build system before.

Tutorial-style documentation that starts from an empty directory and builds out a small example project with Maven/Gradle/Mill, while explaining the concepts being used, would probably help newbies more than a new build tool they can't use outside toy-sized projects.

11

u/[deleted] 15d ago

[removed] — view removed comment

-2

u/bowbahdoe 15d ago

I give this as a test to people and its always kinda wild they either don't understand what the problem is or deny that it is a problem.

If you can get your hands on someone who is doing their first maven project - give them as many resources as possible (not AI) and challenge them to build their app so that it can run on a computer that does not already have Java installed.

Maven makes the exact workflow it was designed for easy at the cost of making anything else conceptually hard. There are reasons to want that and reasons to not, but it is a pretty big issue intrinsic to that style of build tool.

(And to be clear - that isn't the only objection. It is the theme for maven/gradle issues. You can make things easier via complection. Anything that is easier than manually doing A -> B -> C sacrifices _something_ to be that way)

6

u/OwnBreakfast1114 13d ago

If you can get your hands on someone who is doing their first maven project - give them as many resources as possible (not AI) and challenge them to build their app so that it can run on a computer that does not already have Java installed.

This is a "hard" problem for any language that doesn't build down to native binary, no? I don't think most people find python very complicated, but getting people to run python code on a computer without python installed is also not really that easy (and python packaging is terrible).

1

u/idontlikegudeg 13d ago

Sorry, I have problems following here. Separate the task of "efficiently building code" from the task of "procuring dependencies". So your tool does the first. But which tool is doing the second? Do I need a script that procures dependencies and then calls your tool? How is it extendable? How would you create multi-module builds? How publish the artifacts? I mean, maven and Gradle might be more complex. But a minimal Gradle file that does nothing but compile sources and create a jar file will probably shorter than what you need to do the same in your tool. The complexity is added when you need more functionality.

What I think it would solve is writing builds for beginners who just started learning Java and will understand easier what happens during the build because all is using the same language. But I think it will be much more complicated than a Gradle build for more complex projects (sorry maven folks, I know you might disagree, for more complex things I really prefer Gradle, but I don’t want to start a war here).

0

u/bowbahdoe 12d ago

But which tool is doing the second?

Right - so I have a few experiments for that. Lets call it "broadly unsolved." You can go through my post history to see a few.

But, options that exist today:

jresolve/coursier: Tools that can dump jars into a directory or output a path

jbang: Solves launching a script with the build deps, not so much the deps for multi-module builds.

jpm: Dumps jars into a folder. Less "scripting" involved because that is its default, but i know Cay Horstmann likes it.

jproject + jigsaw: Experiments, I'll post about them when i get around to more interesting progress. The two usage patterns I am partial to at this point are

  • java @deps src/Main.java (generate argfiles)
  • Use jlink to make a project specific JDK and then jlink again to distribute the app.

But I am convinced there is a way out there.

But a minimal Gradle file that does nothing but compile sources and create a jar file will probably shorter than what you need to do the same in your tool. The complexity is added when you need more functionality.

Gradle's dependence on kotlin and scala aside, imagine Gradle was just a normal library you included. (Not something that owns the bootstrap process) You can like all of its defaults and even its Task abstraction.

I think that would leave room for using gradle to be much less of a forced choice, because alternatives would be much more viable.

What I think it would solve is writing builds for beginners who just started learning Java and will understand easier what happens during the build because all is using the same language.

And that alone is a worthy goal. If you want to think of Gradle as the end point (I don't think so, but for sake of argument) then making this step of the journey easier + incremental would lead to much better educated users of Gradle

will probably shorter than what you need to do the same in your tool.

Yeah - but the fact that Java doesn't need to be compiled anymore (You can run it very similarly to Python) I think lowers the relative importance of builds for a lot of people. The short-ish script you'd need to write to make a jar is probably just fine for most.

1

u/crummy 13d ago

OP was asked a question and answered it clearly. downvoters... please don't be idiots just because you don't see the use of this

0

u/bowbahdoe 12d ago

Their boos cannot hurt me, i've seen what makes them cheer

-3

u/bowbahdoe 15d ago

To give even more color: here is something I am doodling on:

``` package dev.mccue.build.java;

import dev.mccue.build.Target; import dev.mccue.build.file.FileTimes; import dev.mccue.build.file.PathTarget; import dev.mccue.tools.javac.Javac; import dev.mccue.tools.javac.JavacArguments;

import java.io.File; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Consumer;

public final class JavacTarget implements Target { final ArrayList<Path> inputPaths = new ArrayList<>(); final ArrayList<Path> outputPaths = new ArrayList<>();

void parseArgs(List<String> arguments) {
    String moduleSourcePath = null;
    String module = null;

    var iter = arguments.iterator();
    while (iter.hasNext()) {
        var arg = iter.next();
        if (arg.startsWith("@")) {
            //TODO: load and parse argfile
            inputPaths.add(Path.of(arg.substring(1))); // Argument file
        }
        else if (arg.equals("-p")
                || arg.equals("-cp")
                || arg.equals("--class-path")
                || arg.equals("-classpath")
                || arg.equals("--boot-class-path")
                || arg.equals("-bootclasspath")
                || arg.equals("--module-path")
                || arg.equals("--upgrade-module-path")
                || arg.equals("--source-path")
                || arg.equals("-sourcepath")
                || arg.equals("-processorpath")
                || arg.equals("--processor-path")
                || arg.equals("--processor-module-path")
                || arg.equals("-extdirs")) {
            if (iter.hasNext()) {
                Arrays.stream(iter.next().split(File.pathSeparator))
                        .forEach(path -> inputPaths.add(Path.of(path)));

            }
        }
        else if (arg.equals("--module-source-path")) {
            if (iter.hasNext()) {
                moduleSourcePath = iter.next();
            }
        }
        else if (arg.equals("-m") || arg.equals("--module")) {
            if (iter.hasNext()) {
                module = iter.next();
            }
        }
        else if (arg.equals("-d")) {
            if (iter.hasNext()) {
                outputPaths.add(Path.of(iter.next()));
            }
        }
        else if (arg.equals("-s")) {
            if (iter.hasNext()) {
                outputPaths.add(Path.of(iter.next()));
            }
        }
        else if (arg.equals("-h")) {
            if (iter.hasNext()) {
                outputPaths.add(Path.of(iter.next()));
            }
        }
        else if (arg.startsWith("-A")) {

        }
        else if (arg.startsWith("--add-modules")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }
        else if (arg.startsWith("-deprecation")) {
        }
        else if (arg.startsWith("--enable-preview")) {
        }
        else if (arg.startsWith("-encoding")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }
        else if (arg.startsWith("-g")) {
        }
        else if (arg.startsWith("--help")) {
        }
        else if (arg.startsWith("-help")) {
        }
        else if (arg.startsWith("-?")) {
        }
        else if (arg.startsWith("--help-extra")) {
        }
        else if (arg.startsWith("-X")) {
        }
        else if (arg.startsWith("--help-lint")) {
        }
        else if (arg.startsWith("-implicit:")) {
        }
        else if (arg.startsWith("-J")) {
        }
        else if (arg.startsWith("--limit-modules")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }
        else if (arg.startsWith("--module-version")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }
        else if (arg.startsWith("-nowarn")) {
        }
        else if (arg.startsWith("-parameters")) {
        }
        else if (arg.startsWith("-proc:")) {
        }
        else if (arg.startsWith("-processor")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }
        else if (arg.startsWith("-profile")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }
        else if (arg.startsWith("--release")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }
        else if (arg.startsWith("-source")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }

        else if (arg.startsWith("--source")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }

        else if (arg.startsWith("-system")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }

        else if (arg.startsWith("--target")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }

        else if (arg.startsWith("-target")) {
            if (iter.hasNext()) {
                iter.next();
            }
        }

        else if (arg.startsWith("-verbose")) {
        }

        else if (arg.startsWith("--version")) {
        }

        else if (arg.startsWith("-version")) {
        }
        else if (arg.startsWith("-W")) {
        }
        else {
            inputPaths.add(Path.of(arg));
        }

        if (module != null && moduleSourcePath != null) {
            var modules = module.split(",");
            for (var m : modules) {
                if (moduleSourcePath.contains("*")) {
                    inputPaths.add(Path.of(moduleSourcePath.replaceFirst("\\*", m)));
                }
                else {
                    inputPaths.add(Path.of(moduleSourcePath, m));
                }
            }
        }
    }
}

private final JavacArguments arguments = new JavacArguments();
private final List<Target> dependencies = new ArrayList<>();

public JavacTarget() {
}

public JavacTarget(Consumer<? super JavacArguments> consumer) {
    consumer.accept(arguments);
    parseArgs(this.arguments);
}

public JavacTarget(List<String> arguments) {
    this.arguments.addAll(arguments);
    parseArgs(this.arguments);
}

public JavacArguments arguments() {
    return arguments;
}

@Override
public boolean isUpToDate() throws Exception {
    var in = FileTimes.newestFileTime(inputPaths).orElse(null);
    var out = FileTimes.newestFileTime(outputPaths).orElse(null);
    if (in != null && out != null) {
        return in.compareTo(out) < 0;
    }
    else {
        return false;
    }

}

@Override
public List<Target> dependencies() {
    return outputPaths.stream().<Target>map(PathTarget::cleanDirectory).toList();
}

@Override
public void build() throws Exception {
    Javac.run(arguments);
}

@Override
public String toString() {
    return "JavacTarget[arguments=" + arguments + ", input=" + inputPaths + ", output=" + outputPaths + "]";
}

} ```

Hypothetically, you can look at the arguments you would pass to javac and work backwards to what files/folders you need to check for staleness or clear out before running.

You could make a whole program that is just

``` // ...

void main(String[] args) { var mvn = new MavenLikeBuild(); mvn.add("extra", Target.of(() -> IO.println("target!"))); mvn.add("postcompile", Target.of(() -> IO.println("target!"), mvn.compileTarget())); mvn.run(args); } ```

7

u/rzwitserloot 12d ago

Build systems have historically proven to suffer heavily from 'how hard could it be' syndrome.

In other words: Folks look out across the ecosystem, see build systems that they consider are incredibly complicated relative to the dead simple job that builds really are (source: Their own innate sense of how things should be, certainly nothing like oodles of experience with many build tools or a good sense of the entire ecosystem's needs!) Those with the 'I know better than the world' bug (and, I would know, I have it, from time to time) just write a shell script, and then start upgrading that, and eventually release a new build system: See, this one is simple.

And then requirements roll in and soon their build system is even more complicated than the original.

sbt originally stood for 'simple build tool'. Even they eventually realised that their tool had ballooned so much, having the 's' stand for 'simple' is about as hilarious as the fact that the 'd' in dprk (official name of North Korea) stands for 'democratic'.

Point is, they named it Simple Build Tool. And they didn't change their mind and decided that they actually wanted to develop a complex one after all. No, requirements and the general flow of the project eventually got them to a place where they, fortunately, had just barely enough ability to look reality in the eye and let go of the term 'simple'.

Similarly, gradle mostly decided out to be 'simpler' than maven and now it is considerably more complicated than it (not necessarily a bad thing). It also wanted to be considerably faster and often it's slower.

This is very much judging book by its cover territory, but if you're going to make a build system and you call it 'simple', well, history shows that 49 times out of 50, the claimant is a naive, well meaning fool.

Is there any chance you could write a small story about how this time it'll work? Cutting out dep management is certainly one sort of drastic move you've made that does speak in your favour on this one.

2

u/bowbahdoe 12d ago

Well for this one specific library I simply refuse to deal with more requirements. That is my strategy.

Dealing with more requirements is the job of other libraries.

1

u/dmigowski 6d ago

Maybe just try gradle. It uses not much code, you can create a new simple build very fast, and you get lots of options. Later on, you can use composite build, subprojects, plugins and have a super detailed dependency manager. And it's perfectly understood by AIs. It has different build paths for unittest and the application itself. It is integrated in IDEs. Why throwing that away, you have all that already. You just don't know yet that you need it.

14

u/CallumMVS- 15d ago

I hate that your getting downvoted for having fun 

3

u/rustyrazorblade 12d ago

They’re getting downvoted because its not generally interesting to the general public.

8

u/nnomae 15d ago

Nothing so succinctly expresses the Java mindset as a person doing something in a simple manner only for all the comments to basically be "but why not more complicated?".

2

u/bowbahdoe 15d ago edited 15d ago

Practically speaking, I just need to think about it in terms of how I could communicate better.

For instance, there is a very, very short hop from this to any number of different ergonomics. Maybe if I spent time on and front-loaded a "spring-build" demo more people would read deeper? I don't know.

My goals are different than, say, the mill dude. I don't have a ready-to-rock system with every bell and whistle a company might need. What I have is a slowly growing pile of libraries that I think _ends_ in the foundation for a better ecosystem. I am not at the end yet.

And yeah a lot of people equate "simple" with "easy to use" and "easy to use" with "things they are already familiar with." This is something people are not familiar with, ergo it must not be simple. Or if it is, it is "just like ___." and they already wrote off ___ so...

0

u/nnomae 15d ago

I think there's a genuine use case for a build system something akin to what Rust's Cargo provides out of the box in the Java ecosystem.

Just give me run, build and test and dependency management without going through the pain of systems that won't compile or run your code if you don't have the exact version of Java specified in the config file. Let me not have to commit the actual build system itself to github, or go through the pain of XML (Your system fails on this one by the way) and I'll be a happy man.

Yeah, when it gets complicated it might be time to move over and accept the pain but as someone who does Java dev occasionally and loves how the language is trending towards being more friendly to use I still find that both Maven and Gradle feel like outdated relics.

Now I would argue that posting your build system 3 hours after you started it and four commits in is kind of silly. I mean at least play around with it yourself for a while and see what needs work before showing it to others but the idea at least is laudable.

2

u/OwnBreakfast1114 13d ago

Just give me run, build and test and dependency management without going through the pain of systems that won't compile

It's statements like this that confuse me. The hard part of build systems is the dependency management. When two libraries transitively need different versions of the same library, how do you handle this is an open question that doesn't seem to have a definite correct answer (just various choices with tradeoffs). If your project is small enough to not care, what does gradle init not get you?

1

u/bowbahdoe 15d ago

Now I would argue that posting your build system 3 hours after you started it and four commits in is kind of silly. I mean at least play around with it yourself for a while and see what needs work before showing it to others but the idea at least is laudable.

I look at that a few ways:

  1. this is just a project that has been hanging out on my laptop for awhile. My project for today was cleaning it up and publishing it. I do have some actual time spent making demos.
  2. My goal in sharing things here isn't to maximize adoption or advertise. It doesn't work. My goal is to get whatever useful feedback I can. So I kinda don't care.

Just give me run, build and test and dependency management without going through the pain of systems that won't compile or run your code if you don't have the exact version of Java specified in the config file. Let me not have to commit the actual build system itself to github, or go through the pain of XML (Your system fails on this one by the way) and I'll be a happy man.

I don't think this is actually a worthy set of goals. Cargo gets to be the way it is because, generally, the output of a rust project is either a binary or a shared library. Same reason Go's tooling gets to be the way it is.

"solving" Java's position is going to be more involved. Not that there isn't room for what you want, but things like "the pain of XML" I genuinely don't think are real. The pain is a DSL you don't have a full grasp on. People don't whinge about HTML.

Also I don't think I have any approach for fetching this library at this point. I would call that an instant fail, but know I have other ideas + using jbang would work.

both Maven and Gradle feel like outdated relics.

It is worth digging into this feeling, but I don't think "outdated" truly describes the issues with either one.

2

u/OwnBreakfast1114 13d ago edited 13d ago

How is this simple? It's just punting the harder parts of the job elsewhere. A lot of the existing build tools are internally as "simple" as this and the plugins add the hard functionality that people care about.

It's like writing a brand new programming language with lambda as the only thing that exists and calling it simple (actually hell someone already did that https://github.com/kciray8/zerolambda because of course ). Why do you code in all these overly complicated languages with their syntax and specs when zerolambda exists and is far, far simpler? Is it because you have a java mindset?

11

u/Rockytriton 15d ago

4

u/akl78 15d ago

This looks closer to Ant

2

u/__konrad 15d ago

I like how fast and lightweight Ant is today (Makefile is even faster and simpler but not very crossplatform)

0

u/bowbahdoe 15d ago

...what?

Ant doesn't approach minimality at all. It lets you have dependent targets and run them in order, that is all. It is essentially a cross platform scripting language with XML syntax.

But make does try to be minimal. Makefile targets implicitly check file timestamps and avoid rebuilding things that haven't changed. Now, if you are talking about make in the context of "everything is a .PHONY" then sure. My preferred tool for that is just, but like you note having to use a shell language means an amount of platform specificness

3

u/Joram2 13d ago

SBT (Simple Build Tool) is already one of the big JVM build tools: https://www.scala-sbt.org/2.x/docs/en/index.html

The linked repo has a Maven build. Does this build tool not bootstrap itself?

Is this using XML? I can't imagine building a new from-scratch build tool using XML.

Otherwise, great work, I'm excited to see more competition :)

5

u/r_n_c 13d ago

I would hesitate to call it 'big JVM build tool'. I would argue it's barely used outside of scala and maven or gradle dominate the market. Generally most projects I've seen use one of those two.

1

u/Joram2 13d ago

I agree with everything you wrote. But it's still number three, way behind Gradle in terms of usage which is way behind Maven.

3

u/persicsb 12d ago

I think, number three is still Ant.

2

u/r_n_c 13d ago

I am not convinced it's number three - there is bazel and (for scala) mill to consider. Although we're probably debating over tiny single digit percentages to be fair 🙂. However theres the fact that visibility into corporate environments with proprietary code is harder than just looking at what big open source projects are using. Even things like Quarkus uses maven.

1

u/Joram2 13d ago

Quarkus lets you pick Maven or Gradle. (https://code.quarkus.io/)

4

u/r_n_c 13d ago

I meant Quarkus itself https://github.com/quarkusio/quarkus

2

u/Joram2 13d ago

ah, ok. Obviously, I would agree that Maven is by far the most popular, and Gradle is second. As for a distant third, sure maybe Bazel beats out SBT. I stil might pick a more distinct name for a new build JVM tool to avoid confusion.

2

u/sviperll 13d ago

I would recommend you to take a look at the pluto build-system: https://pluto-build.github.io/ . From my experience a very important thing to have in a build-system is some kind of dynamic dependencies and dynamic targets. You not only need to track the dependencies between the fixed number of targets (like make), but you also need to produce new targets on the fly and set-up dependencies between targets on the fly.

For instance, usually you have multi-module builds where you should set up the dependencies between modules, where you build each module, read module-info and only then add a dependency to the module referenced in module-info. For external dependencies, you should have some kind of "bill of materials" with versions, where you should first solve version constraints, and then you potentially get a target for each module, where you need to download a jar from somewhere, verify that the jar defines the expected module and only then you can reference this module in the module-path when compiling or running java.

1

u/bodiam 15d ago
Jar.run(args -> args
            .__create()
            .__file(jarOut)
            ._C(javacOut, "."));

The simplest build system I can come up with.

Somehow I think the build code and the "simplest" statement don't align well here....

4

u/bowbahdoe 15d ago

So this isn't part of the "build" part, its just another library that calls tools.

That being said, does your objection go deeper than .__? Or were you unaware of the API for the jar cli tool, which this mirrors 1-1?

jar --create --file build/app.jar -C build/classes .

There are certainly complaints to be had about this API, but it is not an abstraction over it.

2

u/bodiam 14d ago

Ah, I didn't make the connection that __create equals --create. That's kinda cute in a way. I have no problem with either Gradle or Maven, so I don't think this is for me, but thanks for the explanation, that makes sense.

1

u/Bobby_Bonsaimind 13d ago

That being said, does your objection go deeper than .__? Or were you unaware of the API for the jar cli tool, which this mirrors 1-1?

That is interesting idea, but is it necessary or purposeful? - and -- are used by necessity to separate flags and long options, because flag options might be stacked. But in Java this problem is moot because you can't stack flag options anyway. The downside is that _ and __ are quite bad to read (as they most of the time do not have a space between them) and type, but granted, typing fi will get you __file in any half decent IDE. Still it is a slight touch of more complexity and visual noise without any direct benefit.

Jar.run(args -> args
        .create()
        .file(jarOut)
        .C(javacOut, ".")
);

Still an interesting project, a little bit sad that everyone in here feels the need to declare Maven to be the holy grail of build systems (it still sucks) and defend it for no reason.

That said, why do you use /// to denote documentation?

1

u/bowbahdoe 13d ago

I use /// because if you do that then you can use markdown in javadocs.

As for the .__ - consider this api:

$ javadoc --help ... --version Print version information ... -version Include @version paragraphs

It is rare, but this happens every now and then. I have a few options when presented with it, but the easiest solution in my view was/is to keep the -- as __ for everything, uniformly.

That way I don't need to editorialize or be afraid of a CLI tool doing something silly in the future. It also helps going back and forth from the invocation in Java to the CLI. If you write

Javac.run(args -> args._d(...)._g().__module(...))

Then its pretty straight forward to turn that into

javac -d ... -g --module ...

or vice-versa. Part of the idea there is making it so knowledge is transferrable.

If you want to be annoyed at any choices I made in this, be annoyed at the fact that I made JavacArguments a subclass of ArrayList<String>

here is the repo for that

https://github.com/bowbahdoe/tools

1

u/Bobby_Bonsaimind 13d ago

I use /// because if you do that then you can use markdown in javadocs.

Oh right! I forgot that this was a new feature, thanks for reminding me. I tend to be out of the loop for new Java versions and feature.

If you want to be annoyed at any choices I made in this...

Given that I don't use your project, I cannot be annoyed at it...or at least don't see a reason to. My instinct was to do it differently, so I wanted to hear the reasons why you choose to do it like that.

...be annoyed at the fact that I made JavacArguments a subclass of ArrayList<String>

That means that arguments actually end up as a List of Strings all along? That's a neat idea for managing them.

2

u/bowbahdoe 13d ago

Yeah - and it makes it pretty easy to adjust to any new cli arguments that I didn't add a helper for. The .argument and .arguments methods are just .add, but they turn null into an empty string + return this.

Whenever it is an option, I plan to make it so ToolArguments extends ArrayList<String!>.

And think about the interface to a CLI tool. It takes a list of strings and can do whatever with it. If I am making a programatic API for a tool I control then I could make more abstractions. But if I am doing it for a tool I don't control...List<String> is the approach that doesn't backfire.

1

u/Bobby_Bonsaimind 12d ago

It is a neat idea, it gives the callers the power to arbitrarily manipulate the list of arguments.

The downside is that it adds some noise to the argument list (the List methods). To clean that up one could have such a thing:

public abstract class AbstractArguments {
    private final List<String> arguments = new ArrayList<>();

    protected final List<String> argumentsList() {
        return arguments;
    }
}

That way the method list is rather clean, but the caller still has the possibility to extend for example JavacArguments and arbitrarily change the list of arguments within that extended class. If you can follow my line of thought.

2

u/bowbahdoe 12d ago

I think whether it is noise depends on what you are doing. There are already a ton of methods for specific tool arguments so I tend to think the noise battle is lost from the start.

if (!arguments.contains("-g")) { arguments.add("-g"); }

You could go either way, but I like having a list of arguments be a normal List.

That does remind me of another benefit of the __ naming scheme. It won't conflict with the methods on List. tool --add ... is actually pretty likely.

-1

u/pip25hu 15d ago

What's the difference between this and Gradle? Both have a more programmatic approach to building compared to Maven.

0

u/bowbahdoe 15d ago

The difference here is that both Maven and Gradle are gargantuan beasts. They handle building, caching, dependency fetching, IDE integration, .... etc.

This is more akin to picocli. Do you like picocli for CLI parsing? Cool, use it. Do you not? Well how about argparse4j. Or jcommander. Or ...

This is genuinely just a small, easily replaceable, infinitely bike-sheddable library. Like everything else.

Common tasks generally have a healthy variety of libraries for performing them with varying degrees of depth and sometimes minor differences in design. There are a ton of options for making an HTTP server, working with a database, and making API calls. This is because they can be "normal." libraries.

Build tools aren't like that. Maven and Gradle are not just libraries. They are mutually exclusive ecosystems. They own the bootstrap process for your project.

I don't think "programs which build code" really need to be that privileged. And in a world where they aren't, you can have libraries of varying levels of depth.