The Problem with Gradle
bruceeckel.com
bruceeckel.com
But actually learning from those docs is amazingly hard. It's like "documentation theater". If you start as a naive user coming in, it keeps teasing you that its going to teach you something but you just get abstract concept after abstract concept and you STILL don't know the mechanics of how a build works. You have to kind of reverse engineer the info that Bruce describes out of the documentation. Even when you manage to hit actual code examples with detailed explanations - they are described in a way that fails to give you the generalisable understanding of the internals that you need to actually create new things that you haven't seen before.
As a developer it generates a distinctly hostile feeling of helplessness and powerlessness.
The fact that the style and language Gradle is written in has changed doesn’t help one iota.
The only projects I see reliably escaping this trap are ones with commercial backing. I'm guessing the problem is that nobody does tech writing as a hobby, so, if you want to get good tech writing, you need to actually hire a tech writer.
Gradle is backed by Gradle, Inc. [1] who provide commercial support and presumably a lot of development as well.
None the abstract concept stuff means anything to me quite yet, and it’s what I find about the most.
It’s as if my Mac user GF asked me about how to get something done in my Windows desktop and I start with the history of Microsoft and how different is their philosophy instead of telling her “Use CTRL instead of CMD and most of your shortcuts will work the same, and file explorer is your Finder”.
- Scoping is mysterious: things that you expect to be in scope AREN'T, and lots of things that you can't see ARE in scope. This makes it impossible for a beginner to look up the source code (or even, often, the documentation) that corresponds to a given thing in a Gradle file: you might realize that "ext" exists, but there's no way to tell what scope it comes from. That defeats most of the usual approaches that programmers have learned for exploring a new system. To use the UX lingo, Gradle is undiscoverable.
- Similarly, your Gradle script is interacting with things you can't see. When you write "sourceSets { ... }" you're actually calling a method on an object. What object? You can't see it, or even a reference to it, in your Gradle file.
- I came to Gradle with the definition in my head that a build tool is a thing that tracks dependencies so that it only has to rebuild things whose inputs have changed. Gradle doesn't do that at all. Many hours of searching for how Gradle detects changes were fruitless, because it doesn't even try, really. Gradle basically always runs everything, which is why it's so goddamn slow. This is because the entire dependency graph is constructed dynamically on every run, and that prevents the system from effectively retaining from run to run which steps have already been done.
Gradle has build cache since version 3.5 [0]. You probably mean the configuration phase for which caching was introduced in version 6.6. [1]
Gradle has been consistently improving the last couple of years. I’m quite happy with its speed since the v6.5 upgrade.
0 - https://docs.gradle.org/current/userguide/build_cache.html
- daemon
- build cache
- config cache
for a build tool! And it only took them more a full decade to implement them (the config cache is still experimental).
And even all those things only take it from "molasses slow" to "bearable".
Something that is not clear from the Gradle documentation is that every task must have its input and output defined (inputFile, inputDirectory, outputFile, outputDitectory property). It is absolutely essential so Gradle can infer which task has to be rerun when, for instance, a build is run. This is usually done for the ‘basic’ task (like build, test etc..) but you have to specify it when you have custom task. This can really make a difference in speed. Another trick is to use the console option (gradle build —console plain) so you will see each task that have been run and their status (if they have be runned, or if they have been skipped due to dependencies)
It’s not well-documented, as you say, and beyond that the tool doesn’t do anything to enforce or encourage correct behavior, or offer you any way to fix incorrect behavior in code you depend on.
It definitely is confusing, but while the rules to scoping are pretty extensive, they're also well defined. The trick is to know how Gradle "travels" through objects and looks for the property in class properties, extensions, extras, etc. I wrote about it [1] if you're interested.
> Gradle script is interacting with things you can't see
Not necessarily -- yes, the receiver for top-level calls (Project instance) is implicit, but you can access it via `project` property. You could prefix each top-level call in your Gradle script with `project.`, making all calls explicit.
Btw `sourceSets { }` calls the lambda on a `SourceSetContainer` instance, accessible via `Project#sourceSets` property [2], so you can get an instance of it. I suppose there might some objects that are configurable but not accessible, but nothing comes to my mind right away, actually.
> a build tool is a thing that tracks dependencies so that it only has to rebuild things whose inputs have changed. Gradle doesn't do that at all.
You might've had a bad experience with a misconfigured plugin/build or a bug. Gradle does track dependencies, and while it doesn't do that perfectly always, it avoids a lot of unnecessary work, on many layers (incremental compilation, task avoidance, task caching). As a general rule, when running the same task(s) twice, the second invocation shouldn't execute any taks.
> This is because the entire dependency graph is constructed dynamically on every run It's true that configuration/task graph creation is still happening on every invocation (this will change with Configuration Cache [3]). But it's definitely not accurate that this prevents Gradle from knowing which steps have already been done. Again, I fully believe it didn't work for you, but it doesn't mean Gradle doesn't do that.
I'm not trying to say Gradle doesn't have flaws. In fact, I think your comment proves the points from the article. Unless one invests in learning how Gradle works, it's impossible to spot e.g. why the build isn't doing any task avoidance, or understand scoping. It's rather unfortunate, because after that initial investment Gradle is a pretty great tool to work with.
[1] https://medium.com/tooploox/where-do-gradle-properties-come-... [2] https://docs.gradle.org/current/dsl/org.gradle.api.Project.h... [3] https://docs.gradle.org/current/userguide/configuration_cach...
And keep in mind this is just for the build tool, someone using Gradle probably already has to juggle a million other details, most likely much more important than Gradle (business concerns, main tech stack concerns, etc.).
Gradle is practically consultingware.
Btw, when was the last time you used Gradle? For what type of projects and are you currently using it in prod?
Yes. Maven.
If Gradle were more of a library, and a Gradle build script were just a Groovy or Kotlin script that calls into that library, everything might be easier to understand.
Exactly how I’ve felt about Ruby or Rails for the last 12 years. There’s always something magic in scope you can’t see.
And duck typing all the things means you just have to trace through the code to actually find out what that <thing> parameter really is and what the function is expecting.
This is the crux of the problem, and is a problem with many libraries, configuration systems, languages, etc.
The author of said tool wants it to be as widely used as possible, so he optimizes for the 100%, going so far up the flexibility curve that it becomes incredibly difficult to do 80% of the things you want.
What these authors should be doing is optimizing for the 80%, which means an opinionated top layer that offers a single way to do everything that 80% of your users would want to do. Then if you're smart, you'll have a layered architecture (which the 80% layer rests upon) that allows the other 20% to learn about how the sausage is made in order to do their complex tasks.
With this, you have a common tool that is at least 80% easily understandable with minimal time investment.
Failure to do this gives you unfriendly systems like CMake, Gradle, OpenSSL, Postfix, and Java's time libraries.
U/X applies to code as well.
I almost never hold my phone in landscape.
It’s not just Apple, though: there’s a whole page of useful settings for Instagram’s Hyperlapse app. Guess how you open it? You tap the screen four times with four fingers. Why on earth did they build and maintain a settings page and then hide it like that?
[0] https://devblogs.microsoft.com/oldnewthing/20030728-00/?p=43...
A recent Windows update even went so far as to proudly boast about how they were adding back menu items that had disappeared in this drive for stupidification of the menus. Hardly progress.
That's why they keep linking to the Control Panel, they're not there yet. I think they'll soon be done with the System page, for example.
A good example of this approach is MSBuild + Nuke [1]. You can have as much complexity as you want in your Nuke build script (which is plain C# in a normal, debuggable C# project), but MSBuild has to exist to bootstrap the Nuke build script.
The java.time libraries are available on android starting API level 26 https://developer.android.com/reference/java/time/package-su...
That is android 11, released in September of 2020 which is not widely distributed.
Does anyone know why the delay? It looks like Java 8 language features finally became compatible through some compatibility layer in Gradle 4.
Was it because of the lawsuits?
Instead, in overthought out systems, complex things are simple, and simple things are possible with unnecessary effort.
Trying to do everything for everyone makes for very complex systems that are too abstract to be easily used.
Bruce Eckel's book "Thinking in C++ Vols 1 and 2"[1][2] was possibly my favorite book ever read. Bruce is (in my personal opinion as an observer of people with zero professional training in psychology) a depth-first learner and naturally seeks to understand how things work by looking at the layers. Building up from there allows true understanding and even mastery of the material.
This is exactly how I am as well, and the result is often that it can take a while for me to pick things up and really understand them (since I have to cover so much more material than a typical person) but my level of mastery afterward is typically very high.
My experience now from well over a decade in the industry is that most people are not this way, and also that many people who are this way don't necessarily realize it. If you feel like understanding history, layers, etc helps you understand things much better, do yourself a favor and follow Bruce Eckel's blog and read his books. Thinking in C++ 2nd edition is 20 years old now. A third edition would be wonderful. Or maybe even better, "Thinking in Rust" :-D
[1] Volume 1 available for free on Archive.org: https://archive.org/details/TICPP2ndEdVolOne
[2] Volume 2 available for free on Archive.org: https://archive.org/details/TICPP2ndEdVolTwo
Making that even worse there wasn't much out there to cover internals. The situation is much better now though. I guess that's part of price you pay to be an early adopter of the latest hotness.
> To do anything you have to know everything
This sums it up pretty well. Coming from a background of using Maven purely for dependency management and basic compilation, existing Gradle projects are incredibly unapproachable and slow. And don’t get me started on the background daemons to speed things up either.
Eventually we'll get to the GitLab CI/CD, but the existing disaster supports the status quo, so it's not a priority.
That said, learning Yet Another Language that only deals with a niche requirement is a total bore.
If we can cook it down to executing some garden-variety bash in a special context, along with some spiffy pipeline visualization, that is enough.
If Clean Project is required to build the project, why isn't that part of the build process?
When I am tasked with fixong other peoples config I usually move things to the correct location, delete the corresponding configuration qnd then everything works (better).
i took up flutter a couple of years ago, which means i am now saddled with (ugh) gradle for android builds. i am used to being able to intuit the underlying architecture of various tech things and fix them, which is utterly impossible as far as gradle goes.
not only are the error messages unhelpful, they are usually confidently incorrect. they tell you the problem is likely thing X, which in fact has nothing to do with the problem. (to be fair, this seems to be coming from whatever stuff google has added to the android build process, rather than gradle itself.)
these days, once gradle starts spitting errors, i know better than to read them. instead, i have to start thinking about when the build last worked, and what might have changed. if that path proves not to bear fruit, then it is time to trash that project completely and recreate it from scratch, which experience has taught me will take a lot less time than trying to “fix” the gradle build errors.
I nominate Groovy. Many of gradle's failures are just idiomatic groovy. The entire mindset of "friendly simplifications" that work 80% of the time, and if they give pretend that you don't notice, look, there's pretty code over there!
Even kotlin suffers from some of that (you can override a val x:Int with a val x:Int get() unless it's tied down otherwise, talk about principle of maximum surprise) but the genes from the other parent avoid the worst.
Also, I guess some people genuinely like gradual typing, although I've never found much use for it.
Nowadays, I wouldn't pick up Groovy anymore, but there was a time where it was basically that, or Java (or a different ecosystem entirely; or something like JRuby / Jython).
I would probably do Kotlin, now, but that wasn't an option when I started this.
The amount of boilerplate removed is one big win, but moreover the more functionally oriented aspects of most anything you'd do with a for-loop in java is wonderful.
And with annotations such as @CompileStatic and @TypeChecked, I am able to forgo SOME of the syntactic niceties for the compile-time binding java-comparable speed.
https://github.com/geb/geb/blob/master/module/geb-core/src/m...
So it looks mostly like Java, its just making use of Groovy's niceties and more powerful constructs in various parts to make the coding nicer.
Could you expand on this? I don't follow what you mean. Are you talking about some form of shadowing?
val x: Int // this is an immutable value
val y: Int // this is an immutable Function0<Int> that is executed each time and but pretends to be a val
get() = System.currentTimeMillis()Python getter properties are evaluated instantly on assignment. To convert them back into function-pointers like the Kotlin example above you really have to go out of your way by deep diving into inspect module. The result is a property-object which is not assignable to a int-variable, but type checking probably wouldn't go this deep as inspect naturally is very dynamic in its nature.
My guess is Kotlin does this as a way of structural typing, if it walks like a int and talks like an int it is an int.
I agree that true immutability - or code that is more obvious about avoiding side-effects - could be useful, but I don't think that was ever the goal.
The kotlin defense to this surely is that immutability happily exists in val constructor args (with the usual exception: not transitive through references), but that's purely contextual and a difference that is very hard to represent at usage site (e.g. they look exactly the same in the intellij "javadoc" view). If for some reason bean-style methods had to be represented through var/val syntax, I think that it would have been much better if read-only calculated getters were represented by a var without set(), leaving val to cases where you're guaranteed to get the same when referenced twice (value set once in constructor or some lazyness shenanigans).
It does lead though to one of the biggest lies which is "if you know Java then you already know Groovy!" - that's just a mean trick to play on people.
Groovy is all the more guilty for carrying the bad tradition forward. It's akin to somebody looking at the success of C and deciding that its macro system must be perfectly preserved going forward.
A common set of conventions in the Ruby community uses both options for optional punctiation, with context controlling which is used for a particular case. E.g.,
1. Use a single expression per line, and don't combine the line starting or ending a block with the contents, and don't use semicolons where they are unnecessary (so, semicolons are used only to separate the opening and “end” of single-line blocks.)
2. Use parens among function calls, except omit them in calls that are part of internal DSLs.
I suppose I should more accurately call that thing JSR223 + Groovy, all stored in an extensive XML file and edited through the JMeter UI..
0 - https://gradle.com/customers
(Edit: Heh, TIL rage downvotes are a thing in HN as well.)
Or Facebook's Buck.
I do not work on Android apps, but I use Gradle in semi-large web app and lots of fat jar projects. Do you happen to know if there are any perf measurements that outline Gradle is slower?
Tricks that Maven, or the other build systems never required.
Buck looks quite interesting. Thanks for pointing it out!
Gradle has Android giving it wind on its sails, because a couple of people there are really into it.
Yet even Google's own teams rather use Basel and Soong instead.
Fun fact: look up gradle in urban dictionary.
Why not have it be something with more general usefulness as well?
So increasing the default max heap size of the gradle build helped fix the issue https://github.com/quarkusio/quarkus/pull/10508/files.
It's impossible to disagree with this, but this is a realization you should have after interacting with it after 10 minutes.
I may be biased because I'd been using Groovy (Grails apps) for years before I used Gradle so the syntax was not a problem at all for me. For me, the problem with Gradle is the very poor documentation. If I want to do something more complex, I either find the answer in Stackoverflow/the Gradle message board, or I will have to look at the source code of the plugins. Creating a task for scratch is fairly simple, it's interacting with the plugins that's difficult, and while I agree with the author that excessive abstraction through DSLs doesn't help, if these DSLs didn't leak and were properly documented it wouldn't be a problem.
Another problem is that it's clear what the right conventions are. Say you want some classes to some exec task. Where should these classes live? Do you use a configuration? Create a sourceSet? Add a directory to an existing sourceSet? You just edit the classpath property in the task to point to a directory? I miss some of the rigidity of Maven, though not its XML syntax. The ability to write actual functions in Groovy (e.g. a closure implementing a file filter) is very much welcome.
Developers simply have to know too much Groovy to operate Gradle effectively.
Buildr, which i think predated Gradle, used Ruby. I'm not sure Ruby is any better than Groovy here.
Perhaps Jython would have been a good choice.
It's interesting because plenty of build systems exist written in Python but they don't get much used because Python itself is not really flexible or expressive enough for writing good DSLs - and build systems are just specialised enough that a good DSL is actually required to do them efficiently.
So we seem to be on a never-ending treadmill of wheel reinvention searching for the right compromise between these things.
Meson seems to be a counterexample.
You can get very very far with just a list of files and some config properties, if there is something complicated needed in the configuration the markup can refer to an external script.
In fact not accidentally mixing the configuration step with the execution step, as Gradle often does, can be considered a feature.
I agree that a lot of the time, you don't need Gradle's sophistication. But large, complex projects, in my experience, inevitably develop strange and unusual build needs. At that point, if your tool doesn't support them naturally, the complexity ends up going somewhere else - you have to write plugins, or standalone tools, or build script generators, or manage a lot of repetition in the build script, or multiply subprojects, etc, so you can do what you need to do.
Cargo is nice--until you have to coexist with something else that also wants to own the world of the build process.
And that build.rs script causes a LOT of heartburn when your needs get larger or you need repeatable builds.
I don't know what the right answer is, but cargo is exactly the kind of system you describe that you have to start working around when you have more sophisticated needs.
The problem is, it is presented as if you don't need to know Groovy. And its not surprising because, if everybody contemplating using Gradle was first told "You should only do this if most of your team is willing to learn Groovy to good proficiency" - hardly anybody would do that.
Now groovy is a great language IMHO but I think the way this goes down results in people hating it because they get handed a build as if they should understand it easily and then experience hours and hours of frustration as it exhibits unexpected behavior as they learn all the surprising things about Groovy (and not just Groovy but the Groovy+Gradle DSL dialect that is much weirder than plain Groovy).
The correct response to discovering the nature of Gradle is to abandon it, immediately. (Similarly Waf, and Scons.) The world does not need yet another one-or-two-language-target build system, or another build system as full general scripting language. Things work better when you don't need expertise. A build system should be just barely powerful enough to do builds, but not powerful enough to mystify. A build system should work equally well for all the languages you might find you need to use. A build system should not need to be studied to be understood: isolated examples should suffice for each need.
Similarly, the build for a project should not need to be understood: everything you need to know about what you are looking at should be right there, or (failing that) in a single, central place for the project. The build should be the least interesting or engaging part of the project; the project itself inevitably demands more attention than you can spare. A build system that demands attention is a child screaming when you need to work.
Writing new plugins is far easier with Gradle than Maven (and way way way less ugly to configure) and there are more plugins available (and usually of higher quality than their maven counterparts).
Speed is significantly better than Maven. Incremental builds gave us back 80% of our time spent sitting on maven. Reusability is much better than maven too.
There are definite footguns but if you can avoid them (which I understand requires work and itd be better if they were not there at all!) then Gradle is decent. Its no Cargo but woe is life. The few times I had to parse a file at build time to read some credentials in jenkins I've been grateful im using Gradle instead of Maven.
It would be annoying to move build tools, and plenty more of hookups are not as convenient in a bunch if cases, but when you get it going things will run faster. (So again, the bigger the project, the more likely you would like that)
A big part of the reason is that tools suck as Buck (which, like Bazel, uses Skylark as the language, which is a very limited python in essence) avoid the general programming patterns to avoid the issues described here.
> A big part of the reason is that tools suck as Buck (which, like Bazel, uses Skylark as the language, which is a very limited python in essence) avoid the general programming patterns to avoid the issues described here.
I also agree with this. My current language of choice is OCaml and the most popular build tool is dune [1], which uses an s-expression based configuration language, and its nice to have a more restrictive DSL that avoids some pitfalls of using a full blown programming language for configuring builds.
If you also use wrapper with sources (`-all` instead of `-bin` suffix in the `gradle/wrapper/gradle-wrapper.properties`) you can step into the entire Gradle source code as well
A long time ago I wrote a Maven plugin and decided to clean it up for open source. It got absolutely nowhere because after everything you describe—setting up a repo; learning the Maven API, which is one of those expansive APIs that has a lot of metaphoric semantic constructs—distributing it involved getting it into Maven Central by which point I had run out of time/mental bandwidth and never made it happen.
Compare if I had been using Gradle at the time: I would have just coded it up in Groovy, a language I thoroughly enjoy, right there in setup. There are some things I don't like about Gradle, but there's a lot it has going for it, too. Personally, I like XML and enjoy Maven for situations where existing plugins are enough—which is 99% of the time—but that lack of flexibility can be constraining when existing plugins aren't enough.
if you had the same code as a maven plugin, it would've been instantly sharable, and easily updatable centrally via a release process (as maven plugins tend to also be built with maven). Upfront work, but for long term reward.
I just used the maven groovy plugin to invoke a groovy script containing the functionality instead of a maven plugin (a few lines of code). the groovy script was part of the source code and therefore easy to tweak. A lot of maven variables from the project are accessible from the script. Yeah, it's not really nice.
Gradle also sucks, but at least you can script it much more easily.
You should not need to write a plugin!
It is Ant all over again, with Groovy slowness and game rig hardware requirements to perform in a sane way. There is hardly any Android conference without build related performance improvement talks.
fwiw I think the Android toolchain being slow doesn't help. On the same conferences someone will also speak about how the new Android Gradle Plugin is X% faster, so it's not all Gradle's fault :) That said there's plenty more Gradle can do (and is doing) to improve performance. The startup cost to configure all tasks is definitely high on my annoyances list
Maven was painful as well but it felt more consistent to me.
You know whats really sad? MsBuild proj and solution files are easier to understand!
I'm sure there's a lot of great work that's going into Gradle but I just don't understand why this is the DSL and syntax we decided to put effort into.
Ever managed to understand Gradle complex files?
There is a full application living there.
Well, there is a funny annecdote I can share.
In some presentation a guy was showing how you can implemnent some non trivial functionality for a Android app. He was very proud to demonstrate how it can be implemented in just few lines of code. Then he showed casually just for few sec the gradle file for the demo app, which was about 50 lines. We were joking how simple and easy things can be as long you are gradle magician.
Years ago, i worked on a project where it made sense to have two integration test tasks - main tests and some special separate set of tests, i forget why. In Gradle, that's trivial. In Maven, it is impossible. You would have to write a new plugin which duplicates the existing integration test plugin to do it.
Maven runs the stages of the build in a fixed order, defined by the plugin code. If you have a reason to run two stages in a different order, bad luck, that can't be done.
There are tons of things like this.
Ironically even Google own teams rather use Basel and Soong as the Gradle based builds that the Android team imposes to everyone else using the Android SDK.
So on the one hand, its the easiest way to get started, and on the other hand its a turing complete language so there isn't anything it can't do so once started, people can always keep going.
And then, yes, Maven is complex and mysterious in completely different ways but almost to the same extent as Gradle.
[1] https://docs.gradle.org/current/userguide/toolchains.html
I think that especially compared to Python with its... very fragmented landscape of dependency and version management tools, Ruby has a pretty standardised way of making sure you run everything in a reproducible environment, which maybe isn't 100% perfect, but usually works quite fine, even though it is a different approach than the gradle wrapper.
--
More details:
In Ruby, it is by now completely standard practice to use some sort of Ruby version manager (rbenv, rvm, chruby, for example - they should all work). Those version managers always can pick up the correct Ruby version from the ".ruby-version" file. This is actually an improvement over the Java situation where different people might in theory run the same project with different versions of the JDK without them even noticing (although the JVM is of course much more backwards-compatible than Ruby usually is, but it can still lead to problems). Even the gradle wrapper doesn't help you there - it assumes you already have the JDK installed.
Also, Gemfiles (dependency declaration files) typically (or maybe nowadays always?) include the version of Ruby used, and the Gemfile.lock also specifies the version of Bundler that was used.
It is standard practice to run every Ruby command in a project through "bundle exec", in which case Bundler will always check all your dependency versions. If the Ruby version is wrong (which can only happen if you don't use a version manager anyway), I think it will just refuse to run and print an error message, same if you haven't installed some dependency with the right version. If the bundler version doesn't match, I think it will at least output a warning in some cases.
So, in practice, the only true differences between that and the gradle wrapper are:
- you need to install bundler yourself (although that's just "gem install bundler")
- you need to remember to use "bundle exec" (although there are tricks to avoid that, e.g. binstubs + something like direnv)
- you might not use the "right" bundler version, unlike with Gradle - however, in several years of writing Ruby I ran into the problem that a mismatched Bundler version led to an issue, exactly once - and they've since added the warning - so I don't think it's that big of a deal
- on the plus side for Bundler, it actually enforces transitive dependency locking, unlike Gradle, for which you have to explicitly enable it - and also many other tools don't support that properly (e.g. Dependabot)
Personally, I've stuck with Gradle, even though I'm not really happy with it, mostly because of these features:
1. Composite builds - https://docs.gradle.org/current/userguide/composite_builds.h... 2. Dependency substitution 3. "local" plugins, i.e., stuff in `./buildSrc`
These features let me combine projects defined in multiple git repositories in a "workspace" project. Then IntelliJ can read all these projects, and it magically substitutes my local version over a specific version. Ergo, I can have multiple libraries, edit and debug them and and application code. When it's time to commit, I push them up in individual MRs back in GitLab - And I could swear GitLab was going to make some kind of multi-project MR possible at some point.
So, the "workspace" is generally for development, then we use GitLab CI for each project independently, for a "formal" versioned approach. It's quite nice.
In any case, I'll typically work with a pretty broad set of apps and libraries in a single workspace, typically between 15-30, and Gradle and IntelliJ handle this just fine. Our Maven projects with fewer modules never seem to get things working as smoothly ironed out - I suspect it's something wacky going on with the build.
So, while Maven allows build composition, but doesn't really have the dependency substitution concept, useful for local workflows. Also, it just doesn't seem to perform as well, especially with the IDEs we've been using. I can't explain it, haven't investigated it, but then again, I've found a solution that works.
I'm very far from a Maven specialist, though.
I suspect with Maven this would require some plugin work.
It’s when I found myself attaching a debugger to my Gradle session to debug it that I threw up my hands in disgust and said “you know what, give me XML back” and went back to Maven.
I will say, Gradles files and console output is usually easier on the eyes. And Maven seems to needlessly re-build things when Gradle does not. Still, I want to fiddle with my project, not my build scripts.
Gradle. It's a full programming language. And let's not forget there's a ray tracer in pure cmake doing the rounds.
Being cross-platform without having to drop out of the main build tool isn't necessarily a benefit, and that's part of the reason for it's complexity.
I guess, that book writing skill is more important in writing good books, than programming skill.
There were other reasons, but de-gradling was one of the main motivations for my fork, and among the first of the major changes I made. The project is an implementation of an API which was discontinued by the original developers, but initially was built using Maven.
After switching from Gradle (which the project switched to in 2014) back to Maven, build times significantly decreased and development became much more pleasant. I found Gradle to be like a speed bump slowing down development, and reverting back to Maven was like a breath of fresh air. Simple, straightforward, and fast. Maven may not be perfect, but it does the job well.
[1] An open source Bukkit server implementation, https://github.com/GlowstoneMC/Glowstone -> http://github.com/GlowstonePlusPlus/GlowstonePlusPlus
I despise it. XML seemed bad when we had Maven but at least it was a markup language.
Maven is still the best build system I've used in any language or ecosystem. It is predictable, stable, has a great community of plugins, and uses XML as it was meant to be used, which is far better than the JSON / DSL monstrosities I see with newer, "cooler" build tools.
Some of the issues I eventually figured out had to do with missing or incomplete documentation explaining some of the caveats or gotchas of Gradle constructs that one might assume should work the same way. For example, the use of old-style vs new-style code for plugins (buildscript/apply plugin vs. plugin DSL) is not always interchangeable. The new-style works at the top-level of multiproject builds but one must use the old apply plugin form in subprojects.
We've had to use Gradle to build non-java projects as well, which need to use Exec and Copy tasks to build and stage build artifacts. However, the Copy tasks, which are usually dependent on the Exec build tasks, don't always execute after the tasks they are dependent on have completed. Or rather, a Gradle project built on the command line will successfully respect those dependencies while a Jenkins job will output NO-SOURCE in the task execution step.
The problem with Gradle is that it is not as simple as Make, which I had to use in a prior position. However, successful use of Gradle does require significant more investment in learning more than you first think you might need to know about Gradle in a tutorial. It also requires significant investment in learning about Groovy, which will help illuminate how the Gradle DSL works.
For example, someone “donated” a gradle build script to an open source project I help maintain. It lasted less than 6 months before it began breaking, not because we changed stuff, but because gradle updates had changed how some things worked and a plug-in stopped working. Now “obviously” the answer is that we should have pegged the version of gradle required to use the script, but (a) I don’t even know if that’s possible (b) the donator definitely didn’t think to do that (c) who changes such things in such a production level tool during a stable release cycle?
Now the gradle people certainly seem decent, in my interactions with them, but I think there is unfortunately a strong case of Stockholm Syndrome going on there.
My first step with either gradle or maven is to install the wrapper generator, which has this effect. After adding the wrapper you invoke it via ./gradlew or ./mvnw in the project directory. The version of gradle or maven is then pinned until manually updated.
It’s not perfect - especially in terms of IDE support - but it’s crucial for keeping your CLI builds consistent across team members and in automation.
gradle wrapper: https://docs.gradle.org/current/userguide/gradle_wrapper.htm...
maven wrapper: https://github.com/takari/maven-wrapper
After years of working with gradle configs, to this day I still don't know groovy or gradle as intimately as I know other things I've spent as much time with. I wish I did, but I just don't.
I can explain to you what our configs do, but if you asked me to make it do something else it'd be straight to google for examples to mangle, a metric tonne of trial and error, and lookups for simple groovy syntax that I keep forgetting in-between the actual work I want to do.
It’s nice to know that I wasn’t the only one who struggled!
In Gradle-insanity land, you'll need a whole plugin for that. That you have to register in the buildScript.classpath closure. Wait, is it also in the plugins block of the root file ? But maybe also the modules themselves, as an apply. Or is it a plugin block too ? Oh wait, you're using buildSrc ? Well that's a whole other problem. Don't forget you need a basic .properties file in META-INF that just gives out the plugin's name so it can discover it through reflection instead of having a proper API to do so.
You would think the Kotlin scripts instead would fix it, but oh no. Not content with adding a solid 10 seconds every time gradle builds your model (which it rebuilds... Kind of when it feels like it), half of the pretended benefits of it simply do not exist. Almost all the documentation online is for the Groovy scripts, which is, obviously, incompatible with the .kts scripts. And I'm not just talking about syntax, no! Entire APIs are different. Setting a flag like `enabled = true` in groovy ? Have fun, we've added an isEnabled instead, because why not. Registering tasks? Wait, hold on, we've added new solutions in Kotlin! The existing APIs weren't batshit insane already, so let's abuse kotlin delegation. Kotlin's `by lazy {}` is nice and simple, why do we not make your write `val taskName by creating { }` ? And through some horrible logic, the name of your variable is what is exposed to Gradle. Or abusing the same thing for you to read things from a .properties file, because what is more clear than `val isFlagEnabled by project` ? Isn't it obvious that it's going to read a .properties file ? Not the values given through the -P flag when building though, because that would actually make sense. But wait! Let the gradle website explain to you the logic of lazy registered tasks. Why do you need these, you ask? Because Gradle is so absolutely terrible that having a large amount of tasks means they all get created when your daemon starts. Or when it performs anything.
Oh, right, you need a daemon because why would you not need to eat 1GB of RAM just to not have horrible startup times for a build tool? Load all the tasks, at all the times! Add a development flavor to all your modules ? lol you've just doubled your amount of tasks.
Let's add to this of course the absolute insanity that are the Android build tools. Making those work in a Makefile would be hell. aapt2, dexing... An absolute abomination of an amalgamation of bad tools (lol aapt2 silently ignoring your vector drawable overrides because it doesn't like the android:fillType attribute.) and horrible performance (aapt2 taking over a minute to process files on a medium sized project, or kapt (but that's a Kotlin problem) being awful in terms of performance.) All this to output a glorified .zip in the end. Or whatever new format's Google drug addled minds have invented to lock you in further to the Play Store and to make you give them your signing keys.
I am not a fan of Gradle. Or Android's build tools.
Not only has the Android team managed to push Gradle no matter what, they sell Android Java as the real Java.
They purposely use Android Java samples, with its half broken support for modern Java, as means to sell Kotlin in the platform, and when one points out the real differences, radio silence.
> Don't forget you need a basic .properties file in META-INF that just gives out the plugin's name so it can discover it through reflection instead of having a proper API to do so.
Not really clear what do you mean, but declaring plugin names should not be done in properties, Gradle has syntax for declaring plugin ids inside its build config. If the plugin author haven’t used it, well, that’s on their conscience. Plugin users should never ever declare anything plugin-related in meta-inf.
> And through some horrible logic, the name of your variable is what is exposed to Gradle.
This is a feature of Kotlin as a language - in it, delegates know the names of the properties they are used for. `lazy` is just the simplest use of the delegates feature, which happens not to care about property names.
> Isn't it obvious that it's going to read a .properties file ? Not the values given through the -P flag when building though, because that would actually make sense.
This is simply untrue. Extracting properties this way most certainly will read them from .properties files and from all other sources of properties on a project, including the -P arguments.
Gradle does have a lot of issues, I can talk about them for hours (there are really nasty ones where there are no non-awful workarounds, for example, the built-in `tarTree` does not support symlinks in TAR archives, and there is no way to rebase the archive contents when extracting, i.e. to specify which directory of the archive to extract from, and these issues are very unlikely to be fixed, and these two are just the surface), but too many of your examples are somewhat wrong.
Now we are stuck in a terrible situation where step 1 of any java/kotlin project is unpleasant because you have to write a long XML file (Maven) or a magical and incomprehensible build file in groovy/kts (Gradle).
That was my "Ah-ha!" moment in using Gradle.
Gradle was easy to start with, but it hides way too many things that end up biting you in the back (sourceSets, anyone?) and those build.gradle files gets way too big.
Too bad there is not much choice.
That is my path to happiness, Netbeans | Eclipse + Maven.
In any case, one of the large problems remaining is the bad discovery of stuff-it-can-do-and-how-to-configure-that, and making it too easy for devs to write imperative code when they should be staying declarative.
Also, I don't very much like how complicated some of the newer features are to use, and how much weird magic they require to be turned on / configured.
Another thing I absolutely hate is how it splatters the build scripts into half a dozen files. Personally, even for large monorepos or multi-module builds, I prefer to have one huge script file + plugins.
The strengths of Gradle, if the scripts are written properly, however are very clear to me:
- Task avoidance and therefore crazy good build speedup ofc that requires tasks to be well defined, ideally the whole project to be split up into smaller modules, and each of the build tasks being deterministic. unfortunately many devs don't understand how to do this, and some of the more exotic build steps in monorepos might not be properly deterministic (looking at you, frontend build tools)
- It can be wrapped around other langs and exotic build steps for monorepos. E.g. you can first fire up a database in a container, and run your migrations against that, then use the resulting schema to generate your database entities. In parallel, you might want to run some protobuf generation, that is then used by e.g. a Java backend and a JS frontend built that's actually using yarn in the background. There are plugins available that will wrap the yarn scripts to the Gradle lifecycles. The output of the frontend can be used to be included in the static files of your jar, which you can fire up for tests with browsers running in docker containers and doing e2e tests from the Java BE code, so you can do amazing test setups. Gradle can do all of that, with incremental builds / cached in- and outputs. Imagine having to keep all these steps in your head and running them on-demand when you think its necessary. Or running them every time no matter what (looking at you, Maven) Gradle allows me to write down all of these steps as individual, declarative tasks, making use of a vast and high-quality plugin ecosystem. Then wire up their order, again, declaratively. Once that is accomplished, I can turn off my brain and focus on building code and don't have to know which parts needs to run when exactly.
When I joined one of my previous teams, they were using a rube goldberg machine of Bash, AWK, Python scripts with a lot of JSON slurper and AWK sprinkled in to manage multiple modules in different repos into a monorepo-like state with individual releasing and versioning. With Gradle, that was much easier to accomplish: The dependency resolution rules could be written as simple code with easy and well defined conditionals, instead of manipulation of Maven pom files during random build steps. Also introspection into the build dependencies allowed an easy way to find out which projects dependend in which other ones. (Now was doing all of this a good idea in the first place? probably not :) but they certainly didnt allow themselves to be stopped by Mavens inflexibility, and the resulting horror of a build pipeline was certainly worse then writing all of that in Gradle)
In think, in the end, for small projects, Gradle isn't that benefitial. Maven can accomplish simple builds with no special rules and no necessary task avoidance just as well as Gradle, if not better due to the restrictions. But once you need to build something bigger (did someone say Enterprise?), Gradle starts shining - as long as you tread carefully.