Add credentials support to sbt-lm-coursier

This commit is contained in:
Alexandre Archambault 2018-11-22 13:52:59 +01:00
parent 3cbb1e3c2e
commit 40489f7fee
27 changed files with 120 additions and 90 deletions

View File

@ -21,22 +21,24 @@ final class CoursierConfiguration private (
val classifiers: Vector[String],
val mavenProfiles: Vector[String],
val scalaOrganization: Option[String],
val scalaVersion: Option[String]) extends Serializable {
val scalaVersion: Option[String],
val authenticationByRepositoryId: Vector[(String, coursier.core.Authentication)],
val authenticationByHost: Vector[(String, coursier.core.Authentication)]) extends Serializable {
private def this() = this(None, sbt.librarymanagement.Resolver.defaults, true, 6, 100, None, None, Vector.empty, Vector.empty, Vector.empty, Vector.empty, true, false, Vector.empty, Vector.empty, None, None)
private def this() = this(None, sbt.librarymanagement.Resolver.defaults, true, 6, 100, None, None, Vector.empty, Vector.empty, Vector.empty, Vector.empty, true, false, Vector.empty, Vector.empty, None, None, Vector.empty, Vector.empty)
override def equals(o: Any): Boolean = o match {
case x: CoursierConfiguration => (this.log == x.log) && (this.resolvers == x.resolvers) && (this.reorderResolvers == x.reorderResolvers) && (this.parallelDownloads == x.parallelDownloads) && (this.maxIterations == x.maxIterations) && (this.sbtScalaOrganization == x.sbtScalaOrganization) && (this.sbtScalaVersion == x.sbtScalaVersion) && (this.sbtScalaJars == x.sbtScalaJars) && (this.interProjectDependencies == x.interProjectDependencies) && (this.excludeDependencies == x.excludeDependencies) && (this.fallbackDependencies == x.fallbackDependencies) && (this.autoScalaLibrary == x.autoScalaLibrary) && (this.hasClassifiers == x.hasClassifiers) && (this.classifiers == x.classifiers) && (this.mavenProfiles == x.mavenProfiles) && (this.scalaOrganization == x.scalaOrganization) && (this.scalaVersion == x.scalaVersion)
case x: CoursierConfiguration => (this.log == x.log) && (this.resolvers == x.resolvers) && (this.reorderResolvers == x.reorderResolvers) && (this.parallelDownloads == x.parallelDownloads) && (this.maxIterations == x.maxIterations) && (this.sbtScalaOrganization == x.sbtScalaOrganization) && (this.sbtScalaVersion == x.sbtScalaVersion) && (this.sbtScalaJars == x.sbtScalaJars) && (this.interProjectDependencies == x.interProjectDependencies) && (this.excludeDependencies == x.excludeDependencies) && (this.fallbackDependencies == x.fallbackDependencies) && (this.autoScalaLibrary == x.autoScalaLibrary) && (this.hasClassifiers == x.hasClassifiers) && (this.classifiers == x.classifiers) && (this.mavenProfiles == x.mavenProfiles) && (this.scalaOrganization == x.scalaOrganization) && (this.scalaVersion == x.scalaVersion) && (this.authenticationByRepositoryId == x.authenticationByRepositoryId) && (this.authenticationByHost == x.authenticationByHost)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "coursier.lmcoursier.CoursierConfiguration".##) + log.##) + resolvers.##) + reorderResolvers.##) + parallelDownloads.##) + maxIterations.##) + sbtScalaOrganization.##) + sbtScalaVersion.##) + sbtScalaJars.##) + interProjectDependencies.##) + excludeDependencies.##) + fallbackDependencies.##) + autoScalaLibrary.##) + hasClassifiers.##) + classifiers.##) + mavenProfiles.##) + scalaOrganization.##) + scalaVersion.##)
37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "coursier.lmcoursier.CoursierConfiguration".##) + log.##) + resolvers.##) + reorderResolvers.##) + parallelDownloads.##) + maxIterations.##) + sbtScalaOrganization.##) + sbtScalaVersion.##) + sbtScalaJars.##) + interProjectDependencies.##) + excludeDependencies.##) + fallbackDependencies.##) + autoScalaLibrary.##) + hasClassifiers.##) + classifiers.##) + mavenProfiles.##) + scalaOrganization.##) + scalaVersion.##) + authenticationByRepositoryId.##) + authenticationByHost.##)
}
override def toString: String = {
"CoursierConfiguration(" + log + ", " + resolvers + ", " + reorderResolvers + ", " + parallelDownloads + ", " + maxIterations + ", " + sbtScalaOrganization + ", " + sbtScalaVersion + ", " + sbtScalaJars + ", " + interProjectDependencies + ", " + excludeDependencies + ", " + fallbackDependencies + ", " + autoScalaLibrary + ", " + hasClassifiers + ", " + classifiers + ", " + mavenProfiles + ", " + scalaOrganization + ", " + scalaVersion + ")"
"CoursierConfiguration(" + log + ", " + resolvers + ", " + reorderResolvers + ", " + parallelDownloads + ", " + maxIterations + ", " + sbtScalaOrganization + ", " + sbtScalaVersion + ", " + sbtScalaJars + ", " + interProjectDependencies + ", " + excludeDependencies + ", " + fallbackDependencies + ", " + autoScalaLibrary + ", " + hasClassifiers + ", " + classifiers + ", " + mavenProfiles + ", " + scalaOrganization + ", " + scalaVersion + ", " + authenticationByRepositoryId + ", " + authenticationByHost + ")"
}
private[this] def copy(log: Option[xsbti.Logger] = log, resolvers: Vector[sbt.librarymanagement.Resolver] = resolvers, reorderResolvers: Boolean = reorderResolvers, parallelDownloads: Int = parallelDownloads, maxIterations: Int = maxIterations, sbtScalaOrganization: Option[String] = sbtScalaOrganization, sbtScalaVersion: Option[String] = sbtScalaVersion, sbtScalaJars: Vector[java.io.File] = sbtScalaJars, interProjectDependencies: Vector[coursier.core.Project] = interProjectDependencies, excludeDependencies: Vector[(String, String)] = excludeDependencies, fallbackDependencies: Vector[coursier.lmcoursier.FallbackDependency] = fallbackDependencies, autoScalaLibrary: Boolean = autoScalaLibrary, hasClassifiers: Boolean = hasClassifiers, classifiers: Vector[String] = classifiers, mavenProfiles: Vector[String] = mavenProfiles, scalaOrganization: Option[String] = scalaOrganization, scalaVersion: Option[String] = scalaVersion): CoursierConfiguration = {
new CoursierConfiguration(log, resolvers, reorderResolvers, parallelDownloads, maxIterations, sbtScalaOrganization, sbtScalaVersion, sbtScalaJars, interProjectDependencies, excludeDependencies, fallbackDependencies, autoScalaLibrary, hasClassifiers, classifiers, mavenProfiles, scalaOrganization, scalaVersion)
private[this] def copy(log: Option[xsbti.Logger] = log, resolvers: Vector[sbt.librarymanagement.Resolver] = resolvers, reorderResolvers: Boolean = reorderResolvers, parallelDownloads: Int = parallelDownloads, maxIterations: Int = maxIterations, sbtScalaOrganization: Option[String] = sbtScalaOrganization, sbtScalaVersion: Option[String] = sbtScalaVersion, sbtScalaJars: Vector[java.io.File] = sbtScalaJars, interProjectDependencies: Vector[coursier.core.Project] = interProjectDependencies, excludeDependencies: Vector[(String, String)] = excludeDependencies, fallbackDependencies: Vector[coursier.lmcoursier.FallbackDependency] = fallbackDependencies, autoScalaLibrary: Boolean = autoScalaLibrary, hasClassifiers: Boolean = hasClassifiers, classifiers: Vector[String] = classifiers, mavenProfiles: Vector[String] = mavenProfiles, scalaOrganization: Option[String] = scalaOrganization, scalaVersion: Option[String] = scalaVersion, authenticationByRepositoryId: Vector[(String, coursier.core.Authentication)] = authenticationByRepositoryId, authenticationByHost: Vector[(String, coursier.core.Authentication)] = authenticationByHost): CoursierConfiguration = {
new CoursierConfiguration(log, resolvers, reorderResolvers, parallelDownloads, maxIterations, sbtScalaOrganization, sbtScalaVersion, sbtScalaJars, interProjectDependencies, excludeDependencies, fallbackDependencies, autoScalaLibrary, hasClassifiers, classifiers, mavenProfiles, scalaOrganization, scalaVersion, authenticationByRepositoryId, authenticationByHost)
}
def withLog(log: Option[xsbti.Logger]): CoursierConfiguration = {
copy(log = log)
@ -104,10 +106,16 @@ final class CoursierConfiguration private (
def withScalaVersion(scalaVersion: String): CoursierConfiguration = {
copy(scalaVersion = Option(scalaVersion))
}
def withAuthenticationByRepositoryId(authenticationByRepositoryId: Vector[(String, coursier.core.Authentication)]): CoursierConfiguration = {
copy(authenticationByRepositoryId = authenticationByRepositoryId)
}
def withAuthenticationByHost(authenticationByHost: Vector[(String, coursier.core.Authentication)]): CoursierConfiguration = {
copy(authenticationByHost = authenticationByHost)
}
}
object CoursierConfiguration {
def apply(): CoursierConfiguration = new CoursierConfiguration()
def apply(log: Option[xsbti.Logger], resolvers: Vector[sbt.librarymanagement.Resolver], reorderResolvers: Boolean, parallelDownloads: Int, maxIterations: Int, sbtScalaOrganization: Option[String], sbtScalaVersion: Option[String], sbtScalaJars: Vector[java.io.File], interProjectDependencies: Vector[coursier.core.Project], excludeDependencies: Vector[(String, String)], fallbackDependencies: Vector[coursier.lmcoursier.FallbackDependency], autoScalaLibrary: Boolean, hasClassifiers: Boolean, classifiers: Vector[String], mavenProfiles: Vector[String], scalaOrganization: Option[String], scalaVersion: Option[String]): CoursierConfiguration = new CoursierConfiguration(log, resolvers, reorderResolvers, parallelDownloads, maxIterations, sbtScalaOrganization, sbtScalaVersion, sbtScalaJars, interProjectDependencies, excludeDependencies, fallbackDependencies, autoScalaLibrary, hasClassifiers, classifiers, mavenProfiles, scalaOrganization, scalaVersion)
def apply(log: xsbti.Logger, resolvers: Vector[sbt.librarymanagement.Resolver], reorderResolvers: Boolean, parallelDownloads: Int, maxIterations: Int, sbtScalaOrganization: String, sbtScalaVersion: String, sbtScalaJars: Vector[java.io.File], interProjectDependencies: Vector[coursier.core.Project], excludeDependencies: Vector[(String, String)], fallbackDependencies: Vector[coursier.lmcoursier.FallbackDependency], autoScalaLibrary: Boolean, hasClassifiers: Boolean, classifiers: Vector[String], mavenProfiles: Vector[String], scalaOrganization: String, scalaVersion: String): CoursierConfiguration = new CoursierConfiguration(Option(log), resolvers, reorderResolvers, parallelDownloads, maxIterations, Option(sbtScalaOrganization), Option(sbtScalaVersion), sbtScalaJars, interProjectDependencies, excludeDependencies, fallbackDependencies, autoScalaLibrary, hasClassifiers, classifiers, mavenProfiles, Option(scalaOrganization), Option(scalaVersion))
def apply(log: Option[xsbti.Logger], resolvers: Vector[sbt.librarymanagement.Resolver], reorderResolvers: Boolean, parallelDownloads: Int, maxIterations: Int, sbtScalaOrganization: Option[String], sbtScalaVersion: Option[String], sbtScalaJars: Vector[java.io.File], interProjectDependencies: Vector[coursier.core.Project], excludeDependencies: Vector[(String, String)], fallbackDependencies: Vector[coursier.lmcoursier.FallbackDependency], autoScalaLibrary: Boolean, hasClassifiers: Boolean, classifiers: Vector[String], mavenProfiles: Vector[String], scalaOrganization: Option[String], scalaVersion: Option[String], authenticationByRepositoryId: Vector[(String, coursier.core.Authentication)], authenticationByHost: Vector[(String, coursier.core.Authentication)]): CoursierConfiguration = new CoursierConfiguration(log, resolvers, reorderResolvers, parallelDownloads, maxIterations, sbtScalaOrganization, sbtScalaVersion, sbtScalaJars, interProjectDependencies, excludeDependencies, fallbackDependencies, autoScalaLibrary, hasClassifiers, classifiers, mavenProfiles, scalaOrganization, scalaVersion, authenticationByRepositoryId, authenticationByHost)
def apply(log: xsbti.Logger, resolvers: Vector[sbt.librarymanagement.Resolver], reorderResolvers: Boolean, parallelDownloads: Int, maxIterations: Int, sbtScalaOrganization: String, sbtScalaVersion: String, sbtScalaJars: Vector[java.io.File], interProjectDependencies: Vector[coursier.core.Project], excludeDependencies: Vector[(String, String)], fallbackDependencies: Vector[coursier.lmcoursier.FallbackDependency], autoScalaLibrary: Boolean, hasClassifiers: Boolean, classifiers: Vector[String], mavenProfiles: Vector[String], scalaOrganization: String, scalaVersion: String, authenticationByRepositoryId: Vector[(String, coursier.core.Authentication)], authenticationByHost: Vector[(String, coursier.core.Authentication)]): CoursierConfiguration = new CoursierConfiguration(Option(log), resolvers, reorderResolvers, parallelDownloads, maxIterations, Option(sbtScalaOrganization), Option(sbtScalaVersion), sbtScalaJars, interProjectDependencies, excludeDependencies, fallbackDependencies, autoScalaLibrary, hasClassifiers, classifiers, mavenProfiles, Option(scalaOrganization), Option(scalaVersion), authenticationByRepositoryId, authenticationByHost)
}

View File

@ -108,6 +108,18 @@
"type": "String?",
"default": "None",
"since": "0.0.1"
},
{
"name": "authenticationByRepositoryId",
"type": "(String, coursier.core.Authentication)*",
"default": "Vector.empty",
"since": "0.0.1"
},
{
"name": "authenticationByHost",
"type": "(String, coursier.core.Authentication)*",
"default": "Vector.empty",
"since": "0.0.1"
}
]
}

