From 79d6e099a6f3596081407097b19f1ca889e8ef31 Mon Sep 17 00:00:00 2001 From: Eric Peters Date: Mon, 10 Jan 2022 14:41:44 -0800 Subject: [PATCH] Add maven-metadata.xml handling --- .github/workflows/build.yml | 8 +- .gitignore | 3 + build.sbt | 2 + src/main/resources/log4j.properties.debug | 8 + src/main/scala-sbt-0.13/fm/sbt/Compat.scala | 15 ++ src/main/scala-sbt-1.0/fm/sbt/Compat.scala | 15 ++ .../coursier/cache/protocol/S3Handler.scala | 13 ++ src/main/scala/fm/sbt/S3URLHandler.scala | 20 ++- src/main/scala/fm/sbt/package.scala | 10 ++ .../scala/fm/sbt/s3/S3MavenMetadata.scala | 167 ++++++++++++++++++ src/main/scala/fm/sbt/s3/S3Response.scala | 60 +++++++ .../scala/fm/sbt/s3/S3URLConnection.scala | 51 ++++-- .../publish_and_resolve/build.sbt | 39 +++- .../publish_and_resolve/project/plugins.sbt | 14 +- .../publish_and_resolve/test | 1 + 15 files changed, 392 insertions(+), 34 deletions(-) create mode 100644 src/main/resources/log4j.properties.debug create mode 100644 src/main/scala-sbt-0.13/fm/sbt/Compat.scala create mode 100644 src/main/scala-sbt-1.0/fm/sbt/Compat.scala create mode 100644 src/main/scala/coursier/cache/protocol/S3Handler.scala create mode 100644 src/main/scala/fm/sbt/package.scala create mode 100644 src/main/scala/fm/sbt/s3/S3MavenMetadata.scala create mode 100644 src/main/scala/fm/sbt/s3/S3Response.scala diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59094b1..b1bc17b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,7 +91,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} run: | # install LocalStack cli and awslocal - pip install localstack awscli-local[ver1] + pip3 install localstack 'awscli-local[ver1]' # Make sure to pull the latest version of the image docker pull localstack/localstack # Start LocalStack in the background @@ -100,7 +100,7 @@ jobs: echo "Waiting for LocalStack startup..." localstack wait -t 30 echo "LocalStack Startup complete" - awslocal s3api create-bucket --bucket fm-sbt-s3-resolver-example-bucket + awslocal s3api create-bucket --bucket fm-sbt-s3-resolver-example-bucket --create-bucket-configuration LocationConstraint=us-west-2 # This will publishLocal for all crossSbtVersions then test example apps # using the matrix.sbt version. For example we will publish the 0.13 and # 1.0 compatible versions of the plugin (via our configured @@ -110,9 +110,9 @@ jobs: env: # LocalStack Notes: # - Seems to want Path Style Access (which is deprecated in AWS S3) - # - Seems to want the service endpoint to contain the bucket name (even with path style access) AWS_ACCESS_KEY_ID: test AWS_SECRET_KEY: test S3_PATH_STYLE_ACCESS: true - S3_SERVICE_ENDPOINT: http://fm-sbt-s3-resolver-example-bucket.s3.us-west-2.localhost.localstack.cloud:4566 + S3_SERVICE_ENDPOINT: http://s3.us-west-2.localhost.localstack.cloud:4566 S3_SIGNING_REGION: us-west-2 + S3_FORCE_GLOBAL_BUCKET_ACCESS: false diff --git a/.gitignore b/.gitignore index e3eeb63..113926e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,16 @@ +.bloop .bsp .cache .cache-main .classpath .idea .idea_modules +.metals .project .scala_dependencies .settings .target .worksheet +.vscode target diff --git a/build.sbt b/build.sbt index c6206fe..b117c09 100644 --- a/build.sbt +++ b/build.sbt @@ -38,6 +38,8 @@ libraryDependencies ++= Seq( "com.amazonaws" % "aws-java-sdk-s3" % amazonSDKVersion, "com.amazonaws" % "aws-java-sdk-sts" % amazonSDKVersion, "org.apache.ivy" % "ivy" % "2.5.0", + // Uncomment, and rename "src/main/resources/log4j.properties.debug" to "log4j.properties" to enable wire debugging + //"log4j" % "log4j" % "1.2.17", "org.scalatest" %% "scalatest" % "3.2.10" % Test ) diff --git a/src/main/resources/log4j.properties.debug b/src/main/resources/log4j.properties.debug new file mode 100644 index 0000000..f8edc8f --- /dev/null +++ b/src/main/resources/log4j.properties.debug @@ -0,0 +1,8 @@ +log4j.rootLogger=WARN, A1 +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n +# Log all HTTP content (headers, parameters, content, etc) for +# all requests and responses. Use caution with this since it can +# be very expensive to log such verbose data! +log4j.logger.org.apache.http.wire=DEBUG \ No newline at end of file diff --git a/src/main/scala-sbt-0.13/fm/sbt/Compat.scala b/src/main/scala-sbt-0.13/fm/sbt/Compat.scala new file mode 100644 index 0000000..bf8af03 --- /dev/null +++ b/src/main/scala-sbt-0.13/fm/sbt/Compat.scala @@ -0,0 +1,15 @@ +package fm.sbt + +object Compat extends Compat + +trait Compat { + type Logger = sbt.Logger + val Logger = sbt.Logger + + val Level = sbt.Level + + type ConsoleLogger = sbt.ConsoleLogger + val ConsoleLogger = sbt.ConsoleLogger + + val Using = sbt.Using +} \ No newline at end of file diff --git a/src/main/scala-sbt-1.0/fm/sbt/Compat.scala b/src/main/scala-sbt-1.0/fm/sbt/Compat.scala new file mode 100644 index 0000000..ac74080 --- /dev/null +++ b/src/main/scala-sbt-1.0/fm/sbt/Compat.scala @@ -0,0 +1,15 @@ +package fm.sbt + +object Compat extends Compat + +trait Compat { + type Logger = sbt.util.Logger + val Logger = sbt.util.Logger + + val Level = sbt.util.Level + + type ConsoleLogger = sbt.internal.util.ConsoleLogger + val ConsoleLogger = sbt.internal.util.ConsoleLogger + + val Using = sbt.io.Using +} \ No newline at end of file diff --git a/src/main/scala/coursier/cache/protocol/S3Handler.scala b/src/main/scala/coursier/cache/protocol/S3Handler.scala new file mode 100644 index 0000000..734850e --- /dev/null +++ b/src/main/scala/coursier/cache/protocol/S3Handler.scala @@ -0,0 +1,13 @@ +package coursier.cache.protocol + +import java.net.{URLStreamHandler, URLStreamHandlerFactory} + +/** + * This class must be named coursier.cache.protocol.{protocol.capitalize}Handler + */ +class S3Handler extends URLStreamHandlerFactory { + def createURLStreamHandler(protocol: String): URLStreamHandler = protocol match { + case "s3" => new fm.sbt.s3.Handler + case _ => null + } +} \ No newline at end of file diff --git a/src/main/scala/fm/sbt/S3URLHandler.scala b/src/main/scala/fm/sbt/S3URLHandler.scala index c55eb29..eb011f7 100644 --- a/src/main/scala/fm/sbt/S3URLHandler.scala +++ b/src/main/scala/fm/sbt/S3URLHandler.scala @@ -225,6 +225,11 @@ object S3URLHandler { if (cname.isEmpty || cname.exists{ matches.contains(_) }) matches else getDNSAliasesForHost(cname.get, cname.get :: matches) } + + def getEnvOrProp(key: String): Option[String] = { + sys.props.get(key.replaceAllLiterally("_", ".").toLowerCase) orElse + sys.env.get(key) + } } /** @@ -286,22 +291,27 @@ final class S3URLHandler extends URLHandler { if (null == client) { // This allows you to change the S3 endpoint and signing region to point to a non-aws S3 implementation (e.g. LocalStack). val endpointConfiguration: Option[EndpointConfiguration] = for { - serviceEndpoint: String <- Option(System.getenv("S3_SERVICE_ENDPOINT")) - signingRegion: String <- Option(System.getenv("S3_SIGNING_REGION")) + serviceEndpoint: String <- getEnvOrProp("S3_SERVICE_ENDPOINT") + signingRegion: String <- getEnvOrProp("S3_SIGNING_REGION") } yield new EndpointConfiguration(serviceEndpoint, signingRegion) // Path Style Access is deprecated by Amazon S3 but LocalStack seems to want to use it - val pathStyleAccess: Boolean = Option(System.getenv("S3_PATH_STYLE_ACCESS")).map{ _.toBoolean }.getOrElse(false) + val pathStyleAccess: Boolean = getEnvOrProp("S3_PATH_STYLE_ACCESS").map{ _.toBoolean }.getOrElse(false) + + // This can cause problems in LocalStack trying to lookup via s3.amazonaws.com + val forceGlobalBucketAccess: Boolean = getEnvOrProp("S3_FORCE_GLOBAL_BUCKET_ACCESS").map{ _.toBoolean }.getOrElse(true) val tmp: AmazonS3ClientBuilder = AmazonS3Client.builder() .withCredentials(getCredentialsProvider(bucket)) .withClientConfiguration(getProxyConfiguration) - .withForceGlobalBucketAccessEnabled(true) + .withForceGlobalBucketAccessEnabled(forceGlobalBucketAccess) .withPathStyleAccessEnabled(pathStyleAccess) // Only one of the endpointConfiguration or region can be set at a time. client = (endpointConfiguration match { - case Some(endpoint) => tmp.withEndpointConfiguration(endpoint) + case Some(endpoint) => + Message.info("S3URLHandler - Using S3 Endpoint: " + endpoint.getServiceEndpoint + ", signingRegion: " + endpoint.getSigningRegion) + tmp.withEndpointConfiguration(endpoint) case None => tmp.withRegion(getRegion(url, bucket)) }).build() diff --git a/src/main/scala/fm/sbt/package.scala b/src/main/scala/fm/sbt/package.scala new file mode 100644 index 0000000..c4552f7 --- /dev/null +++ b/src/main/scala/fm/sbt/package.scala @@ -0,0 +1,10 @@ +package fm + +package object sbt { + val logger = { + import fm.sbt.Compat._ + val l = ConsoleLogger(System.out) + l.setLevel(Level.Info) + l + } +} diff --git a/src/main/scala/fm/sbt/s3/S3MavenMetadata.scala b/src/main/scala/fm/sbt/s3/S3MavenMetadata.scala new file mode 100644 index 0000000..a9c60ef --- /dev/null +++ b/src/main/scala/fm/sbt/s3/S3MavenMetadata.scala @@ -0,0 +1,167 @@ +/* + * Copyright 2014 Frugal Mechanic (http://frugalmechanic.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fm.sbt.s3 + +import com.amazonaws.AmazonServiceException +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.model.{ListObjectsRequest, ObjectListing, S3Object} +import java.nio.charset.StandardCharsets +import java.text.SimpleDateFormat +import java.util.Date +import scala.annotation.tailrec +import scala.xml.NodeSeq +import scala.collection.JavaConverters._ + +/** + * This creates a `maven-metadata.xml` based upon the s3 directory listings in a backward-compatible + * on-demand generation, instead of publishing after release. + */ +object S3MavenMetadata extends fm.sbt.Compat { + private val sha1MessageDigest = java.security.MessageDigest.getInstance("SHA-1") + + def getSha1(client: AmazonS3, bucketName: String, path: String)(implicit logger: Logger): Option[String] = { + getXml(client, bucketName, path).map { contents => + sha1MessageDigest + .digest(contents.getBytes(StandardCharsets.UTF_8)) + .map( b => "%02x".format(b) ) + .mkString + } + } + + /** This gets s3 object listings for the specified path, and returns back a generated maven-metadata.xml file */ + def getXml(client: AmazonS3, bucketName: String, path: String)(implicit logger: Logger): Option[String] = { + val bucketPath = path + .stripPrefix("/") + .stripSuffix("maven-metadata.xml") + .stripSuffix("maven-metadata.xml.sha1") + .stripSuffix("/") + "/" + + val dirListings: Seq[ObjectListing] = + getObjectListings(client, bucketName, bucketPath) + + val lastUpdated: Date = dirListings + .flatMap{ _.getObjectSummaries.asScala.map{ _.getLastModified } } + .sorted + .headOption + .getOrElse(new Date) + + val versions: Seq[String] = for { + obj <- dirListings + key <- obj.getCommonPrefixes.asScala + } yield { + assert(key.startsWith(bucketPath)) + key.stripPrefix(bucketPath).stripSuffix("/") + } + + if (versions.isEmpty) logger.error(s"[S3ResolverPlugin] S3MavenMetadata.getXml($bucketPath) no versions found.") + + for { + latestVersion <- versions.headOption + artifactName = { + val s = bucketPath.stripSuffix("/") + val idx = s.lastIndexOf('/') + s.drop(idx+1) + } + groupId <- getGroupId(client, bucketName, bucketPath, latestVersion, artifactName) + } yield { + makeXml( + groupId = groupId, + artifactName = artifactName, + latestVersion = latestVersion, + versions = versions.map{ v => {v} }, + lastUpdated = lastUpdated + ) + } + } + + @tailrec private def getObjectListingsImpl(client: AmazonS3, bucketName: String, objectListing: ObjectListing, accum: Seq[ObjectListing] = Nil): Seq[ObjectListing] = { + val updatedAccum = accum :+ objectListing + if (!objectListing.isTruncated) updatedAccum + else getObjectListingsImpl(client, bucketName, client.listNextBatchOfObjects(objectListing), updatedAccum) + } + + private def getObjectListings(client: AmazonS3, bucketName: String, bucketPath: String)(implicit logger: Logger): Seq[ObjectListing] = { + tryS3(bucketPath){ + // "/" as a delimiter to be returned only entries in the first level (no recursion), + // with (pseudo) sub-directories indeed ending with a "/" + val req = new ListObjectsRequest(bucketName, bucketPath, null, "/", null) + getObjectListingsImpl(client, bucketName, client.listObjects(req)) + }.getOrElse(Nil) + } + + private def tryS3[T](path: String)(f: => T)(implicit logger: Logger): Option[T] = { + try { + Option(f) + } catch { + case ex: AmazonServiceException if ex.getStatusCode == 404 => + logger.error(s"[S3ResolverPlugin] S3MavenMetadata.tryS3($path) not found.") + None + + } + } + + private def getGroupId( + client: AmazonS3, + bucketName: String, + path: String, + latestVersion: String, + artifactName: String + )(implicit logger: Logger): Option[String] = { + val artifactPom = artifactName + "-" + latestVersion + ".pom" + val key = path + latestVersion + "/" + artifactPom + + val pomObject: S3Object = tryS3(key) { + client.getObject(bucketName, key) + }.getOrElse(throw new IllegalStateException("[S3ResolverPlugin] S3MavenMetadata.getGroupId() - could not find pom for artifact: " + artifactName + ", version: " + latestVersion + " (s3 path: " + key + ")")) + + val pomContent = + Using + .wrap{ identity[S3Object] } + .apply(pomObject) { obj => + sbt.IO.readStream(obj.getObjectContent) + } + + try { + val pomXML = scala.xml.XML.loadString(pomContent) + Some((pomXML \ "groupId").text) + } catch { + case ex: IllegalArgumentException => + logger.error("[S3ResolverPlugin] S3MavenMetadata.getGroupId() - artifact pom: " +artifactPom + " did not contain 'groupId': " + ex.getMessage) + None + } + } + + private def makeXml( + groupId: String, + artifactName: String, + latestVersion: String, + versions: NodeSeq, + lastUpdated: java.util.Date + ): String = { + + {groupId} + {artifactName} + + {latestVersion} + {latestVersion} + + {versions} + + {new SimpleDateFormat("yyyyMMddHHmmss").format(lastUpdated)} + + + }.toString() +} \ No newline at end of file diff --git a/src/main/scala/fm/sbt/s3/S3Response.scala b/src/main/scala/fm/sbt/s3/S3Response.scala new file mode 100644 index 0000000..452a05d --- /dev/null +++ b/src/main/scala/fm/sbt/s3/S3Response.scala @@ -0,0 +1,60 @@ +package fm.sbt.s3 + +import com.amazonaws.services.s3.model.{ObjectMetadata, S3Object} + +import java.io.{ByteArrayInputStream, InputStream} +import java.util.Date + +private[s3] sealed trait S3Response extends AutoCloseable { + def meta: ObjectMetadata + def inputStream: Option[InputStream] +} + +private[s3] final case class HEADResponse(meta: ObjectMetadata) extends S3Response { + def close(): Unit = {} + def inputStream: Option[InputStream] = None +} + +private[s3] final case class GETResponse(obj: S3Object) extends S3Response { + def meta: ObjectMetadata = obj.getObjectMetadata + def inputStream: Option[InputStream] = Option(obj.getObjectContent()) + def close(): Unit = obj.close() +} + +private[s3] sealed trait CustomResponse extends S3Response { + def payload: Array[Byte] + def meta: ObjectMetadata + + def inputStream: Option[InputStream] = Option(payload).map{ new ByteArrayInputStream(_) } + def close(): Unit = {} +} + +private[s3] final case class TextResponse(payload: Array[Byte], lastModified: Date) extends CustomResponse { + val meta: ObjectMetadata = { + val m = new ObjectMetadata() + m.setContentType("text/plain") + m.setContentLength(inputStream.map{ _.available().toLong }.getOrElse(payload.length.toLong)) + m.setLastModified(lastModified) + m + } +} + +private[s3] final case class HtmlResponse(payload: Array[Byte], lastModified: Date) extends CustomResponse { + val meta: ObjectMetadata = { + val m = new ObjectMetadata() + m.setContentType("text/html") + m.setContentLength(inputStream.map{ _.available().toLong }.getOrElse(payload.length.toLong)) + m.setLastModified(lastModified) + m + } +} + +private[s3] final case class XmlResponse(payload: Array[Byte], lastModified: Date) extends CustomResponse { + val meta: ObjectMetadata = { + val m = new ObjectMetadata() + m.setContentType("text/xml") + m.setContentLength(inputStream.map{ _.available().toLong }.getOrElse(payload.length.toLong)) + m.setLastModified(lastModified) + m + } +} \ No newline at end of file diff --git a/src/main/scala/fm/sbt/s3/S3URLConnection.scala b/src/main/scala/fm/sbt/s3/S3URLConnection.scala index d0dabbd..a93196a 100644 --- a/src/main/scala/fm/sbt/s3/S3URLConnection.scala +++ b/src/main/scala/fm/sbt/s3/S3URLConnection.scala @@ -16,12 +16,15 @@ package fm.sbt.s3 import com.amazonaws.AmazonServiceException -import com.amazonaws.services.s3.model.{ObjectMetadata, S3Object} +import com.amazonaws.services.s3.model.ObjectMetadata import fm.sbt.S3URLHandler + import java.io.InputStream import java.net.{HttpURLConnection, URL} +import java.nio.charset.StandardCharsets import java.time.ZoneOffset import java.time.format.DateTimeFormatter +import java.util.Date object S3URLConnection { private val s3: S3URLHandler = new S3URLHandler() @@ -32,37 +35,47 @@ object S3URLConnection { */ final class S3URLConnection(url: URL) extends HttpURLConnection(url) { import S3URLConnection.s3 - - private trait S3Response extends AutoCloseable { - def meta: ObjectMetadata - def inputStream: Option[InputStream] - } - - private case class HEADResponse(meta: ObjectMetadata) extends S3Response { - def close(): Unit = {} - def inputStream: Option[InputStream] = None - } - - private case class GETResponse(obj: S3Object) extends S3Response { - def meta: ObjectMetadata = obj.getObjectMetadata - def inputStream: Option[InputStream] = Option(obj.getObjectContent()) - def close(): Unit = obj.close() - } + implicit def logger = fm.sbt.logger private[this] var response: Option[S3Response] = None def connect(): Unit = { val (client, bucket, key) = s3.getClientBucketAndKey(url) + logger.debug(s"[S3URLConnection] connect() (client: $client, bucket: $bucket, key: $key)") try { response = getRequestMethod.toLowerCase match { - case "head" => Option(HEADResponse(client.getObjectMetadata(bucket, key))) - case "get" => Option(GETResponse(client.getObject(bucket, key))) + case "head" => + url.getPath match { + // Respond to maven-metadata.xml HEAD requests + case p if p.endsWith("/maven-metadata.xml") || p.endsWith("/maven-metadata.xml.sha1") => + val meta = new ObjectMetadata() + meta.setLastModified(new Date) + Option(HEADResponse(meta)) + case _ => Option(HEADResponse(client.getObjectMetadata(bucket, key))) + } + case "get" => + url.getPath match { + // Generate 'maven-metadata.xml' contents to allow getting artifact versions + // https://github.com/coursier/coursier/issues/1874#issuecomment-783632512 + case p if p.endsWith("/maven-metadata.xml.sha1") => + S3MavenMetadata.getSha1(client, bucket, key).map{ contents => + TextResponse(contents.getBytes(StandardCharsets.UTF_8), new Date) + } + case p if p.endsWith("/maven-metadata.xml") => + S3MavenMetadata.getXml(client, bucket, key).map{ contents => + XmlResponse(contents.getBytes(StandardCharsets.UTF_8), new Date) + } + case _ => Option(GETResponse(client.getObject(bucket, key))) + + } case "post" => ??? case "put" => ??? case _ => throw new IllegalArgumentException("Invalid request method: "+getRequestMethod) } + logger.debug(s"[S3URLConnection] response: $response") + responseCode = if (response.isEmpty) 404 else 200 } catch { case ex: AmazonServiceException => responseCode = ex.getStatusCode diff --git a/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/build.sbt b/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/build.sbt index b734891..730edd3 100644 --- a/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/build.sbt +++ b/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/build.sbt @@ -1,4 +1,3 @@ - // This could be pulled from an environment variable or Java system properties val s3Bucket: String = "fm-sbt-s3-resolver-example-bucket" @@ -17,7 +16,43 @@ lazy val lib = (project in file("example-lib")) version := "1.0.0", scalaVersion := scalaVersionForCompile, publishMavenStyle := true, - publishTo := Some(s"S3 Test Repository - $s3Bucket" at s"s3://$s3Bucket/$s3Directory") + publishTo := Some(s"S3 Test Repository - $s3Bucket" at s"s3://$s3Bucket/$s3Directory"), + TaskKey[Unit]("checkCoursierVersions") := { + import coursierapi._ + import java.time.LocalDateTime + import java.time.temporal.ChronoUnit + + val s3Repo = MavenRepository.of(s"s3://$s3Bucket/$s3Directory") + val now = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS) + + val csVersions: Versions = + Versions + .create() + .withModule(coursierapi.Module.of("com.example", "example-lib_2.13")) + .withRepositories(s3Repo) + + val res: VersionListing = + csVersions + .versions() + .getMergedListings() + + val expectedMergedListing = + VersionListing.of( + "1.0.0", + "1.0.0", + java.util.Arrays.asList[String]("1.0.0"), + now + ) + + def assertEquals[T](expected: T, actual: T): Unit = { + assert(expected == actual, s"$expected was not equal to $actual") + } + + assertEquals(expectedMergedListing.getLatest, res.getLatest) + assertEquals(expectedMergedListing.getRelease, res.getRelease) + assertEquals(expectedMergedListing.getAvailable, res.getAvailable) + assert(res.getLastUpdated == now || res.getLastUpdated.isAfter(now), s"${res.getLastUpdated} was not after $now") + } ) lazy val app = (project in file("example-app")) diff --git a/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/project/plugins.sbt b/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/project/plugins.sbt index 692556f..d930d0a 100644 --- a/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/project/plugins.sbt +++ b/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/project/plugins.sbt @@ -1,5 +1,11 @@ -sys.props.get("plugin.version") match { - case Some(x) => addSbtPlugin("com.frugalmechanic" % "fm-sbt-s3-resolver" % x) - case _ => sys.error("""|The system property 'plugin.version' is not defined. - |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +{ + val pluginVersion = Option(System.getProperty("plugin.version")) getOrElse { + throw new RuntimeException("The system property 'plugin.version' is not defined. Specify this property using the scriptedLaunchOpts -D.") + } + addSbtPlugin("com.frugalmechanic" % "fm-sbt-s3-resolver" % pluginVersion) } + +libraryDependencies ++= Seq( + // Java API for coursier for easier compat with 2.10 and 2.12 + "io.get-coursier" % "interface" % "1.0.6" +) diff --git a/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/test b/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/test index 48d3b0a..c36d044 100644 --- a/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/test +++ b/src/sbt-test/fm-sbt-s3-resolver/publish_and_resolve/test @@ -5,6 +5,7 @@ # Now let's publish the library > project lib > publish +> checkCoursierVersions # Now let's try to resolve what we just published > project app