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..1c653cf 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 csVersions: Versions =
+ Versions
+ .create()
+ .withModule(coursierapi.Module.of("com.example", "example-lib_2.13"))
+ .withRepositories(s3Repo)
+
+ val res: VersionListing =
+ csVersions
+ .versions()
+ .getMergedListings()
+
+ val now = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)
+ 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