Merge pull request #8744 from bitloi/fix/8741-dependency-lock-file-url

[2.x] fix: Fixes dependency lock file url
This commit is contained in:
eugene yokota 2026-02-15 16:52:23 -05:00 committed by GitHub
commit f132dc0a67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 111 additions and 41 deletions

View File

@ -382,7 +382,8 @@ class CoursierDependencyResolution(
.groupBy(_._1) .groupBy(_._1)
.view .view
.mapValues(_.map { case (_, pub, art, _) => .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) (originalUrl, pub.classifier.value, pub.ext.value)
}) })
.toMap .toMap
@ -459,32 +460,6 @@ object CoursierDependencyResolution {
def defaultCacheLocation: File = def defaultCacheLocation: File =
CacheDefaults.location CacheDefaults.location
private[lmcoursier] def cacheFileToOriginalUrl(fileUrl: String, cacheDir: File): String = { private[lmcoursier] def cacheFileToOriginalUrl(fileUrl: String, cacheDir: File): String =
val filePrefix = "file:" lmcoursier.internal.CacheUrlConversion.cacheFileToOriginalUrl(fileUrl, cacheDir)
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
}
} }

View File

@ -0,0 +1,64 @@
/*
* 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}"
private def normalizePathForComparison(path: String): String =
path.replace('\\', '/')
private def normalizedFilePath(fileUrl: String): String = {
val afterPrefix = fileUrl.stripPrefix(FileUrlPrefix).replaceFirst("^/+", "/")
val withForwardSlash = normalizePathForComparison(afterPrefix)
if (
withForwardSlash.length >= 3 && withForwardSlash
.charAt(0) == '/' && withForwardSlash.charAt(2) == ':'
)
withForwardSlash.substring(1)
else
withForwardSlash
}
def cacheFileToOriginalUrl(fileUrl: String, cacheDir: File): String = {
if (!fileUrl.startsWith(FileUrlPrefix)) return fileUrl
val filePath = normalizedFilePath(fileUrl)
val cachePaths = Seq(
cacheDir.getAbsolutePath,
cacheDir.getCanonicalPath
).distinct.map(p =>
normalizePathForComparison(if (p.endsWith("/") || 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 resolverNames = fullResolvers.value.map(_.name)
val buildClock = DependencyLockFile.computeBuildClock(deps, resolverNames) val buildClock = DependencyLockFile.computeBuildClock(deps, resolverNames)
val cacheDir = csrCacheDirectory.value
val lock = DependencyLockManager.createFromUpdateReport( val lock = DependencyLockManager.createFromUpdateReport(
projectId, projectId,
report, report,
sv, sv,
scalaV, scalaV,
buildClock, buildClock,
log log,
Some(cacheDir)
) )
DependencyLockManager.write(lockFile, lock, log) DependencyLockManager.write(lockFile, lock, log)

View File

@ -44,19 +44,44 @@ object DependencyLockManager:
isValid 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( def createFromUpdateReport(
projectId: String, projectId: String,
report: UpdateReport, report: UpdateReport,
sbtVersion: String, sbtVersion: String,
scalaVersion: Option[String], scalaVersion: Option[String],
buildClock: String, buildClock: String,
log: Logger log: Logger,
cacheDir: Option[File] = None
): LockFileData = ): LockFileData =
val configurations = report.configurations.map { configReport => val configurations = report.configurations.map { configReport =>
val deps = configReport.modules.map { moduleReport => val deps = configReport.modules.map { moduleReport =>
val artifacts = moduleReport.artifacts.map { case (artifact, file) => val artifacts = moduleReport.artifacts.map { case (artifact, file) =>
val url = artifactUrlForLock(file.toURI.toString, cacheDir, moduleReport.module.toString)
ArtifactLock( ArtifactLock(
url = file.toURI.toString, url = url,
classifier = artifact.classifier, classifier = artifact.classifier,
extension = artifact.extension, extension = artifact.extension,
tpe = artifact.`type` tpe = artifact.`type`

View File

@ -149,6 +149,16 @@ object ActionCache:
val json = Parser.parseUnsafe(str) val json = Parser.parseUnsafe(str)
Converter.fromJsonUnsafe[CachedCompileFailure](json) Converter.fromJsonUnsafe[CachedCompileFailure](json)
def parseCachedValue(
str: String,
origin: Option[String],
isFailure: Boolean,
): Option[Either[Option[CachedCompileFailure], O]] =
try
if isFailure then Some(Left(Some(failureFromStr(str))))
else Some(Right(valueFromStr(str, origin)))
catch case _: Exception => None
// Optimization: Check if we can read directly from symlinked value file // Optimization: Check if we can read directly from symlinked value file
val (input, valuePath) = mkInput(key, codeContentHash, extraHash) val (input, valuePath) = mkInput(key, codeContentHash, extraHash)
val resolvedValuePath = config.fileConverter.toPath(VirtualFileRef.of(valuePath)) val resolvedValuePath = config.fileConverter.toPath(VirtualFileRef.of(valuePath))
@ -160,13 +170,10 @@ object ActionCache:
Exception.nonFatalCatch Exception.nonFatalCatch
.opt(IO.read(resolvedValuePath.toFile(), StandardCharsets.UTF_8)) .opt(IO.read(resolvedValuePath.toFile(), StandardCharsets.UTF_8))
.flatMap: str => .flatMap: str =>
// We still need to sync output files for side effects and check exitCode
findActionResult(key, codeContentHash, extraHash, config) match findActionResult(key, codeContentHash, extraHash, config) match
case Right(result) => case Right(result) =>
store.syncBlobs(result.outputFiles, config.outputDirectory) store.syncBlobs(result.outputFiles, config.outputDirectory)
if result.exitCode.contains(failureExitCode) then parseCachedValue(str, Some("disk"), result.exitCode.contains(failureExitCode))
Some(Left(Some(failureFromStr(str))))
else Some(Right(valueFromStr(str, Some("disk"))))
case Left(_) => None case Left(_) => None
else None else None
@ -175,21 +182,18 @@ object ActionCache:
case None => case None =>
findActionResult(key, codeContentHash, extraHash, config) match findActionResult(key, codeContentHash, extraHash, config) match
case Right(result) => case Right(result) =>
// Check exitCode to determine if this is a cached failure
val isFailure = result.exitCode.contains(failureExitCode) val isFailure = result.exitCode.contains(failureExitCode)
result.contents.headOption match result.contents.headOption match
case Some(head) => case Some(head) =>
store.syncBlobs(result.outputFiles, config.outputDirectory) store.syncBlobs(result.outputFiles, config.outputDirectory)
val str = String(head.array(), StandardCharsets.UTF_8) val str = String(head.array(), StandardCharsets.UTF_8)
if isFailure then Left(Some(failureFromStr(str))) parseCachedValue(str, result.origin, isFailure).getOrElse(Left(None))
else Right(valueFromStr(str, result.origin))
case _ => case _ =>
val paths = store.syncBlobs(result.outputFiles, config.outputDirectory) val paths = store.syncBlobs(result.outputFiles, config.outputDirectory)
if paths.isEmpty then Left(None) if paths.isEmpty then Left(None)
else else
val str = IO.read(paths.head.toFile()) val str = IO.read(paths.head.toFile())
if isFailure then Left(Some(failureFromStr(str))) parseCachedValue(str, result.origin, isFailure).getOrElse(Left(None))
else Right(valueFromStr(str, result.origin))
case Left(_) => Left(None) case Left(_) => Left(None)
/** /**