View File

@ -4,8 +4,9 @@ import java.io.{File, OutputStreamWriter}
import _root_.coursier.{Artifact, Cache, CachePolicy, FileError, Organization, Resolution, TermDisplay, organizationString}
import _root_.coursier.core.{Classifier, Configuration, ModuleName}
import _root_.coursier.extra.Typelevel
import _root_.coursier.ivy.IvyRepository
import coursier.extra.Typelevel
import _root_.coursier.lmcoursier.Inputs.withAuthenticationByHost
import sbt.internal.librarymanagement.IvySbt
import sbt.librarymanagement._
import sbt.util.Logger
@ -84,15 +85,18 @@ class CoursierDependencyResolution(conf: CoursierConfiguration) extends Dependen
else
None
val authenticationByRepositoryId = conf.authenticationByRepositoryId.toMap
val mainRepositories = resolvers
.flatMap { resolver =>
FromSbt.repository(
resolver,
ivyProperties,
log,
None // FIXME What about authentication?
authenticationByRepositoryId.get(resolver.name)
)
}
.map(withAuthenticationByHost(_, conf.authenticationByHost.toMap))
val globalPluginsRepos =
for (p <- ResolutionParams.globalPluginPatterns(sbtBinaryVersion))

View File

@ -1,10 +1,14 @@
package coursier.lmcoursier
import coursier.core.{Configuration, ModuleName, Organization, Project}
import coursier.Cache
import coursier.core._
import coursier.ivy.IvyRepository
import coursier.maven.MavenRepository
import sbt.librarymanagement.{InclExclRule, ModuleID}
import sbt.util.Logger
import scala.collection.mutable
import scala.util.Try
object Inputs {
@ -146,4 +150,37 @@ object Inputs {
)
}
def withAuthenticationByHost(repo: Repository, credentials: Map[String, Authentication]): Repository = {
def httpHost(s: String) =
if (s.startsWith("http://") || s.startsWith("https://"))
Try(Cache.url(s).getHost).toOption
else
None
repo match {
case m: MavenRepository =>
if (m.authentication.isEmpty)
httpHost(m.root).flatMap(credentials.get).fold(m) { auth =>
m.copy(authentication = Some(auth))
}
else
m
case i: IvyRepository =>
if (i.authentication.isEmpty) {
val base = i.pattern.chunks.takeWhile {
case _: coursier.ivy.Pattern.Chunk.Const => true
case _ => false
}.map(_.string).mkString
httpHost(base).flatMap(credentials.get).fold(i) { auth =>
i.copy(authentication = Some(auth))
}
} else
i
case _ =>
repo
}
}
}

