Every artifact went up under the plain -SNAPSHOT name with no maven-metadata.xml, so a repo with unique snapshot naming stamped each upload on its own. Remote Maven snapshot deploys now pick one timestamp and build number for the whole publication and send the metadata.
**Problem**
sbt compile through the thin client prints only elapsed time: 2 s, where the
same command in the shell or under sbt --server prints
elapsed time: 2 s, cache 94%, 13 disk cache hits, 24 remote cache hits. The
summary is computed server-side and thrown away: ManagedLogger.success is gated
on Terminal.isSuccessEnabled, and a NetworkChannel's terminal answered
interactive.get || isInWatch, which is false whenever a command is passed on
the command line. The line the user sees is the client's own, built from
wall-clock alone.
**Solution**
Report the line from the server, and have the client print only when the server
will not.
NetworkChannel's terminal reports isSuccessEnabled = true, so the server
writes the line on every channel, and it advertises successLog in its
capabilities. A server from before this sends no such key, which reads as false,
so an older client-server pair behaves as it does today; an unattached client
also always prints, since it renders build/logMessage rather than the proxied
systemOut bytes. Older sbtn keeps printing its own line and so reports
success twice until it updates.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_015BqrfmHbJUph6Kx7gJn3G8
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
The flag and the axes both say whether a row is built for Scala, and
where they disagree the flag wins: a row given versions and
autoScalaLibrary = false is built for none of them, and the versions
are dropped. Nothing said so.
ProjectMatrix: validate axes against autoScalaLibrary
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
**Problem**
With HTTP/2, Apache HttpClient can begin the authenticated replay while the rejected stream is still consuming the same repeatable file producer.
The overlapping consumption can send corrupted artifacts, according to mkurz.
**Solution**
This hopefully fixes the issue by updateing to Gigahorse 0.9.6, which fixes the preauth support.
* [2.x] test: Pin what a platform axis alone does
**Problem**
`jsPlatform` enables the Scala.js plugin; the axis does not. A row
built by `customRow` with `VirtualAxis.js` is a Scala.js row by every
name it carries and a JVM row by what runs.
**Solution**
Assert what such a row reports as its platform.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* [2.x] feat: Enable JS/Native plugins if the axis is used
Only jsPlatform and nativePlatform enabled the plugin their platform
needs, so using customRow directly would skip it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* [2.x] feat: Configure the rows a matrix builds
A build could reach every row with configure, or one row with the call
that made it, and nothing in between, so settings for the rows of one
platform travelled with every call that creates them. Now
configureRows takes a function of a row's axes and answers with what
to apply, and a row is given it as it is built, so a row declared
after the call is covered too.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* test: Pin the axes of a row that carries no Scala version
`jvmPlatform(autoScalaLibrary = false)` passes `VirtualAxis.jvm` to
`customRow`, which appends it again, so the row holds the platform axis
twice and its generated directories are named `scalajvm-jvm` and
`javajvm-jvm`. Nothing covered that path.
* fix: Keep one platform axis on a row that names its own
`jvmPlatform(autoScalaLibrary = false)` hands `customRow` the platform
axis, and `customRow` appended a second one, so the row's generated
directories were named `scalajvm-jvm` and `javajvm-jvm` and a cell
whose sources sat in the usual places compiled nothing. Now the axis is
appended only when the caller named no platform.
**Problem**
The clean task no longer works as intended on sbt 2.x due to the disk cache.
**Solution**
This implements an in-memory state of invalidation for subproject and
configuration axes. So clean task would mark subprojects to be
invalidated regardless of the disk cache state.
The invalidation state would be lifted once any cached task executes,
so it's not technically same as sbt 1.x, but the UX should be similar.
**Problem**
Tags are enforced per task node.
This means that a chain like setup-test-cleanup
would release the tags midway, and allow interleaving
executions even for exclusive tests.
**Solution**
This implements a special Span tag, which lets us
keep the tags across the task chains.
Since this requires the flatMapped tasks to not carry their
own tags to prevent a deadlock, the feature needs to be opt-in /
internal implementation.
compileOrder and usePipelining were missing from CompileInputs2, so two builds differing only in those settings shared one cache entry and replayed each other's results. Adds both to the key, with a unit test on the hash and a scripted test that fails without the change.
**Problem**
When a key does not say which project it belongs to, Extracted.runAggregated filled in the global scope instead of the current project.
**Solution**
Fill in the current project instead, the way runInputTask always has. The only difference between the two scopes is the project, so a key that already names a project behaves exactly as before.
**Problem**
If a client cannot reach the server, it deletes the portfile. Then it
starts a second server. The first server is displaced: its socket path
now belongs to the second server.
A displaced server keeps running. A dropIfIdle notification cannot
reach it, because the proc file it registered names the socket that
the second server now owns.
**Solution**
The portfile holds the serverId of whichever server wrote it. Watch
that file, and exit when the id is not this server's. Leave the
portfile to the server that owns it.
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
+cleanFull failed with Not a valid key: Full. The cross command re-parses its argument with the key parser, and clean is an accepting prefix, so the rest was taken as task args. Args now have to be preceded by whitespace.
**Problem**
Currently bad resources are silently dropped.
**Solution**
1. Fix a bad resource generator in the build.
2. Fail the build when bad resources are found.
The overload that takes axisValues and settings passes an empty
scalaVersions into the fold that builds the rows, so a call with
autoScalaLibrary = true returns the matrix unchanged. The assertion
records that, so the fix can move it.
Now the call adds one row from the axes it was given, so a build can name a partial or a full version instead.
* [2.x] feat: testForkedWorker setting
**Problem**
A major bottleneck of the default setting is that concurrentRestriction
allows exactly one forked test in flight at a time,
and partly due to the restriction all test suites from a subproject runs
within the forked process.
**Solution**
This adds a new setting called testForkedWorker, which lets the build users
tune the number of concurrent worker process in flight.
I've set the default to CPU count / 3.
Within a single worker, the default parallelism is reduced to 2.
The default test grouping now splits the test suites into
testForkedWorker count.
* Use Global scope for overall worker count
Subproject scoping can be used for single vs split
* Adds math.max(..., 1)
* Avoid running setup/cleanup on an empty selection
* Fix the tag application order
* Rename key to workerMaxInstances
* Implement TestTopology
* fix: Fixes exclusive test
Note due to the limitation of the tag system, setup-clean/cleanup
may overlap.
* Check that workerMaxInstances >= 1
**Problem**
The accept loop did not catch an exception from onIncomingSocket, so
its thread ended. Nothing closed the server socket, so the path stayed
bound: the server took no further client, and the process kept running.
The socket of the client that failed stayed open too.
**Solution**
Log that client, close its socket, and take the next one. An exception
from accept itself keeps the handling it had, so a socket that is
really broken still ends the loop.
The callback now takes a holder rather than the socket, and calls
AtomicCloseable.release to keep it. The loop closes whatever the
callback left behind.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
CommandExchange asked twice whether an exec came from the closing
channel, and cancelled an exec in two places. Each has a name now, as
does the read of shuttingDown and the Try whose failure is ignored.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Problem
-------
SemanticdbPlugin.compileIncAndCacheSemanticdbTargetRootTask is a cached task
that reads semanticdbTargetRoot as a bare File, so its absolute path is hashed
into the action cache key:
((true,${OUT}/.../classes,${OUT}/.../classes.sbtdir.zip),/tmp/repro/target/out/.../meta)
Every other component of that key is virtualized. This one names one checkout
on one machine, so the entry can never be served to a second checkout, to a
colleague, or to CI. A monorepo we measured had 118 such keys, none of them
reachable across machines.
Fixes#9709
Solution
--------
Read the target root through semanticdbTargetRootVF, following the convention
sourcesVF and resourcesVF already set: an uncached task holding the virtualized
form of a File-typed key, so that a cached task hashes ${OUT}/.../meta rather
than the path behind it. A cached helper would key on the same bare File and
reintroduce the problem one level up.
The scripted test asserts the property the bug breaks: relocating
rootOutputDirectory must leave every key alone, so the second compile is a pure
cache hit. It pins Compile / semanticdbOptions, because -semanticdb-target
carries an absolute path into scalacOptions as well, which is a separate issue
and would otherwise mask this one.
Generated-by: Claude Opus 5
The cache now writes each value-distinct ModuleReport once into a "modules"
table and gives each configuration's details a list of indices into it. That is
~5x smaller and hands the reader instance sharing structurally, with nothing to
probe to rediscover it.
CacheStoreFactory.makeCompressed writes gzip-framed JSON and reads either
framing, sniffing the magic bytes so a cache written before a store was
switched to compression still loads. It defaults to make, so existing
CacheStoreFactory implementations are unaffected.
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
**Problem**
Four fields hold a closeable that a later caller replaces. Each one
reads the field, closes what it finds and writes the new value in its
own way.
**Solution**
AtomicCloseable holds such a value. It closes the value it replaces,
and closes the value that loses a race to fill an empty field.
Two things change for a caller. The client replaced its session
without closing the one it dropped, and now closes it. A close that
throws no longer escapes: each of these sites is discarding the value
it closes, and whatever led there matters more than the close.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
**Problem**
ThisBuild scoped baresettings are treated as a common setting,
which results in duplicate appends etc.
**Solution**
Only treat This-project and ThisProject scoped setting as a common setting.
**Problem**
InMemoryCacheStore.CacheStoreImpl.read returns cacheStore.read[T]() on a miss
without putting the value in the cache, so the only thing that ever fills the
cache is write. A task whose stored output is already up to date never writes,
so it re-reads and re-deserialises that output on every invocation for the life of
the server, and the cache can never warm up for it.
update is the costly instance: transitiveUpdate runs it once per project in
the dependency closure, and every call goes through
UpdateReportPersistence.readFrom.
**Solution**
Put the value in the cache after a successful disk read. The keying is unchanged
-- an entry is still (path, lastModified), read re-stats the file on every
call, and write still invalidates before rewriting -- so a populated read is
sound for the same reason a populated write is.
Measured on an 81-module workspace, no-op build, real output files (no
cache-restored symlinks), exportJars=true, alternating the two builds over 8
runs each:
update task 398ms across 82 projects -> 30ms across 29
task total 4797ms -> 4623ms
wall (median) 5.15s -> 4.97s
This makes maximumWeight bind where it could not before: reads now admit
entries, so a build whose working set exceeds sbt.file.cache.size will evict
rather than never cache at all. That is the intended behaviour of a bounded cache.
Generated-by: Claude Opus 5
Fixes#899
Add the task join as an extension method on Seq[Def.Initialize[Task[A]]] in
Def. Member resolution tries extension methods before implicit
conversions, so it is selected ahead of the generic conversion regardless of
whether sbt.Scoped is in scope.
Watch mode copied and sorted the whole dynamic-input set on every file event, once per config (Continuous.inputs()). The sort is irrelevant to the glob match, so match against the set directly. Biggest relative gain on macOS, where FSEvents coalescing turns a git checkout into a burst of thousands of events.
**Problem**
CompileInputs2.sources and packageBin / mappings are hashed positionally but arrive in
filesystem traversal order, which differs between APFS, ext4 and overlayfs. Two machines
hash an identical tree into different cache keys and cannot use each other's entries.
**Solution**
Sort sourcesVF by virtualized id, and mappings where packageConfigurationTask reads
it. Both are where the keys are built, so nothing appended later escapes the sort.
Generated-by: Claude Opus 5
Only rewrite generated plugin descriptors when their normalized lines change, preserving the descriptor timestamp and downstream cache inputs for unchanged plugin discovery.
Add unit coverage plus direct packageBin and assembly Scripted scenarios. The descriptor regressions fail with the former unconditional write and pass with this change; the packageBin fixture also preserves SBT 2’s existing cache-hit behavior.
Co-authored-by: Dmitrii Naumenko <[email protected]>
Co-authored-by: Codex <[email protected]>
Add a resolvedScalacOptions task that resolves cache placeholders in scalacOptions to absolute machine paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
**Problem**
packageBin includes version into the file name, which ends up
invalidating the cache.
**Solution**
Define packageInternal, which does not include version in the file name, and used during Compile or Test/compile. Note that publishing and Runtime classpath would continue to use packageBin.
Both compileOptions sites mapped the whole classpath through
converter.toPath -> converter.toVirtualFile. Going through toPath
defeats FileConverter.toVirtualFile(VirtualFileRef), which already
returns the ref unchanged when it is a VirtualFile, so every entry was
rebuilt from scratch. For a class directory that means walking the
entire output tree, once per dependent.
backendOutput is converted explicitly because it is a settingKey
evaluated once at project load, so reusing it would pin the listing
taken before anything compiled.
Co-authored-by: Claude Opus 5 <[email protected]>
**Problem**
CrossVersion(module, scalaModuleInfo) is handed everything needed to name an
artifact, including ScalaModuleInfo.platform, and the name it returns is what
a caller publishes or resolves under. Nothing covers what it does with the
platform.
**Solution**
Name the artifact in full: platform suffix before cross suffix, matching the
coordinate (sbt/sbt#9117), through addPlatformSuffix, which already knows that
jvm contributes no suffix.
Generated-by: Claude Opus 5
**Problem/Solution**
This extends the idea started with TestRecap, and applies it to
both test success and failures.
1. Existing TestResultLogger trait is extended to handle the summary
rendering.
2. TestSummary enum is added to control the verbosity via
commandline option, system property, or a setting.
3. Script test captures the log.
Reuse the sibling dirzip when zinc reports it wrote nothing. The output
is still declared, so the stored ActionResult stays complete and a later hit
restores the class directory exactly as before -- it is the same
HashedVirtualFileRef the packaging path would have produced, read off disk
rather than rebuilt.
Co-authored-by: Claude Opus 5 <[email protected]>
**Problem**
1. build.sbt was compiled with console reporter,
so we couldn't capture the error.
2. Another problem was that reload deleted global log
so we couldn't get to the previous load failure.
**Solution**
1. This forwards build.sbt compilation errors to the logger.
2. This retains the failed loading log.
3. Using the facility above, this adds negative test for loading.
The local analysis cache validates an entry against the analysis file's
timestamp and size, so a file re-created with the same content loses its
entry. That is exactly what happens to every analysis sbt writes: the
analysis file is a declared output of compileIncremental, so once the task
body has written it, syncFile deletes it and re-creates it as a symlink into
the CAS. Measured on a two-module build, the file came back four
milliseconds later at the same size under a new timestamp, and the read in
compileIncremental that follows deserialized the analysis it had just
written - one full deserialization per compiled module per compile, which is
the cost the cache exists to avoid. On this repo's own largest analysis
(519K) that read costs 19.5ms, against 0.098ms to hash the file and 0.0025ms
to stat it.
Populating the cache from set() is not enough on its own, and populating it
with the contents handed to set() is wrong: ConsistentAnalysisFormat does not
persist Compilations, so serving the in-memory analysis flips
CompileResult.hasModified from false to true and makes compileTask store the
analysis on every compile, including no-ops. set() now records what a read
of the file returns, and an entry whose size and content hash still match is
served under a new timestamp rather than discarded.
A read whose timestamp and size still match does not hash the file, so a warm
no-op compile costs one stat per read; a read that finds no entry to compare
against hashes the file it is about to deserialize, and set() hashes the file
it wrote, since the file it has to describe is replaced moments later.
Recording an entry cannot fail a compile that has already written its
analysis, so an IO error from stat-ing or hashing that file is dropped.
Across five compiles of two modules with two source edits, the
deserializations go from five to none: previousCompile, compileIncremental,
compileScalaBackend and the dependency analysis read in compileIncSetup are
all served from memory after the first write of each module's analysis.
Nothing a build can observe says whether a compile deserialized an analysis
it already had: an analysis served from the cache is indistinguishable from
one read back, so the unit tests can pin the store's behaviour but not the
compile's. The cache therefore counts what its reads cost - answered from
memory, hashed the file to answer, deserialized it - and a scripted test
reads those counts around a compile. The counts are sbt-private, so the test
reaches them from a helper declared in package sbt under project/.
Two invariants, one per failure mode. A compile with nothing to do must
answer every read from memory without even re-reading the file to hash it,
which pins the timestamp check; dropping it makes 7 of 7 reads hash. A
compile that recompiles must be served the analysis it just wrote, which pins
the fix; keeping develop's cache and adding only the counters deserializes 1
of 7.
Both depend on the compiles being real ones, because an analysis the action
cache served was never written and so there is nothing for the local cache to
have kept. The action cache is global, and its key does not depend on where
the build sits, so a second run of this test would otherwise recompile
nothing: the build points localCacheDirectory inside the sandbox - in Global,
which is the scope cacheStores resolves it in - and the test deletes target
first, so a run cannot inherit what an earlier one compiled.
Generated-by: claude-opus-5 (Claude Code)
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
**Problem**
Ivyless publishing doesn't work with sbt-pgp.
**Solution**
This refactors the ivyless publishing code and revives publishOrSkip, which is called by the plugin.
Instead of overriding publish task, implement proper PublisherInterface.
**Problem**
Some custom LSP calls do not check initialize-handshake,
which over TCP includes token-based authentication.
**Solution**
This adds checkAuthenticated check around sbt/exec etc.