Fix #8741: dependencyLock no longer captures file:// URLs

- Add CacheUrlConversion in lm-coursier internal to convert cache file
  paths back to original repository URLs (single place for logic).
- CoursierDependencyResolution delegates to CacheUrlConversion.
- DependencyLockManager.createFromUpdateReport now accepts optional
  cacheDir; when an artifact has a file URL it either converts via
  cache dir (Coursier layout) or fails with a clear message.
- dependencyLock task passes csrCacheDirectory so lock file gets
  portable HTTPS URLs instead of machine-specific cache paths.

Expectation 1: Lock file contains original Maven Central (or repo) URLs.
Expectation 2: If conversion is not possible, lock creation fails.
This commit is contained in:
bitloi 2026-02-15 05:12:34 +01:00
parent 14211d7c4f
commit afae6daea2
4 changed files with 76 additions and 32 deletions

View File

@ -382,7 +382,7 @@ class CoursierDependencyResolution(
.groupBy(_._1)
.view
.mapValues(_.map { case (_, pub, art, _) =>
val originalUrl = CoursierDependencyResolution.cacheFileToOriginalUrl(art.url, cache)
val originalUrl = lmcoursier.internal.CacheUrlConversion.cacheFileToOriginalUrl(art.url, cache)
(originalUrl, pub.classifier.value, pub.ext.value)
})
.toMap
@ -459,32 +459,6 @@ object CoursierDependencyResolution {
def defaultCacheLocation: File =
CacheDefaults.location
private[lmcoursier] def cacheFileToOriginalUrl(fileUrl: String, cacheDir: File): String = {
val filePrefix = "file:"
if (fileUrl.startsWith(filePrefix)) {
val filePath = fileUrl.stripPrefix(filePrefix).replaceFirst("^/+", "/")
val cachePaths = Seq(
cacheDir.getAbsolutePath,
cacheDir.getCanonicalPath
).distinct.map(p => if (p.endsWith("/")) p else p + "/")
def extractHttpUrl(relativePath: String): Option[String] = {
val protocolSepIndex = relativePath.indexOf('/')
if (protocolSepIndex > 0) {
val protocol = relativePath.substring(0, protocolSepIndex)
val rest = relativePath.substring(protocolSepIndex + 1)
Some(s"$protocol://$rest")
} else None
}
cachePaths
.collectFirst {
case cachePath if filePath.startsWith(cachePath) =>
val relativePath = filePath.stripPrefix(cachePath)
extractHttpUrl(relativePath)
}
.flatten
.getOrElse(s"$${CSR_CACHE}$filePath")
} else fileUrl
}
private[lmcoursier] def cacheFileToOriginalUrl(fileUrl: String, cacheDir: File): String =
lmcoursier.internal.CacheUrlConversion.cacheFileToOriginalUrl(fileUrl, cacheDir)
}

View File

@ -0,0 +1,47 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package lmcoursier.internal
import java.io.File
object CacheUrlConversion {
final val FileUrlPrefix = "file:"
final val UnconvertiblePrefix = "${CSR_CACHE}"
def cacheFileToOriginalUrl(fileUrl: String, cacheDir: File): String = {
if (!fileUrl.startsWith(FileUrlPrefix)) return fileUrl
val filePath = fileUrl.stripPrefix(FileUrlPrefix).replaceFirst("^/+", "/")
val cachePaths = Seq(
cacheDir.getAbsolutePath,
cacheDir.getCanonicalPath
).distinct.map(p => if (p.endsWith("/")) p else p + "/")
def extractHttpUrl(relativePath: String): Option[String] = {
val protocolSepIndex = relativePath.indexOf('/')
if (protocolSepIndex > 0) {
val protocol = relativePath.substring(0, protocolSepIndex)
val rest = relativePath.substring(protocolSepIndex + 1)
Some(s"$protocol://$rest")
} else None
}
cachePaths
.collectFirst {
case cachePath if filePath.startsWith(cachePath) =>
val relativePath = filePath.stripPrefix(cachePath)
extractHttpUrl(relativePath)
}
.flatten
.getOrElse(s"$UnconvertiblePrefix$filePath")
}
def isPortableUrl(url: String): Boolean =
!url.startsWith(FileUrlPrefix) && !url.contains(UnconvertiblePrefix)
}

View File

@ -3828,13 +3828,15 @@ object Classpaths {
val resolverNames = fullResolvers.value.map(_.name)
val buildClock = DependencyLockFile.computeBuildClock(deps, resolverNames)
val cacheDir = csrCacheDirectory.value
val lock = DependencyLockManager.createFromUpdateReport(
projectId,
report,
sv,
scalaV,
buildClock,
log
log,
Some(cacheDir)
)
DependencyLockManager.write(lockFile, lock, log)

View File

@ -44,19 +44,40 @@ object DependencyLockManager:
isValid
}
private def artifactUrlForLock(rawUrl: String, cacheDir: Option[File], moduleDesc: String): String =
if !rawUrl.startsWith(CacheUrlConversion.FileUrlPrefix) then rawUrl
else
cacheDir match
case None =>
throw new RuntimeException(
s"Cannot create dependency lock file: artifact has file URL (e.g. local cache path). " +
s"Lock files must use portable repository URLs. Module: $moduleDesc. " +
s"Run 'update' first or ensure dependencies are resolved from remote repositories."
)
case Some(dir) =>
val converted = CacheUrlConversion.cacheFileToOriginalUrl(rawUrl, dir)
if !CacheUrlConversion.isPortableUrl(converted) then
throw new RuntimeException(
s"Cannot create dependency lock file: artifact path is not under Coursier cache. " +
s"Module: $moduleDesc. URL: $rawUrl"
)
converted
def createFromUpdateReport(
projectId: String,
report: UpdateReport,
sbtVersion: String,
scalaVersion: Option[String],
buildClock: String,
log: Logger
log: Logger,
cacheDir: Option[File] = None
): LockFileData =
val configurations = report.configurations.map { configReport =>
val deps = configReport.modules.map { moduleReport =>
val artifacts = moduleReport.artifacts.map { case (artifact, file) =>
val url = artifactUrlForLock(file.toURI.toString, cacheDir, moduleReport.module.toString)
ArtifactLock(
url = file.toURI.toString,
url = url,
classifier = artifact.classifier,
extension = artifact.extension,
tpe = artifact.`type`