I’m using SBT (and Scala) for small projects, so I have my own perspective for its problems.
Performance is not an issue. If I need to recompile entire project the scalac would eat much more time than sbt. If I works incremently, then SBT is already running. Fast start is always better than slow, but current speed is bearable. The same goes for the Scala itself that takes plenty of time just to run hello world script.
Using SBT is not so difficult. Comparing to other widely adopted Scala madness like typeclasses full of implicits generated by macroses. SBT uses dependency graph conception that is slightly harder than usual batch script approach, but data driven programming is pretty familiar for functional programmes that are attracted to the scala. And it is easy enough (comparing to Free monads) to be used even by an imperative programmer. Syntax sugar is very common thing in the Scala world too. And it is used moderately in SBT.
SBT is confusing. Especially for a newbie. In a several ways. And I could not suggest a way out of it.
The first issue is
project/build.scala duality. SBT could not refuse either because it aims for scalability. .scala file is sure overkill for a small test project, since it could take more lines to initialize a project that it has itself while single line in the .sbt file enough. For a large project .scala is irreplaceable since it could implement complicated build logic. Removing any of it would split community: some would refuse to update SBT, some would go for a fork/alternatives.
Keeping both rises questions. Where should I put bits of my configuration? What .sbt equivalent is for the .scala code and vice versa? What is the order of evaluations? The latter was a problem for me when I started to use SBT. Confusion could be mitigated with good documentation describing all confused point. Just took into account that users may have previous experience with other build tools, possible make/ant/maven and they have appropriate expectations.
So you need to teach make guy that although there is a order in which configuration files read, they are evaluated later in lazy manner. Maven guy expects special syntax for static configuration without any dynamic scripts. Tell him that
.sbt is generally used as configuration with
:= syntax and
project/build.scala is like in-place plugin that describes all non-trivial behaviour.
The second confusion source is half-way DSL. It is somewhere between completely new language and orthodox scala. If you would treat it as clean scala, it would give you some surprises when compiling the project. If you would treat it as complete DSL you may forget to do some courtesy like calling
.value. But you still could not enhance DSL further without breaking connection with scala world. And you could not give up on DSL, because it will increase significantly code amount for a small projects. The same scalability problem.
SBT emits confusing errors when it get something that it could not chew. Not a problem for a seasoned scala programmers. They had saw more complex and arcane compiler errors. But SBT should suit for beginners. How else a beginner could start learning scala if not with SBT? And at that stage he could not process build errors naturally. So you need some template/graphic configuration for newbies.
SBT derive strength from turing complexity of the Scala used as configuration language. And there lies great danger for a newbie. To guard him from perils some extra wrapper that would take turing complexity and give confidence is needed.
SBT is poor documented for advanced users. Documentation for beginners may lack few things, but you could spend a hour, another hour at most entire day if you is persistent enough and you would get enough information to use SBT consciously. But the documentation has only two pages related to plugin development. And when you are writing a non-trivial plugin, you should interact with SBT internals a lot. It is not documented anywhere. I was forced to look into the SBT sources and found them hard to read. Probably I’m a weak scala programmer. But there were almost no comments.
So I spend a week to fight SBT and won. But eventually 0.13.5 emerged and my code broke. I find easier to freeze SBT version than to do another dive into SBT sources to fix my code appropriately. So I’m waiting for either SBT stabilize in version and I could take another dive and forget for years or until it finally become well documented.
SBT forget to export number of essential functionality to the users. SBT uses it internally, but users should reimplement it, which sometimes leads to very cumbersome code.
The first feature that comes to mind is caching dependencies. When you use internal SBT tasks they checks if dependencies has changed since the last run and recompile the target only if some changes were made. But when you try to write your own tasks you discover that there is no such functionality provided.
Let take a simple example: we have documentation in a folder with markdown files and would like to compile html site from the files. The SBT would recompile the html each time even if there is not a slightest change in the markdown files, even if it modifier time is unchanged. And user has no means to prevent it unless they would like to write a bunch of code. Even primitive make utility could compare file modifying dates and avoid worthless work. That shortage become pain every time I generates some files manually.
Another example is
run command behaviour. They scan entire code base and find classes with specific signatures, make list of them and suggests user to select. Such filtering is not available for plugin writers. I have a use case when it is needed: my own test and benchmarking utility. I need to find all benchmark classes and execute them.
One more thing about the previous case. The first problem prevails: if you use naive approach for finding alternatives, full scan would be performed each time user executes mentioned command. So you need a way to cache scan results.
I’ve looked in the SBT source code and it has a lot of black magic for such use cases. Carefully deployed magic, so users rarely wonders how they beloved
testOnly works. But if you carefully read all the provided documentations you should become curious how can config keys, task keys and commands be composed to reproduce complicated behaviour of the mentioned commands.
So I think that the biggest drawback of SBT in its current state is that you could not reproduce basic behaviour with provided public API. So there is an indicator on SBT maturity: when its scala sources build implementation would become decoupled from SBT dependency graph solver (together with cash provider, logger and other utilities) . So all scala-related build mechanics would be delivered as separate plugin, and SBT could be used as generic build tool without it.
SBT needs call-by-name semantics for keys. That is highly subjective theme, but I found current implementation very inconsistent. Suppose you has keys
B depends on
A value, e.g.
B := A.value*2. They could be accessed through
Outer scope. And
A get a reasonable default value
1 in the
Outer scope. How
Inner/B is resolved? Took
Outer/B and inherit it, so it would be 2. And what if
Inner/A changes to 10?
Inner/B remains the same: it is already assigned 2 and it has no reassigning. Call-by-name means that
B would follow the
A in every scope.
End users pays little attention to the issue since they get their environment ready to use. Mid users (that uses SBT API to write plugins) developed already workaround: abstract configuration into a variable, and apply it to each scope separately. Piece of boilerplate and endless source for strange errors when someone forgets to apply variable to a particular scope.