You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何利用SBT插件将Git依赖克隆到自定义路径并添加为子项目?

问题描述

我正在创建一个聚合SBT项目,它依赖多个Git项目。我知道可以通过RootProject(uri("..."))把这些项目作为依赖引用,SBT会把它们克隆到自己管理的路径里。但我需要把这些依赖下载到自定义路径,以此搭建一个能自动下载相关Git项目、支持开发的工作区。

我已经用sbt-git插件写了一个克隆Git仓库的插件任务:

BundleResolver.scala

def resolve: Def.Initialize[Task[Seq[String]]] = Def.task {
  val log = streams.value.log
  log.info("starting bundle resolution")

  val bundles = WorkspacePlugin.autoImport.workspaceBundles.value
  val bundlePaths = bundles.map(x => {
    val bundleName = extractBundleName(x)
    val localPath = file(".").toPath.toAbsolutePath.getParent.resolveSibling(bundleName)

    log.info(s"Cloning bundle : $bundleName")
    val (resultCode, outStr, errStr) = runCommand(Seq("git", "clone", x, localPath.toString))
    resultCode match {
      case 0 =>
        log.info(outStr)
        log.info(s"cloned $bundleName to path $localPath")

      case _ =>
        log.err(s"failed to clone $bundleName")
        log.err(errStr)
    }
    localPath.toString
  })

  bundlePaths
}

WorkspacePlugin.scala

object WorkspacePlugin extends AutoPlugin {

  override def trigger = allRequirements
  override def requires: Plugins = JvmPlugin && GitPlugin

  object autoImport {
    // settings
    val workspaceBundles   = settingKey[Seq[String]]("Dependency bundles for this Workspace")
    val stagingPath        = settingKey[File]("Staging path")

    // tasks
    val workspaceClean     = taskKey[Unit]("Remove existing Workspace depedencies")
    val workspaceImport    = taskKey[Seq[String]]("Download the dependency bundles and setup builds")
  }

  import autoImport._

  override lazy val projectSettings = Seq(
    workspaceBundles   := Seq(), // default to no dependencies
    stagingPath        := Keys.target.value,

    workspaceClean     := BundleResolver.clean.value,
    workspaceImport    := BundleResolver.resolve.value,
  )
  override lazy val buildSettings = Seq()
  override lazy val globalSettings = Seq()
}

但这个插件没法把克隆后的仓库添加为主项目的子项目,请问怎么实现这一点?


更新想法

我想到扩展RootProject的逻辑,创建一个自定义项目类,接收Git URL、把它克隆到自定义路径后返回Project对象:

object WorkspaceProject {
  def apply(uri: URI): Project = {
    val bundleName = GitResolver.extractBundleName(uri.toString)
    val localPath = file(".").toPath.toAbsolutePath.getParent.resolveSibling(bundleName)

    // clone the project
    GitResolver.clone(uri, localPath)

    Project.apply(bundleName.replaceAll(".", "-"), localPath.toFile)
  }
}

我已经在插件项目里声明了这个类,但目标项目没法访问它。请问这个方案可行吗?怎么让目标项目能访问到它?


解决方案

一、将克隆仓库添加为子项目的实现

SBT的聚合项目配置是构建时确定的,而你的workspaceImport是运行时任务,直接在任务里添加子项目行不通。要实现这个,得从构建定义层面入手:

  1. 把克隆逻辑移到构建初始化阶段(而非任务),因为SBT需要在加载构建时就知晓子项目的存在。
  2. 在插件的buildSettings里动态生成子项目:
    • 读取workspaceBundles配置的Git地址
    • 提前克隆(或检查更新)仓库到自定义路径
    • 把这些路径转换成Project对象,添加到聚合列表中

修改插件的buildSettings示例:

override lazy val buildSettings = Seq(
  Keys.onLoad := { state =>
    val extracted = Project.extract(state)
    val bundles = extracted.get(workspaceBundles)
    val log = extracted.get(Keys.streams).value.log

    // 克隆/更新仓库到自定义路径
    val subProjects = bundles.map { uriStr =>
      val uri = URI.create(uriStr)
      val bundleName = extractBundleName(uriStr)
      val localPath = file(".").toPath.toAbsolutePath.getParent.resolveSibling(bundleName).toFile

      // 处理仓库已存在的情况:未克隆则执行clone,已存在则执行pull更新
      if (!localPath.exists()) {
        log.info(s"Cloning bundle: $bundleName to $localPath")
        val (code, out, err) = runCommand(Seq("git", "clone", uriStr, localPath.getAbsolutePath))
        if (code != 0) throw new Exception(s"Failed to clone $bundleName: $err")
      } else {
        log.info(s"Updating bundle: $bundleName")
        val (code, out, err) = runCommand(Seq("git", "-C", localPath.getAbsolutePath, "pull"))
        if (code != 0) log.warn(s"Failed to update $bundleName: $err")
      }

      // 创建子项目
      Project(bundleName.replaceAll("\\.", "-"), localPath)
    }

    // 更新当前构建的聚合项目列表
    val currentBuild = extracted.get(Keys.buildStructure).units(extracted.currentRef.build)
    val newBuild = currentBuild.copy(
      projects = currentBuild.projects ++ subProjects,
      aggregates = currentBuild.aggregates ++ subProjects.map(_.ref)
    )

    val newStructure = extracted.get(Keys.buildStructure).copy(
      units = extracted.get(Keys.buildStructure).units + (extracted.currentRef.build -> newBuild)
    )

    Project.setProjectStructure(newStructure, state)
  }
)

二、自定义WorkspaceProject类的访问方案

这个方案可行,要让目标项目能访问到该类,需按以下步骤操作:

  1. 发布插件到本地仓库:
    在插件项目中执行sbt publishLocal,将插件发布到本地Ivy仓库。

  2. 在插件的autoImport中暴露类:
    修改插件的autoImport对象,把WorkspaceProject暴露出去:

    object autoImport {
      // 原有的settings和tasks
      val workspaceBundles   = settingKey[Seq[String]]("Dependency bundles for this Workspace")
      val stagingPath        = settingKey[File]("Staging path")
      val workspaceClean     = taskKey[Unit]("Remove existing Workspace depedencies")
      val workspaceImport    = taskKey[Seq[String]]("Download the dependency bundles and setup builds")
    
      // 暴露自定义项目类
      val WorkspaceProject = com.yourpackage.WorkspaceProject
    }
    
  3. 在目标项目中导入并使用:
    目标项目的project/plugins.sbt添加插件依赖:

    addSbtPlugin("你的组织ID" % "workspace-plugin" % "插件版本")
    

    然后在build.sbt中导入使用:

    import WorkspacePlugin.autoImport._
    
    lazy val root = (project in file("."))
      .aggregate(
        WorkspaceProject(URI.create("git@github.com:xxx/xxx.git")),
        WorkspaceProject(URI.create("git@github.com:yyy/yyy.git"))
      )
    

注意:这种方式会在构建加载时就执行克隆操作,适合开发场景,但要妥善处理仓库已存在的情况(比如跳过克隆直接使用本地代码)。


内容的提问来源于stack exchange,提问作者devin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 17:03:19