Skip to content

Commit 3d03a3b

Browse files
hcannoodtRobrecht Cannoodt
andauthored
Fix local dependencies of dependencies of local dependencies (#870)
* fix local dependencies of dependencies of local dependencies * add changelog * move some prints to trace * remove `logger.` from debug or trace calls * commit missing changes * Update CHANGELOG.md Co-authored-by: Robrecht Cannoodt <robrecht@data-intuitive.com> * fix passing relative paths causing issues with dependency resolution * add local dependencies in the mix for the testbench * add helper methods to reduce boilerplate code * remove debug code --------- Co-authored-by: Robrecht Cannoodt <robrecht@data-intuitive.com>
1 parent 37e0fda commit 3d03a3b

4 files changed

Lines changed: 168 additions & 84 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* `Nextflowrunner`: fix publishing of directories when the output file name template contains a trailing slash (PR #867).
88

99
* `Logging`: Reduce log level for fetching repositories from info to debug (PR #871).
10+
*
11+
* `Dependencies`: Fix an edge case during dependency resolving (PR #870). Local dependencies of dependencies of local dependencies were resolved incorrectly and resulted in a wrongly resolved destination path.
1012

1113
# Viash 0.9.6 (2025-10-10): Hotfix for dependency path resolution
1214

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ case class Dependency(
9797

9898
@internalFunctionality
9999
internalDependencyTargetScope: ScopeEnum = ScopeEnum.Public
100-
) {
100+
) extends Logging {
101101
if (alias.isDefined) {
102102
// check functionality name
103103
require(alias.get.matches("^[A-Za-z][A-Za-z0-9_]*$"), message = f"alias '${alias.get}' must begin with a letter and consist only of alphanumeric characters or underscores.")
@@ -124,12 +124,14 @@ case class Dependency(
124124
if (isLocalDependency) {
125125
// Local dependency so it will only exist once the component is built.
126126
// TODO improve this, for one, the runner id should be dynamic
127+
debug("getRelativePath: isLocalDependency is true, using ViashNamespace to determine the path")
127128
Some(ViashNamespace.targetOutputPath("", "executable", internalDependencyTargetScope, None, name))
128129
} else {
129130
// Previous existing dependency. Use the location of the '.build.yaml' to determine the relative location.
130131
val relativePath = Dependency.getRelativePath(fullPath, Paths.get(workRepository.get.localPath))
131132
if (relativePath.isEmpty)
132133
throw new MissingBuildYamlException(fullPath, this)
134+
debug(s"getRelativePath: relativePath: $relativePath, workRepository: ${workRepository.get}")
133135
relativePath.flatMap(rp => workRepository.map(r => Paths.get(r.subOutputPath).resolve(rp).toString()))
134136
}
135137
}
@@ -154,22 +156,25 @@ object Dependency extends Logging {
154156
* @param mainDependency Top level dependency for which optionally dependencies of dependencies are being resolved. Used to relativize paths
155157
* @return Tuple with source and destination paths, relativized to current repository locations, ready to be copied
156158
*/
157-
def getSourceAndDestinationFromWrittenPath(dependencyPath: String, output: Path, repoPath: Path, mainDependency: Dependency, remoteLocalDependencyResolver: Option[(Path, Path)]): (Path, Path) = {
159+
def getSourceAndDestinationFromWrittenPath(dependencyPath: String, output: Path, repoPath: Path, mainDependency: Dependency, remoteLocalDependencyResolver: Option[(Path, Path, Path)]): (Path, Path) = {
158160
import scala.jdk.CollectionConverters._
159161

162+
trace(s"getSourceAndDestinationFromWrittenPath: dependencyPath: $dependencyPath, output: $output, repoPath: $repoPath, mainDependency: $mainDependency, remoteLocalDependencyResolver: $remoteLocalDependencyResolver")
163+
160164
val defaultSourcePath = repoPath.resolve(dependencyPath)
161-
val sourcePath = if (defaultSourcePath.toFile().exists()) {
165+
val (sourcePath, relativePath) = if (defaultSourcePath.toFile().exists()) {
162166
// If the dependencyPath is a valid path, use it as source
163-
defaultSourcePath
167+
debug(s"defaultSourcePath exists, use that as sourcePath: $defaultSourcePath")
168+
(defaultSourcePath, None)
164169
} else if (remoteLocalDependencyResolver.isDefined) {
165170
// This is empty if we're resolving the first level of dependencies.
166171
// 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.
167172
// 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,
168173
// 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")
174+
debug(s"Couldn't find sourcePath, using remote local dependency resolver for $dependencyPath")
175+
debug(s"Remote local dependency resolver: $remoteLocalDependencyResolver")
171176
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
177+
val alternativeTargetPath = remoteLocalDependencyResolver.get._3 // This is the relative path of the dependant in the target folder
173178

174179
// strips alternativeTargetPath from dependencyPath
175180
// 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
@@ -179,10 +184,10 @@ object Dependency extends Logging {
179184
case (depPart, targetPart) => depPart == targetPart
180185
}
181186
val relativePath = zipped.flatMap(_._1).reduce((p1, p2) => p1.resolve(p2))
182-
logger.debug(s"relativePath: $relativePath")
187+
debug(s"relativePath: $relativePath")
183188
val res = alternativeSourcePath.resolve(relativePath)
184-
logger.debug(s"Using alternative source path: $res")
185-
res
189+
debug(s"Using alternative source path: $res")
190+
(res, Some(relativePath))
186191
} else {
187192
// Otherwise, throw an error. We shouldn't end up here.
188193
throw new MissingBuildYamlException(defaultSourcePath, mainDependency)
@@ -194,12 +199,18 @@ object Dependency extends Logging {
194199
// Drop the other "target" folder from the found path. This can be multiple folders too
195200
val relativePath = Dependency.getRelativePath(sourcePath, repoPath)
196201
.fold(throw new MissingBuildYamlException(sourcePath, mainDependency))(identity)
202+
trace(s"destinationPath case 1: output: $output, relativePath: $relativePath")
197203
output.resolve(relativePath)
204+
} else if (remoteLocalDependencyResolver.isDefined && relativePath.isDefined) {
205+
trace(s"destinationPath case 2: remoteLocalDependencyResolver._2: ${remoteLocalDependencyResolver.get._2}, relativePath: $relativePath")
206+
remoteLocalDependencyResolver.get._2.resolve(relativePath.get)
198207
} else {
199208
val subPath = mainDependency.getRelativePath(sourcePath)
200209
.fold(throw new MissingBuildYamlException(sourcePath, mainDependency))(identity)
210+
trace(s"destinationPath case 3: output: $output, subPath: $subPath")
201211
output.resolve("dependencies").resolve(subPath)
202212
}
213+
debug(s"Determined destination path: $destinationPath")
203214

204215
(sourcePath, destinationPath)
205216
}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ object DependencyResolver extends Logging {
141141
IO.copyFolder(dependencyRepoPath, dependencyOutputPath)
142142
// Copy dependencies of dependencies, all the way down
143143
// Parse new location of the copied dependency. That way that path can be used to determine the new location of namespace dependencies
144-
recurseBuiltDependencies(Paths.get(output), Paths.get(dep.workRepository.get.localPath), dependencyOutputPath.toString(), dep)
144+
recurseBuiltDependencies(Paths.get(output).toAbsolutePath(), Paths.get(dep.workRepository.get.localPath), dependencyOutputPath.toString(), dep)
145145
// Store location of the copied files
146146
dep.copy(writtenPath = Some(dependencyOutputPath.toString()))
147147
}
@@ -300,7 +300,7 @@ object DependencyResolver extends Logging {
300300
}
301301

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

306306
// Limit recursion depth to prevent infinite loops in e.g. cross dependencies (TODO)
@@ -309,7 +309,7 @@ object DependencyResolver extends Logging {
309309

310310
// this returns paths relative to `repoPath` of dependencies to be copied to `output`
311311
val (dependencyPaths, relativeOutput) = getSparseDependencyInfo(builtDependencyPath + "/.config.vsh.yaml")
312-
logger.debug(s"Paths to relativize: dependencySourcePath: $dependencySourcePath, relativeOutput: $relativeOutput")
312+
debug(s"\nPaths to relativize: dependencySourcePath: $dependencySourceDestPath, relativeOutput: $relativeOutput")
313313

314314
// remove the trailing path parts as far as relativeOutputPath matches the dependencySourcePath
315315
// dependencySourcePath: a/b/c/d/e
@@ -319,18 +319,21 @@ object DependencyResolver extends Logging {
319319
// - left: the the path where the dependency is stored down to common root, matching to 'target'
320320
// - right: the original 'target' folder name
321321
// this is needed to relativize paths correctly when resolving a local dependency of this dependency
322-
val dependencySourceParts = dependencySourcePath.map { dsp =>
322+
val dependencySourceParts = dependencySourceDestPath.map { (dsp, ddp) =>
323323
val dspParts = dsp.iterator().asScala.toList.map(p => Some(p)).reverse
324+
val ddpParts = ddp.iterator().asScala.toList.map(p => Some(p)).reverse
324325
val relativeOutputPath = Paths.get(relativeOutput).iterator().asScala.toList.map(p => Some(p)).reverse
325326
// 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+
val commonSourceParts = dspParts.zipAll(relativeOutputPath, None, None).dropWhile{ case (a, b) => a == b }
328+
val commonDestParts = ddpParts.zipAll(relativeOutputPath, None, None).dropWhile{ case (a, b) => a == b }
327329

328-
val leftPath = commonParts.flatMap(_._1).reverse.fold(dsp.getRoot())((p1, p2) => p1.resolve(p2))
329-
val rightPath = commonParts.flatMap(_._2).reverse.reduceOption((p1, p2) => p1.resolve(p2)).getOrElse(Paths.get("")) // if there is no right part, use empty path
330+
val leftSourcePath = commonSourceParts.flatMap(_._1).reverse.fold(dsp.getRoot())((p1, p2) => p1.resolve(p2))
331+
val leftDestPath = commonDestParts.flatMap(_._1).reverse.fold(ddp.getRoot())((p1, p2) => p1.resolve(p2))
332+
val rightPath = commonSourceParts.flatMap(_._2).reverse.reduceOption((p1, p2) => p1.resolve(p2)).getOrElse(Paths.get("")) // if there is no right part, use empty path
330333

331-
(leftPath, rightPath)
334+
(leftSourcePath, leftDestPath, rightPath)
332335
}
333-
logger.debug(s"dependencySourceParts: $dependencySourceParts")
336+
debug(s"dependencySourceParts: $dependencySourceParts")
334337

335338
for (dp <- dependencyPaths) {
336339
// Get the source & destination path for the dependency, functionality depends whether it was a previous dependency or not.
@@ -346,7 +349,7 @@ object DependencyResolver extends Logging {
346349
IO.copyFolder(sourcePath, destPath)
347350

348351
// Check for more dependencies
349-
recurseBuiltDependencies(output, repoPath, destPath.toString(), dependency, Some(sourcePath), depth + 1)
352+
recurseBuiltDependencies(output, repoPath, destPath.toString(), dependency, Some((sourcePath, destPath)), depth + 1)
350353
}
351354
}
352355

0 commit comments

Comments
 (0)