diff --git a/build.sbt b/build.sbt index 851ceeb9e..f43a8e459 100644 --- a/build.sbt +++ b/build.sbt @@ -1284,6 +1284,15 @@ lazy val lmCoursierDependencies = Def.settings( excludeDependencies ++= Seq( ExclusionRule("org.scala-lang.modules", "scala-xml_2.13"), ), + // lmCoursierShaded merges every dependency into one assembly with a single surviving + // MANIFEST.MF, so coursier's own Implementation-Version doesn't survive there to be read back + // at runtime. Stash coursierVersion in a resource under our own package instead, which assembly + // merges in unchanged. + Compile / resourceGenerators += Def.task { + val file = (Compile / resourceManaged).value / "lmcoursier" / "coursier.properties" + IO.write(file, s"version=$coursierVersion\n") + Seq(file) + }.taskValue, ) lazy val lmCoursier = project diff --git a/lm-coursier/definitions/src/main/scala/lmcoursier/CoursierConfiguration.scala b/lm-coursier/definitions/src/main/scala/lmcoursier/CoursierConfiguration.scala index 8e5a006bd..b704a706f 100644 --- a/lm-coursier/definitions/src/main/scala/lmcoursier/CoursierConfiguration.scala +++ b/lm-coursier/definitions/src/main/scala/lmcoursier/CoursierConfiguration.scala @@ -73,4 +73,6 @@ import scala.concurrent.duration.{ Duration, FiniteDuration } localArtifactsShouldBeCached: Boolean = false, @since lockFile: Option[File] = None, + @since + userAgent: Option[String] = None, ) diff --git a/lm-coursier/src/main/scala/lmcoursier/CoursierDependencyResolution.scala b/lm-coursier/src/main/scala/lmcoursier/CoursierDependencyResolution.scala index dc4266af0..78f8aa656 100644 --- a/lm-coursier/src/main/scala/lmcoursier/CoursierDependencyResolution.scala +++ b/lm-coursier/src/main/scala/lmcoursier/CoursierDependencyResolution.scala @@ -256,7 +256,8 @@ class CoursierDependencyResolution( checksums = checksums, credentials = conf.credentials.map(ToCoursier.credentials), followHttpToHttpsRedirections = conf.followHttpToHttpsRedirections.getOrElse(true), - localArtifactsShouldBeCached = conf.localArtifactsShouldBeCached + localArtifactsShouldBeCached = conf.localArtifactsShouldBeCached, + userAgent = conf.userAgent ) val excludeDependencies = conf.excludeDependencies.map { (strOrg, strName) => @@ -547,6 +548,30 @@ class CoursierDependencyResolution( end CoursierDependencyResolution object CoursierDependencyResolution: + // Built at runtime, not as a single literal: lmCoursierShaded's relocation rewrites any string + // constant shaped like a "coursier."-prefixed path, which would otherwise silently turn this + // into "lmcoursier.internal.shaded.coursier.http.agent" and break the override. + private val userAgentPropertyKey: String = Seq("coursier", "http", "agent").mkString(".") + + lazy val coursierUserAgent: String = + sys.props.get(userAgentPropertyKey).getOrElse(coursierUserAgent0) + // Reads the version from a resource rather than the jar manifest, which lmCoursierShaded's assembly merge clobbers. + private def coursierUserAgent0: String = + val version = + Option(getClass.getResourceAsStream("/lmcoursier/coursier.properties")) + .flatMap { in => + scala.util + .Using(in) { in0 => + val props = new java.util.Properties + props.load(in0) + Option(props.getProperty("version")) + } + .toOption + .flatten + } + .getOrElse("2.1") + s"Coursier/$version (+https://github.com/coursier)" + def apply(configuration: CoursierConfiguration): DependencyResolution = DependencyResolution(new CoursierDependencyResolution(configuration)) diff --git a/lm-coursier/src/main/scala/lmcoursier/syntax/package.scala b/lm-coursier/src/main/scala/lmcoursier/syntax/package.scala index 97e4e7a25..6e88d15dc 100644 --- a/lm-coursier/src/main/scala/lmcoursier/syntax/package.scala +++ b/lm-coursier/src/main/scala/lmcoursier/syntax/package.scala @@ -79,6 +79,7 @@ package object syntax: sameVersions = Nil, localArtifactsShouldBeCached = false, lockFile = None, + userAgent = None, ) end extension @@ -120,6 +121,8 @@ package object syntax: def withRetry(retry: (FiniteDuration, Int)): CoursierConfiguration = value.withRetry(Some((retry._1, retry._2))) + def withUserAgent(userAgent: String): CoursierConfiguration = + value.withUserAgent(Option(userAgent)) end extension extension (value: Publication) diff --git a/main/src/main/scala/sbt/coursierint/LMCoursier.scala b/main/src/main/scala/sbt/coursierint/LMCoursier.scala index 22b50cdee..f8e8157dc 100644 --- a/main/src/main/scala/sbt/coursierint/LMCoursier.scala +++ b/main/src/main/scala/sbt/coursierint/LMCoursier.scala @@ -35,6 +35,12 @@ object LMCoursier: private val credentialRegistry: ConcurrentHashMap[(String, String), IvyCredentials] = new ConcurrentHashMap + def defaultUserAgent(sbtVer: String): String = + sys.props.get("sbt.http.agent").getOrElse { + val cs = CoursierDependencyResolution.coursierUserAgent + s"$cs sbt/$sbtVer (+https://www.scala-sbt.org/)".trim + } + def defaultCacheLocation: File = def absoluteFile(path: String): File = new File(path).getAbsoluteFile() def windowsCacheDirectory: File = @@ -119,6 +125,7 @@ object LMCoursier: val missingOk = updateConfig match case Some(uc) => uc.missingOk case _ => false + val sbtVer = appConfig.provider.id.version CoursierConfiguration() .withResolvers(rs.toVector) .withInterProjectDependencies(interProjectDependencies.toVector) @@ -144,6 +151,7 @@ object LMCoursier: .withSameVersions(sameVersions) .withLocalArtifactsShouldBeCached(localArtifactsShouldBeCached) .withLockFile(lockFile) + .withUserAgent(defaultUserAgent(sbtVer)) end coursierConfiguration def coursierConfigurationTask: Def.Initialize[Task[CoursierConfiguration]] = Def.task {