Skip to content

Commit 3288f43

Browse files
authored
Merge pull request #838 from viash-io/fix/broken_dependency_resolving
fix dependency resolving for dependencies of dependencies needing a local dependency
2 parents 90ea37b + 5e2bed1 commit 3288f43

2 files changed

Lines changed: 69 additions & 12 deletions

File tree

src/main/scala/io/viash/config/dependencies/Dependency.scala

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import java.nio.file.Files
2424
import io.viash.ViashNamespace
2525
import io.viash.exceptions.MissingBuildYamlException
2626
import io.viash.config.ScopeEnum
27+
import io.viash.helpers.Logging
2728

2829
@description(
2930
"""Specifies a Viash component (script or executable) that should be made available for the code defined in the component.
@@ -140,7 +141,7 @@ case class Dependency(
140141
}.getOrElse(false)
141142
}
142143

143-
object Dependency {
144+
object Dependency extends Logging {
144145

145146
/**
146147
* Relativize the writtenPath info from a dependency to a source and destination path so it can be copied.
@@ -153,10 +154,39 @@ object Dependency {
153154
* @param mainDependency Top level dependency for which optionally dependencies of dependencies are being resolved. Used to relativize paths
154155
* @return Tuple with source and destination paths, relativized to current repository locations, ready to be copied
155156
*/
156-
def getSourceAndDestinationFromWrittenPath(dependencyPath: String, output: Path, repoPath: Path, mainDependency: Dependency): (Path, Path) = {
157+
def getSourceAndDestinationFromWrittenPath(dependencyPath: String, output: Path, repoPath: Path, mainDependency: Dependency, remoteLocalDependencyResolver: Option[(Path, Path)]): (Path, Path) = {
157158
import scala.jdk.CollectionConverters._
158159

159-
val sourcePath = repoPath.resolve(dependencyPath)
160+
val defaultSourcePath = repoPath.resolve(dependencyPath)
161+
val sourcePath = if (defaultSourcePath.toFile().exists()) {
162+
// If the dependencyPath is a valid path, use it as source
163+
defaultSourcePath
164+
} else if (remoteLocalDependencyResolver.isDefined) {
165+
// This is empty if we're resolving the first level of dependencies.
166+
// For the most part we should not end up here, but there is an edge case where we have to resolve a local dependency for a dependency of a dependency.
167+
// In this case, we don't have the context of the location where the dependency is stored under the dependency folder, however we know where the dependant is stored,
168+
// so we can use the remote local dependency resolver to find the source path.
169+
logger.debug(s"Couldn't find sourcePath, using remote local dependency resolver for $dependencyPath")
170+
logger.debug(s"Remote local dependency resolver: $remoteLocalDependencyResolver")
171+
val alternativeSourcePath = remoteLocalDependencyResolver.get._1 // This is the path where the dependant is located
172+
val alternativeTargetPath = remoteLocalDependencyResolver.get._2 // This is the relative path of the dependant in the target folder
173+
174+
// strips alternativeTargetPath from dependencyPath
175+
// ie. `target` from `target/foo/bar` so that it can be added to alternativeSourcePath as it doesn't contain the `target` folder anymore at this point
176+
val targetIter = alternativeTargetPath.iterator().asScala.toList.map(p => Some(p))
177+
val depIter = Paths.get(dependencyPath).iterator().asScala.toList.map(p => Some(p))
178+
val zipped = depIter.zipAll(targetIter, None, None).dropWhile {
179+
case (depPart, targetPart) => depPart == targetPart
180+
}
181+
val relativePath = zipped.flatMap(_._1).reduce((p1, p2) => p1.resolve(p2))
182+
logger.debug(s"relativePath: $relativePath")
183+
val res = alternativeSourcePath.resolve(relativePath)
184+
logger.debug(s"Using alternative source path: $res")
185+
res
186+
} else {
187+
// Otherwise, throw an error. We shouldn't end up here.
188+
throw new MissingBuildYamlException(defaultSourcePath, mainDependency)
189+
}
160190
// Split the path into chunks so we can manipulate them more easily
161191
val pathParts = Paths.get(dependencyPath).iterator().asScala.toList.map(p => p.toString())
162192

src/main/scala/io/viash/helpers/DependencyResolver.scala

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,9 @@ object DependencyResolver extends Logging {
265265
}
266266
}
267267

268-
// Read a config file from a built target. Extract dependencies 'writtenPath'.
269-
def getSparseDependencyInfo(configPath: String): List[String] = {
268+
// Read a config file from a built target. Extract dependencies 'writtenPath' and the `output` path of the component.
269+
// In 'legacy mode', the output path is not properly sanitized and thus useless, so we return an empty string instead.
270+
def getSparseDependencyInfo(configPath: String): (List[String], String) = {
270271
try {
271272
val yamlText = IO.read(IO.uri(configPath))
272273
val json = Convert.textToJson(yamlText, configPath)
@@ -275,15 +276,18 @@ object DependencyResolver extends Logging {
275276
val dependencies =
276277
if (legacyMode) {
277278
val jsonVec = json.hcursor.downField("functionality").downField("dependencies").focus.flatMap(_.asArray).get
278-
jsonVec.flatMap(_.hcursor.downField("writtenPath").as[String].toOption).toList
279+
val depList = jsonVec.flatMap(_.hcursor.downField("writtenPath").as[String].toOption).toList
280+
(depList, "")
279281
}
280282
else {
281283
val jsonVec = json.hcursor.downField("build_info").downField("dependencies").focus.flatMap(_.asArray).get
282-
jsonVec.flatMap(_.hcursor.as[String].toOption).toList
284+
val depList = jsonVec.flatMap(_.hcursor.as[String].toOption).toList
285+
val outputPath = json.hcursor.downField("build_info").downField("output").as[String].toOption.get
286+
(depList, outputPath)
283287
}
284288
dependencies
285289
} catch {
286-
case _: Throwable => Nil
290+
case _: Throwable => (Nil, "")
287291
}
288292
}
289293

@@ -296,19 +300,42 @@ object DependencyResolver extends Logging {
296300
}
297301

298302
// Handle dependencies of dependencies. For a given already built component, get their dependencies, copy them to our new target folder and recurse into these.
299-
def recurseBuiltDependencies(output: Path, repoPath: Path, builtDependencyPath: String, dependency: Dependency, depth: Int = 0): Unit = {
303+
def recurseBuiltDependencies(output: Path, repoPath: Path, builtDependencyPath: String, dependency: Dependency, dependencySourcePath: Option[Path] = None, depth: Int = 0): Unit = {
304+
import scala.jdk.CollectionConverters._
300305

301306
// Limit recursion depth to prevent infinite loops in e.g. cross dependencies (TODO)
302307
if (depth > 10)
303308
throw new RuntimeException("Copying dependencies traces too deep. Possibibly caused by a cross dependency.")
304309

305310
// this returns paths relative to `repoPath` of dependencies to be copied to `output`
306-
val dependencyPaths = getSparseDependencyInfo(builtDependencyPath + "/.config.vsh.yaml")
311+
val (dependencyPaths, relativeOutput) = getSparseDependencyInfo(builtDependencyPath + "/.config.vsh.yaml")
312+
logger.debug(s"Paths to relativize: dependencySourcePath: $dependencySourcePath, relativeOutput: $relativeOutput")
313+
314+
// remove the trailing path parts as far as relativeOutputPath matches the dependencySourcePath
315+
// dependencySourcePath: a/b/c/d/e
316+
// relativeOutputPath: c'/d/e
317+
// output: a/b/c & c'
318+
// dependencySourcePath contains a tuple of:
319+
// - left: the the path where the dependency is stored down to common root, matching to 'target'
320+
// - right: the original 'target' folder name
321+
// this is needed to relativize paths correctly when resolving a local dependency of this dependency
322+
val dependencySourceParts = dependencySourcePath.map { dsp =>
323+
val dspParts = dsp.iterator().asScala.toList.map(p => Some(p)).reverse
324+
val relativeOutputPath = Paths.get(relativeOutput).iterator().asScala.toList.map(p => Some(p)).reverse
325+
// Find the first part that is not in the relative output path
326+
val commonParts = dspParts.zipAll(relativeOutputPath, None, None).dropWhile{ case (a, b) => a == b }
327+
328+
val leftPath = commonParts.flatMap(_._1).reverse.fold(dsp.getRoot())((p1, p2) => p1.resolve(p2))
329+
val rightPath = commonParts.flatMap(_._2).reverse.reduce((p1, p2) => p1.resolve(p2))
330+
331+
(leftPath, rightPath)
332+
}
333+
logger.debug(s"dependencySourceParts: $dependencySourceParts")
307334

308335
for (dp <- dependencyPaths) {
309336
// Get the source & destination path for the dependency, functionality depends whether it was a previous dependency or not.
310337
// Paths are relativized depending the original dependency.
311-
val (sourcePath, destPath) = Dependency.getSourceAndDestinationFromWrittenPath(dp, output, repoPath, dependency)
338+
val (sourcePath, destPath) = Dependency.getSourceAndDestinationFromWrittenPath(dp, output, repoPath, dependency, dependencySourceParts)
312339

313340
// Make sure the destination is clean so first remove the destination folder if it exists
314341
if (destPath.toFile().exists())
@@ -319,7 +346,7 @@ object DependencyResolver extends Logging {
319346
IO.copyFolder(sourcePath, destPath)
320347

321348
// Check for more dependencies
322-
recurseBuiltDependencies(output, repoPath, destPath.toString(), dependency, depth + 1)
349+
recurseBuiltDependencies(output, repoPath, destPath.toString(), dependency, Some(sourcePath), depth + 1)
323350
}
324351
}
325352

0 commit comments

Comments
 (0)