diff --git a/build.sbt b/build.sbt index 8cb4a343f..33a410a47 100644 --- a/build.sbt +++ b/build.sbt @@ -37,8 +37,9 @@ lazy val lmRoot = (project in file(".")) Seq( homepage := Some(url("https://github.com/sbt/librarymanagement")), description := "Library management module for sbt", - scmInfo := Some(ScmInfo(url("https://github.com/sbt/librarymanagement"), - "git@github.com:sbt/librarymanagement.git")), + scmInfo := Some(ScmInfo( + url("https://github.com/sbt/librarymanagement"), "git@github.com:sbt/librarymanagement.git" + )), bintrayPackage := "librarymanagement", scalafmtOnCompile := true, // scalafmtVersion 1.0.0-RC3 has regression @@ -63,21 +64,17 @@ lazy val lm = (project in file("librarymanagement")) .settings( commonSettings, name := "librarymanagement", - libraryDependencies ++= Seq(ivy, - jsch, - scalaReflect.value, - launcherInterface, - gigahorseOkhttp, - okhttpUrlconnection, - sjsonnewScalaJson.value % Optional), + libraryDependencies ++= Seq( + ivy, jsch, scalaReflect.value, launcherInterface, gigahorseOkhttp, okhttpUrlconnection, + sjsonnewScalaJson.value % Optional, + scalaTest + ), libraryDependencies ++= scalaXml.value, - resourceGenerators in Compile += Def - .task( - Util.generateVersionFile(version.value, - resourceManaged.value, - streams.value, - (compile in Compile).value)) - .taskValue, + resourceGenerators in Compile += Def.task( + Util.generateVersionFile( + version.value, resourceManaged.value, streams.value, (compile in Compile).value + ) + ).taskValue, // mimaBinaryIssueFilters ++= Seq(), managedSourceDirectories in Compile += baseDirectory.value / "src" / "main" / "contraband-scala", @@ -91,19 +88,14 @@ lazy val lm = (project in file("librarymanagement")) (((srcs --- sdirs --- base) pair (relativeTo(sdirs) | relativeTo(base) | flat)) toSeq) } ) - .configure(addSbtIO, - addSbtUtilLogging, - addSbtUtilTesting, - addSbtUtilCollection, - addSbtUtilCompletion, - addSbtUtilCache) + .configure(addSbtIO, addSbtUtilLogging, addSbtUtilPosition, addSbtUtilCache) .enablePlugins(ContrabandPlugin, JsonCodecPlugin) def customCommands: Seq[Setting[_]] = Seq( commands += Command.command("release") { state => // "clean" :: - "so compile" :: - "so publishSigned" :: + "+compile" :: + "+publishSigned" :: "reload" :: state } diff --git a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/JsonUtil.scala b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/JsonUtil.scala index 19c835393..2780d5e12 100644 --- a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/JsonUtil.scala +++ b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/JsonUtil.scala @@ -1,11 +1,10 @@ package sbt.internal.librarymanagement import java.io.File -import org.apache.ivy.core -import core.module.descriptor.ModuleDescriptor +import org.apache.ivy.core.module.descriptor.ModuleDescriptor +import sbt.io.IO import sbt.util.{ CacheStore, Logger } -import sbt.librarymanagement._ -import sbt.librarymanagement.LibraryManagementCodec._ +import sbt.librarymanagement._, LibraryManagementCodec._ private[sbt] object JsonUtil { def sbtOrgTemp = "org.scala-sbt.temp" @@ -22,14 +21,17 @@ private[sbt] object JsonUtil { fromLite(lite, cachedDescriptor) } catch { case e: Throwable => - log.error("Unable to parse mini graph: " + path.toString) + log.error(s"Unable to parse mini graph: $path") throw e } } + def writeUpdateReport(ur: UpdateReport, graphPath: File): Unit = { - sbt.io.IO.createDirectory(graphPath.getParentFile) - CacheStore(graphPath).write(toLite(ur)) + val updateReportLite = toLite(ur) + IO.createDirectory(graphPath.getParentFile) + CacheStore(graphPath).write(updateReportLite) } + def toLite(ur: UpdateReport): UpdateReportLite = UpdateReportLite(ur.configurations map { cr => ConfigurationReportLite( @@ -65,6 +67,7 @@ private[sbt] object JsonUtil { } ) }) + // #1763/#2030. Caller takes up 97% of space, so we need to shrink it down, // but there are semantics associated with some of them. def filterOutArtificialCallers(callers: Vector[Caller]): Vector[Caller] = diff --git a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/RepositoriesParser.scala b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/RepositoriesParser.scala deleted file mode 100644 index a10741b64..000000000 --- a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/RepositoriesParser.scala +++ /dev/null @@ -1,117 +0,0 @@ -package sbt -package internal -package librarymanagement - -import java.io.File -import java.net.URL - -import scala.io.Source -import sbt.internal.util.complete.Parser -import sbt.internal.util.complete.DefaultParsers._ - -private[sbt] object RepositoriesParser { - - private case class AfterPattern(artifactPattern: Option[String], flags: Int) - final case class PredefinedRepository(override val id: xsbti.Predefined) - extends xsbti.PredefinedRepository - final case class MavenRepository(override val id: String, override val url: URL) - extends xsbti.MavenRepository - final case class IvyRepository( - override val id: String, - override val url: URL, - override val ivyPattern: String, - override val artifactPattern: String, - override val mavenCompatible: Boolean, - override val skipConsistencyCheck: Boolean, - override val descriptorOptional: Boolean, - val bootOnly: Boolean - ) extends xsbti.IvyRepository - - // Predefined repositories - def local: Parser[xsbti.Repository] = - "local" ^^^ new PredefinedRepository(xsbti.Predefined.Local) - def mavenLocal: Parser[xsbti.Repository] = - "maven-local" ^^^ new PredefinedRepository(xsbti.Predefined.MavenLocal) - def mavenCentral: Parser[xsbti.Repository] = - "maven-central" ^^^ new PredefinedRepository(xsbti.Predefined.MavenCentral) - def predefinedResolver: Parser[xsbti.Repository] = local | mavenLocal | mavenCentral - - // Options - def descriptorOptional: Parser[Int] = "descriptorOptional" ^^^ Flags.descriptorOptionalFlag - def skipConsistencyCheck: Parser[Int] = "skipConsistencyCheck" ^^^ Flags.skipConsistencyCheckFlag - def bootOnly: Parser[Int] = "bootOnly" ^^^ Flags.bootOnlyFlag - def mavenCompatible: Parser[Int] = "mavenCompatible" ^^^ Flags.mavenCompatibleFlag - - def option: Parser[Int] = descriptorOptional | skipConsistencyCheck | bootOnly | mavenCompatible - def options: Parser[Int] = rep1sep(option, separator) map (_ reduce (_ | _)) - - def name: Parser[String] = ID - def separator: Parser[String] = "," ~> charClass(c => c == ' ' || c == '\t').*.string - def nonComma: Parser[String] = charClass(_ != ',').*.string - def ivyPattern: Parser[String] = nonComma - def artifactPattern: Parser[String] = nonComma - private def afterPattern: Parser[AfterPattern] = { - def onlyOptions = options map (AfterPattern(None, _)) - def both = artifactPattern ~ (separator ~> options).? map { - case ap ~ opts => AfterPattern(Some(ap), opts getOrElse 0) - } - onlyOptions | both - } - - def customResolver: Parser[xsbti.Repository] = - name ~ ": " ~ basicUri ~ (separator ~> ivyPattern).? ~ (separator ~> afterPattern).? map { - case name ~ ": " ~ uri ~ None ~ _ => - new MavenRepository(name, uri.toURL) - case name ~ ": " ~ uri ~ Some(ivy) ~ ap => - // scalac complains about the recursion depth if we pattern match over `ap` directly. - ap match { - case Some(AfterPattern(artifactPattern, Flags(dOpt, sc, bo, mc))) => - new IvyRepository( - name, - uri.toURL, - ivy, - artifactPattern getOrElse ivy, - mc, - sc, - dOpt, - bo - ) - case None => - new IvyRepository(name, uri.toURL, ivy, ivy, false, false, false, false) - } - } - - def resolver: Parser[xsbti.Repository] = - predefinedResolver | customResolver - - def getResolver[T](in: String)(parser: Parser[T]): Option[T] = - Parser.parse(in.trim, parser).right.toOption - - def apply(lines: Iterator[String]): Seq[xsbti.Repository] = - if (lines.isEmpty) Nil - else { - if (lines.next != "[repositories]") - throw new Exception("Repositories file must start with '[repositories]'") - lines.flatMap(getResolver(_)(resolver)).toList - } - def apply(str: String): Seq[xsbti.Repository] = apply(str.lines) - def apply(file: File): Seq[xsbti.Repository] = { - if (!file.exists) Nil - else apply(Source.fromFile(file).getLines) - } - - object Flags { - val descriptorOptionalFlag = 1 << 0 - val skipConsistencyCheckFlag = 1 << 1 - val bootOnlyFlag = 1 << 2 - val mavenCompatibleFlag = 1 << 3 - - def unapply(flags: Int): Some[(Boolean, Boolean, Boolean, Boolean)] = { - val dOpt = (flags & descriptorOptionalFlag) != 0 - val sc = (flags & skipConsistencyCheckFlag) != 0 - val bo = (flags & bootOnlyFlag) != 0 - val mc = (flags & mavenCompatibleFlag) != 0 - Some((dOpt, sc, bo, mc)) - } - } -} diff --git a/librarymanagement/src/test/scala/CrossVersionTest.scala b/librarymanagement/src/test/scala/CrossVersionTest.scala index 376a17cb1..b2c52a732 100644 --- a/librarymanagement/src/test/scala/CrossVersionTest.scala +++ b/librarymanagement/src/test/scala/CrossVersionTest.scala @@ -1,6 +1,6 @@ package sbt.librarymanagement -import sbt.internal.util.UnitSpec +import sbt.internal.librarymanagement.UnitSpec import CrossVersion._ class CrossVersionTest extends UnitSpec { diff --git a/librarymanagement/src/test/scala/DMSerializationSpec.scala b/librarymanagement/src/test/scala/DMSerializationSpec.scala index 06c178d8e..35c0f7c01 100644 --- a/librarymanagement/src/test/scala/DMSerializationSpec.scala +++ b/librarymanagement/src/test/scala/DMSerializationSpec.scala @@ -3,7 +3,7 @@ package sbt.librarymanagement import java.net.URL import java.io.File -import sbt.internal._, librarymanagement._, util.UnitSpec +import sbt.internal._, librarymanagement._ import scalajson.ast.unsafe._ import sjsonnew._, support.scalajson.unsafe._ import org.scalatest.Assertion diff --git a/librarymanagement/src/test/scala/MergeDescriptorSpec.scala b/librarymanagement/src/test/scala/MergeDescriptorSpec.scala index dddeadfdd..b435027bd 100644 --- a/librarymanagement/src/test/scala/MergeDescriptorSpec.scala +++ b/librarymanagement/src/test/scala/MergeDescriptorSpec.scala @@ -1,6 +1,6 @@ package sbt.internal.librarymanagement -import org.apache.ivy.core.module.descriptor.{ DependencyArtifactDescriptor } +import org.apache.ivy.core.module.descriptor.DependencyArtifactDescriptor import sbt.librarymanagement._ import sbt.internal.librarymanagement.ivyint._ diff --git a/librarymanagement/src/test/scala/ModuleIdTest.scala b/librarymanagement/src/test/scala/ModuleIdTest.scala index 8b1d7d7b7..a9bab61f0 100644 --- a/librarymanagement/src/test/scala/ModuleIdTest.scala +++ b/librarymanagement/src/test/scala/ModuleIdTest.scala @@ -1,6 +1,6 @@ package sbt.librarymanagement -import sbt.internal.util.UnitSpec +import sbt.internal.librarymanagement.UnitSpec class ModuleIdTest extends UnitSpec { "Module Id" should "return cross-disabled module id as equal to a copy" in { diff --git a/librarymanagement/src/test/scala/OfflineModeSpec.scala b/librarymanagement/src/test/scala/OfflineModeSpec.scala index ea6b5f1d7..f4747a7f9 100644 --- a/librarymanagement/src/test/scala/OfflineModeSpec.scala +++ b/librarymanagement/src/test/scala/OfflineModeSpec.scala @@ -3,7 +3,7 @@ package sbt.librarymanagement import org.scalatest.Assertion import sbt.internal.librarymanagement._ import sbt.internal.librarymanagement.impl.DependencyBuilders -import sbt.io.{ FileFilter, IO, Path } +import sbt.io.IO class OfflineModeSpec extends BaseIvySpecification with DependencyBuilders { private final def targetDir = Some(currentDependency) @@ -40,7 +40,7 @@ class OfflineModeSpec extends BaseIvySpecification with DependencyBuilders { // Compute an estimate to ensure that the second resolution does indeed use the cache val originalResolveTime = onlineResolution.right.get.stats.resolveTime - val estimatedCachedTime = originalResolveTime * 0.15 + val estimatedCachedTime = originalResolveTime * 0.3 val offlineResolution = IvyActions.updateEither(toResolve, offlineConf, warningConf, noClock, targetDir, log) diff --git a/librarymanagement/src/test/scala/ResolverTest.scala b/librarymanagement/src/test/scala/ResolverTest.scala index 2682797fa..d98eab799 100644 --- a/librarymanagement/src/test/scala/ResolverTest.scala +++ b/librarymanagement/src/test/scala/ResolverTest.scala @@ -2,7 +2,7 @@ package sbt.librarymanagement import java.net.URL -import sbt.internal.util.UnitSpec +import sbt.internal.librarymanagement.UnitSpec object ResolverTest extends UnitSpec { diff --git a/librarymanagement/src/test/scala/ScalaOverrideTest.scala b/librarymanagement/src/test/scala/ScalaOverrideTest.scala index 2417db786..bb872438d 100644 --- a/librarymanagement/src/test/scala/ScalaOverrideTest.scala +++ b/librarymanagement/src/test/scala/ScalaOverrideTest.scala @@ -3,7 +3,7 @@ package sbt.librarymanagement import org.apache.ivy.core.module.id.ModuleRevisionId import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor -import sbt.internal.util.UnitSpec +import sbt.internal.librarymanagement.UnitSpec import IvyScala.OverrideScalaMediator import ScalaArtifacts._ diff --git a/librarymanagement/src/test/scala/UpdateOptionsSpec.scala b/librarymanagement/src/test/scala/UpdateOptionsSpec.scala index 8a152bb13..eaf61fe29 100644 --- a/librarymanagement/src/test/scala/UpdateOptionsSpec.scala +++ b/librarymanagement/src/test/scala/UpdateOptionsSpec.scala @@ -1,6 +1,6 @@ package sbt.librarymanagement -import sbt.internal.util.UnitSpec +import sbt.internal.librarymanagement.UnitSpec class UpdateOptionsSpec extends UnitSpec { diff --git a/librarymanagement/src/test/scala/VersionNumberSpec.scala b/librarymanagement/src/test/scala/VersionNumberSpec.scala index 66fd96f61..5a1c27a7a 100644 --- a/librarymanagement/src/test/scala/VersionNumberSpec.scala +++ b/librarymanagement/src/test/scala/VersionNumberSpec.scala @@ -1,6 +1,6 @@ package sbt.librarymanagement -import sbt.internal.util.UnitSpec +import sbt.internal.librarymanagement.UnitSpec // This is a specification to check the version number parsing. class VersionNumberSpec extends UnitSpec { diff --git a/librarymanagement/src/test/scala/sbt/internal/librarymanagement/RepositoriesParserSpecification.scala b/librarymanagement/src/test/scala/sbt/internal/librarymanagement/RepositoriesParserSpecification.scala deleted file mode 100644 index 0aec79d39..000000000 --- a/librarymanagement/src/test/scala/sbt/internal/librarymanagement/RepositoriesParserSpecification.scala +++ /dev/null @@ -1,175 +0,0 @@ -package sbt -package internal -package librarymanagement - -import java.net.URL - -/** - * Tests that we can correctly parse repositories definitions. - */ -class RepositoriesParserSpecification extends UnitSpec { - import RepositoriesParser._ - - "The RepositoriesParser" should "check that repositories file starts with [repositories]" in { - val file = """local - |maven-central""".stripMargin - a[Exception] should be thrownBy RepositoriesParser(file) - } - - it should "parse the local repository" in { - val file = """[repositories] - | local""".stripMargin - val repos = RepositoriesParser(file) - repos.size shouldBe 1 - repos(0) shouldBe PredefinedRepository(xsbti.Predefined.Local) - - } - - it should "parse the local maven repository" in { - val file = """[repositories] - | maven-local""".stripMargin - val repos = RepositoriesParser(file) - repos.size shouldBe 1 - repos(0) shouldBe PredefinedRepository(xsbti.Predefined.MavenLocal) - } - - it should "parse Maven Central repository" in { - val file = """[repositories] - | maven-central""".stripMargin - val repos = RepositoriesParser(file) - repos.size shouldBe 1 - repos(0) shouldBe PredefinedRepository(xsbti.Predefined.MavenCentral) - } - - it should "parse simple Maven repository" in { - val file = """[repositories] - | mavenRepo: https://repo1.maven.org""".stripMargin - val repos = RepositoriesParser(file) - repos.size shouldBe 1 - repos(0) shouldBe MavenRepository("mavenRepo", new URL("https://repo1.maven.org")) - } - - it should "parse `bootOnly` option" in { - val file = """[repositories] - | ivyRepo: https://repo1.maven.org, [orgPath], bootOnly""".stripMargin - val repos = RepositoriesParser(file) - val expected = - IvyRepository( - "ivyRepo", - new URL("https://repo1.maven.org"), - "[orgPath]", - "[orgPath]", - mavenCompatible = false, - skipConsistencyCheck = false, - descriptorOptional = false, - bootOnly = true - ) - repos.size shouldBe 1 - repos(0) shouldBe expected - } - - it should "parse `mavenCompatible` option" in { - val file = """[repositories] - | ivyRepo: https://repo1.maven.org, [orgPath], mavenCompatible""".stripMargin - val repos = RepositoriesParser(file) - val expected = - IvyRepository( - "ivyRepo", - new URL("https://repo1.maven.org"), - "[orgPath]", - "[orgPath]", - mavenCompatible = true, - skipConsistencyCheck = false, - descriptorOptional = false, - bootOnly = false - ) - repos.size shouldBe 1 - repos(0) shouldBe expected - } - - it should "parse `skipConsistencyCheck` option" in { - val file = """[repositories] - | ivyRepo: https://repo1.maven.org, [orgPath], skipConsistencyCheck""".stripMargin - val repos = RepositoriesParser(file) - val expected = - IvyRepository( - "ivyRepo", - new URL("https://repo1.maven.org"), - "[orgPath]", - "[orgPath]", - mavenCompatible = false, - skipConsistencyCheck = true, - descriptorOptional = false, - bootOnly = false - ) - repos.size shouldBe 1 - repos(0) shouldBe expected - } - - it should "parse `descriptorOptional` option" in { - val file = """[repositories] - | ivyRepo: https://repo1.maven.org, [orgPath], descriptorOptional""".stripMargin - val repos = RepositoriesParser(file) - val expected = - IvyRepository( - "ivyRepo", - new URL("https://repo1.maven.org"), - "[orgPath]", - "[orgPath]", - mavenCompatible = false, - skipConsistencyCheck = false, - descriptorOptional = true, - bootOnly = false - ) - repos.size shouldBe 1 - repos(0) shouldBe expected - } - - it should "parse complex ivy repository definition" in { - val file = - """[repositories] - | ivyRepo: https://repo1.maven.org, [orgPath], [artPath], descriptorOptional, skipConsistencyCheck""".stripMargin - val repos = RepositoriesParser(file) - val expected = - IvyRepository( - "ivyRepo", - new URL("https://repo1.maven.org"), - "[orgPath]", - "[artPath]", - mavenCompatible = false, - skipConsistencyCheck = true, - descriptorOptional = true, - bootOnly = false - ) - repos.size shouldBe 1 - repos(0) shouldBe expected - } - - it should "parse multiple repositories defined together" in { - val file = - """[repositories] - | local - | ivyRepo: https://repo1.maven.org, [orgPath], [artPath], descriptorOptional, skipConsistencyCheck - | mavenRepo: https://repo1.maven.org""".stripMargin - val expected0 = PredefinedRepository(xsbti.Predefined.Local) - val expected1 = - IvyRepository( - "ivyRepo", - new URL("https://repo1.maven.org"), - "[orgPath]", - "[artPath]", - mavenCompatible = false, - skipConsistencyCheck = true, - descriptorOptional = true, - bootOnly = false - ) - val expected2 = MavenRepository("mavenRepo", new URL("https://repo1.maven.org")) - - val repos = RepositoriesParser(file) - repos.size shouldBe 3 - repos(0) shouldBe expected0 - repos(1) shouldBe expected1 - repos(2) shouldBe expected2 - } - -} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 8567c1012..73f3d42c3 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -7,14 +7,12 @@ object Dependencies { val scala212 = "2.12.2" private val ioVersion = "1.0.0-M12" - private val utilVersion = "1.0.0-M25" + private val utilVersion = "1.0.0-M26" private val sbtIO = "org.scala-sbt" %% "io" % ioVersion - private val utilCollection = "org.scala-sbt" %% "util-collection" % utilVersion + private val utilPosition = "org.scala-sbt" %% "util-position" % utilVersion private val utilLogging = "org.scala-sbt" %% "util-logging" % utilVersion - private val utilTesting = "org.scala-sbt" %% "util-testing" % utilVersion - private val utilCompletion = "org.scala-sbt" %% "util-completion" % utilVersion private val utilCache = "org.scala-sbt" %% "util-cache" % utilVersion def getSbtModulePath(key: String, name: String) = { @@ -28,27 +26,17 @@ object Dependencies { lazy val sbtIoPath = getSbtModulePath("sbtio.path", "sbt/io") lazy val sbtUtilPath = getSbtModulePath("sbtutil.path", "sbt/util") - def addSbtModule(p: Project, - path: Option[String], - projectName: String, - m: ModuleID, - c: Option[Configuration] = None) = + def addSbtModule(p: Project, path: Option[String], projectName: String, m: ModuleID) = path match { - case Some(f) => - p dependsOn c.fold[ClasspathDep[ProjectReference]](ProjectRef(file(f), projectName))( - ProjectRef(file(f), projectName) % _) - case None => p settings (libraryDependencies += c.fold(m)(m % _)) + case Some(f) => p dependsOn ProjectRef(file(f), projectName) + case None => p settings (libraryDependencies += m) } def addSbtIO(p: Project): Project = addSbtModule(p, sbtIoPath, "io", sbtIO) - def addSbtUtilCollection(p: Project): Project = - addSbtModule(p, sbtUtilPath, "utilCollection", utilCollection) + def addSbtUtilPosition(p: Project): Project = + addSbtModule(p, sbtUtilPath, "utilPosition", utilPosition) def addSbtUtilLogging(p: Project): Project = addSbtModule(p, sbtUtilPath, "utilLogging", utilLogging) - def addSbtUtilTesting(p: Project): Project = - addSbtModule(p, sbtUtilPath, "utilTesting", utilTesting, Some(Test)) - def addSbtUtilCompletion(p: Project): Project = - addSbtModule(p, sbtUtilPath, "utilComplete", utilCompletion) def addSbtUtilCache(p: Project): Project = addSbtModule(p, sbtUtilPath, "utilCache", utilCache) val launcherInterface = "org.scala-sbt" % "launcher-interface" % "1.0.0" @@ -56,6 +44,7 @@ object Dependencies { val jsch = "com.jcraft" % "jsch" % "0.1.46" intransitive () val scalaReflect = Def.setting { "org.scala-lang" % "scala-reflect" % scalaVersion.value } val scalaXml = scala211Module("scala-xml", "1.0.5") + val scalaTest = "org.scalatest" %% "scalatest" % "3.0.1" % Test val sjsonnew = Def.setting { "com.eed3si9n" %% "sjson-new-core" % contrabandSjsonNewVersion.value } val sjsonnewScalaJson = Def.setting { "com.eed3si9n" %% "sjson-new-scalajson" % contrabandSjsonNewVersion.value } val gigahorseOkhttp = "com.eed3si9n" %% "gigahorse-okhttp" % "0.3.0"