Extending Versions
Skip unless you are writing an sbt plugin that sits on zipx (a company catalog, CDN or vendor pins; sbt-splice is
one). Consumers stay on Versions: they extend ZipxVersions, drop MyVersions.settings, and write Lib /
Plugin / Pin / Action vals. Outbound Ship / ShipGroup rows are Independent versions.
Two jobs. They are not the same hook.
Emit your plugin line into generated
project/plugins.sbtfrom the version on the classpath (zipxSelfPlugins/ZipxSelf.emit). Consumers should not duplicate that GAV inZipxVersions.Optionally put your libraries, pins, and Actions in the consumer catalog (
Lib/Pin/Actionvals on a subtype, orAsCoords/AsPins/AsActionson a bundle). That is the row story below. A plugin that ships aPinFeed(lookup plus optionalmaterialize) does not ship the inventory: the consumer repo owns which version is pinned.
Emit your plugin line
zipxEmitSelf / zipxPluginVersion are sbt-zipx only (dogfood, scripted). Every other plugin that sits on zipx appends
zipxSelfPlugins. A Plugin val on ZipxVersions is for consumer-owned plugins that do not emit themselves
(scalafmt, native-packager).
Group and artifact are always written out. zipx never scans the session. Missing Implementation-Version is a zipx:
error naming that GAV. Duplicate group+artifact in the self list fails generate. ZipxSelf.emit does not read zipx's
-Dplugin.version.
object SpliceZipxPlugin extends AutoPlugin:
override def requires = ZipxPlugin
override def buildSettings = Seq(
zipxSelfPlugins += ZipxSelf.emit("rocks.earlyeffect", "sbt-splice", getClass)
)
Generate writes zipx first, then your line, then the consumer's catalog plugins:
{
val zipx = Plugin("rocks.earlyeffect", "sbt-zipx", "0.5.1")
val splice = Plugin("rocks.earlyeffect", "sbt-splice", "1.0.0")
val scalafmt = Plugin("org.scalameta", "sbt-scalafmt", "2.6.2")
ZipxCatalog.renderPlugins(List(scalafmt), self = List(zipx, splice))
}// Generated by zipx. Do not edit. Run 'sbt zipxWorkflowGenerate' to regenerate.
addSbtPlugin("rocks.earlyeffect" % "sbt-zipx" % "0.5.1")
addSbtPlugin("rocks.earlyeffect" % "sbt-splice" % "1.0.0")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2")
Your libraries in that same catalog
Collection is a typeclass, AsCoords / AsPins / AsActions / AsShips. Lib and Plugin share AsCoords
(A <: ZipxCoord). Pin has AsPins. Action has AsActions. Ship / ShipGroup have AsShips. You add a given
for your own type, or you put Lib / Plugin / Pin / Action / Ship vals on a ZipxVersions subtype. There is no
second coords list for the consumer to keep in sync. Do not put your plugin GAV on that subtype if you already emit
it; that catalog line is dropped.
Two paths
Path 1: Lib / Plugin / Pin / Action vals on your subtype. Inherited fields are collected. The consumer writes
object MyVersions extends SpliceVersions and your vals are rows.
trait SpliceVersions extends ZipxVersions:
val spliceRuntime = Lib("rocks.earlyeffect", "splice-core", "1.0.0")
val preact = Pin("cdn", "preact", "10.26.4", sha256 = "sha256-abc", purl = "pkg:npm/preact@10.26.4")
// extra settings: MyVersions.settings ++ spliceCdnFeed in build.sbt
The consumer still owns those Pin vals (or copies them). You ship the feed, not a second inventory list.
Path 2: a bundle type with given AsCoords[YourType]. Put the given on the companion so the catalog file does not
import extra machinery. Collection summons it the same way it summons Lib. CDN bundles use given AsPins the same
way; Action bundles use given AsActions.
final case class SpliceLibs(runtime: Lib)
object SpliceLibs:
given AsCoords[SpliceLibs] with
def coords(s: SpliceLibs) = Seq(s.runtime)
trait SpliceVersions extends ZipxVersions:
val splice = SpliceLibs(
Lib("rocks.earlyeffect", "splice-core", "1.0.0")
)
Do not add given AsCoords[List[Lib]]. That would collect every helper list on the consumer object. Own a named
type.
Keep Lib("g", "a", "from") / Plugin("g", "a", "from") / Pin("feed", "id", "from", …) /
Action("owner/repo", "from", sha = …) constructors in project/ZipxVersions.scala. zipxDepUpdate, zipxPinUpdate,
and zipxActionUpdate rewrite those literals; they do not know about SpliceLibs("1.0.0"). Maven, pin, and self-emit
hunks are below.
What collection sees
coordsOf / pinsOf walk vals on the concrete object (this.type). A def is skipped. SbtVersion / ScalaVersion
have no given. A parent-trait Lib val and a bundle val both become rows:
Sample.coords.map(c => s"${c.group}:${c.artifact}").sorted.mkString("\n")com.acme:acme-core
com.acme:from-trait
com.acme:sbt-acmezipxCheckDeps compares libraryDependencies to the flattened Lib rows. A GAV that came out of your bundle is a
catalog hit, same as a bare val zio = Lib(...).
{
val extra = ZipxCatalog.extraLibs(
List(DeclaredGav("com.acme", "acme-core", "1.2.3")),
Sample.coords,
)
if extra.isEmpty then "(none)" else extra.map(_.render).mkString("\n")
}(none)settings stays on the plugin
coords / pins / actions live on the core trait so a process that is not the target sbt can compile the catalog
file. settings is an inline extension on the plugin (MyVersions.settings in build.sbt). Extra settings belong next
to that call (MyVersions.settings ++ spliceSettings), not as inline override def settings on a subtype.
The consumer still writes one line in build.sbt: MyVersions.settings.
Classpath and package
The consumer compiles project/ZipxVersions.scala with your plugin and sbt-zipx on the meta classpath. Put the trait,
the bundle, and the given in a package they can import (import splice.*, plus import zipx.* for ZipxVersions /
AsCoords / AsPins / AsActions / Pin / Action). The given on the bundle companion is found without a further
import.
Do not name a package sbt. On sbt 2 that shadows _root_.sbt and the plugin will not compile. zipx keeps catalog
types in package zipx for the same reason.
Apply still rewrites constructors
Flattening into zipxVersions / zipxPins is what check and lookup see. Apply is still a constructor rewrite in the
catalog source. Nested Lib(...) / Plugin(...) / Pin(...) literals inside a bundle are what move. A bundle that
stores only a version string will list as stale and then not rewrite.
A Path 1 Lib / Plugin val on your subtype looks like Versions. Path 2 is the same rewrite, with your type
around the constructor. Sibling constructors in that bundle stay put. Pins are the next section.
bundleCatalogPrDiffCustom pins
You ship the PinFeed (lookup, policy, optional materialize). The consumer writes the Pin vals, on your
ZipxVersions subtype or inside an AsPins bundle. Apply still rewrites those Pin(...) constructors: version,
sha256, and purl together. Maven rows stay put. Your own addSbtPlugin line stays put.
A given AsPins bundle is the same rewrite: the Pin(...) literal inside the case class, not CdnPins("1.2.4").
If materialize writes a vendored file, that second path is in the same PR. zipx does not regex that file; your feed
wrote it. Policy and local zipxPinUpdate are Pin feeds.
DocDiff.stack(customPinCatalogDiff, customPinVendorDiff)Your plugin line is a different file
ZipxSelf.emit writes your GAV into generated project/plugins.sbt from the JAR on the classpath. That version is
not a Plugin val. zipxDepUpdate will not rewrite it. When you publish sbt-splice 1.1.0, the consumer loads that
plugin and generate rewrites the addSbtPlugin line. project/ZipxVersions.scala does not change.
If they also wrote Plugin("rocks.earlyeffect", "sbt-splice", …) on ZipxVersions, generate drops that catalog line
and keeps the loaded version. The PR they open is still plugins.sbt:
selfEmitPrDiffNested example on the catalog PR
If this plugin repo has a nested consumer example whose generated ci.yml must track Action peels, set
zipxVersionUpdatesExtraSteps: publishLocal the in-dev plugin, then zipxWorkflowGenerate in that tree. Nested
.github/workflows/ is not repo-root, so the bot can commit it. Root ci.yml still needs a human generate. Full
recipe: Dependency updates.