View File

@ -67,4 +67,32 @@ object InputsTasks {
}
}
val authenticationByHostTask = Def.taskDyn {
val useSbtCredentials = coursierUseSbtCredentials.value
if (useSbtCredentials)
Def.task {
val log = streams.value.log
sbt.Keys.credentials.value
.flatMap {
case dc: sbt.DirectCredentials => List(dc)
case fc: sbt.FileCredentials =>
sbt.Credentials.loadCredentials(fc.path) match {
case Left(err) =>
log.warn(s"$err, ignoring it")
Nil
case Right(dc) => List(dc)
}
}
.map { c =>
c.host -> Authentication(c.userName, c.passwd)
}
.toMap
}
else
Def.task(Map.empty[String, Authentication])
}
}

View File

@ -1,5 +1,6 @@
package coursier.sbtcoursiershared
import coursier.Credentials
import coursier.core.{Configuration, Project, Publication}
import coursier.lmcoursier.{FallbackDependency, SbtCoursierCache}
import sbt.{AutoPlugin, Classpaths, Compile, Setting, TaskKey, Test, settingKey, taskKey}
@ -29,6 +30,9 @@ object SbtCoursierShared extends AutoPlugin {
val coursierFallbackDependencies = taskKey[Seq[FallbackDependency]]("")
val mavenProfiles = settingKey[Set[String]]("")
val coursierUseSbtCredentials = settingKey[Boolean]("")
val coursierCredentials = taskKey[Map[String, Credentials]]("")
}
import autoImport._
@ -39,7 +43,9 @@ object SbtCoursierShared extends AutoPlugin {
override def buildSettings: Seq[Setting[_]] =
Seq(
coursierReorderResolvers := true,
coursierKeepPreloaded := false
coursierKeepPreloaded := false,
coursierUseSbtCredentials := true,
coursierCredentials := Map.empty
)
private val pluginIvySnapshotsBase = Resolver.SbtRepositoryRoot.stripSuffix("/") + "/ivy-snapshots"

View File

@ -22,8 +22,6 @@ object CoursierPlugin extends AutoPlugin {
val coursierCachePolicies = Keys.coursierCachePolicies
val coursierTtl = Keys.coursierTtl
val coursierVerbosity = Keys.coursierVerbosity
val coursierUseSbtCredentials = Keys.coursierUseSbtCredentials
val coursierCredentials = Keys.coursierCredentials
val coursierCache = Keys.coursierCache
val coursierConfigGraphs = Keys.coursierConfigGraphs
val coursierSbtClassifiersModule = Keys.coursierSbtClassifiersModule
@ -194,8 +192,6 @@ object CoursierPlugin extends AutoPlugin {
coursierCachePolicies := CachePolicy.default,
coursierTtl := Cache.defaultTtl,
coursierVerbosity := Settings.defaultVerbosityLevel(sLog.value),
coursierUseSbtCredentials := true,
coursierCredentials := Map.empty,
coursierCache := Cache.default,
coursierCreateLogger := { () => new TermDisplay(new OutputStreamWriter(System.err)) }
)

View File

@ -21,9 +21,6 @@ object Keys {
val coursierVerbosity = SettingKey[Int]("coursier-verbosity")
val coursierUseSbtCredentials = SettingKey[Boolean]("coursier-use-sbt-credentials")
val coursierCredentials = TaskKey[Map[String, Credentials]]("coursier-credentials")
val coursierCache = SettingKey[File]("coursier-cache")
val coursierConfigGraphs = TaskKey[Seq[Set[Configuration]]]("coursier-config-graphs")

View File

@ -1,20 +1,17 @@
package coursier.sbtcoursier
import java.net.URL
import coursier.{Cache, ProjectCache}
import coursier.ProjectCache
import coursier.core._
import coursier.extra.Typelevel
import coursier.ivy.IvyRepository
import coursier.lmcoursier._
import coursier.maven.MavenRepository
import coursier.lmcoursier.Inputs.withAuthenticationByHost
import coursier.sbtcoursier.Keys._
import coursier.sbtcoursiershared.InputsTasks.authenticationByHostTask
import coursier.sbtcoursiershared.SbtCoursierShared.autoImport._
import sbt.Def
import sbt.Keys._
import scala.util.Try
object ResolutionTasks {
def resolutionsTask(
@ -49,34 +46,6 @@ object ResolutionTasks {
else
Def.task(coursierRecursiveResolvers.value.distinct)
val authenticationByHostTask = Def.taskDyn {
val useSbtCredentials = coursierUseSbtCredentials.value
if (useSbtCredentials)
Def.task {
val log = streams.value.log
sbt.Keys.credentials.value
.flatMap {
case dc: sbt.DirectCredentials => List(dc)
case fc: sbt.FileCredentials =>
sbt.Credentials.loadCredentials(fc.path) match {
case Left(err) =>
log.warn(s"$err, ignoring it")
Nil
case Right(dc) => List(dc)
}
}
.map { c =>
c.host -> Authentication(c.userName, c.passwd)
}
.toMap
}
else
Def.task(Map.empty[String, Authentication])
}
Def.task {
val projectName = thisProjectRef.value.project
@ -141,39 +110,6 @@ object ResolutionTasks {
.map(_.foldLeft[ProjectCache](Map.empty)(_ ++ _))
.getOrElse(Map.empty)
def withAuthenticationByHost(repo: Repository, credentials: Map[String, Authentication]): Repository = {
def httpHost(s: String) =
if (s.startsWith("http://") || s.startsWith("https://"))
Try(Cache.url(s).getHost).toOption
else
None
repo match {
case m: MavenRepository =>
if (m.authentication.isEmpty)
httpHost(m.root).flatMap(credentials.get).fold(m) { auth =>
m.copy(authentication = Some(auth))
}
else
m
case i: IvyRepository =>
if (i.authentication.isEmpty) {
val base = i.pattern.chunks.takeWhile {
case _: coursier.ivy.Pattern.Chunk.Const => true
case _ => false
}.map(_.string).mkString
httpHost(base).flatMap(credentials.get).fold(i) { auth =>
i.copy(authentication = Some(auth))
}
} else
i
case _ =>
repo
}
}
val mainRepositories = resolvers
.flatMap { resolver =>
FromSbt.repository(

View File

@ -1 +0,0 @@
> coursierResolutions

View File

@ -1 +0,0 @@
> coursierResolutions

View File

@ -0,0 +1 @@
> update

View File

@ -0,0 +1 @@
> update

View File

@ -0,0 +1 @@
> update

View File

@ -2,6 +2,7 @@ package coursier.sbtlmcoursier
import coursier.core.Classifier
import coursier.lmcoursier.{CoursierConfiguration, CoursierDependencyResolution, Inputs}
import coursier.sbtcoursiershared.InputsTasks.authenticationByHostTask
import coursier.sbtcoursiershared.SbtCoursierShared
import sbt.{AutoPlugin, Classpaths, Def, Setting, Task, taskKey}
import sbt.Project.inTask
@ -76,6 +77,9 @@ object LmCoursierPlugin extends AutoPlugin {
val autoScalaLib = autoScalaLibrary.value
val profiles = mavenProfiles.value
val authenticationByRepositoryId = coursierCredentials.value.mapValues(_.authentication)
val authenticationByHost = authenticationByHostTask.value
val internalSbtScalaProvider = appConfiguration.value.provider.scalaProvider
val sbtBootJars = internalSbtScalaProvider.jars()
val sbtScalaVersion = internalSbtScalaProvider.version()
@ -105,6 +109,8 @@ object LmCoursierPlugin extends AutoPlugin {
.withMavenProfiles(profiles.toVector.sorted)
.withScalaOrganization(scalaOrg)
.withScalaVersion(scalaVer)
.withAuthenticationByRepositoryId(authenticationByRepositoryId.toVector.sortBy(_._1))
.withAuthenticationByHost(authenticationByHost.toVector.sortBy(_._1))
.withLog(s.log)
}
}