mirror of https://github.com/sbt/sbt.git
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:
commit
f132dc0a67
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue