Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix sbt task dependency resolution for pre-built natives
Use Def.taskDyn to conditionally choose between pre-built natives
and building from source. This prevents sbt from trying to build
natives when pre-built binaries exist in target/native-bin/.

The key insight is that sbt extracts ALL .value calls at compile
time regardless of if/else branches. By using Def.taskDyn with
the check outside the returned Def.task, we properly isolate the
dependency on buildDylibDir to only when actually needed.
  • Loading branch information
VeryHarry7 committed Nov 26, 2025
commit 5ca0d53f28c1d785898d8ed2870f5e85a9e37ac4
156 changes: 101 additions & 55 deletions project/PatchBuild.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,93 +22,139 @@

import sbt.*
import sbt.Keys.*
import sbt.util.Logger

import java.nio.charset.StandardCharsets
import scala.xml.*

object PatchBuild {
// Helper function to build the patch files map from native files
private def buildPatchFilesMap(
log: Logger,
nativeFiles: Array[File],
baseDir: File,
versionData: Map[String, String],
versionFile: File
): Map[String, Array[Byte]] = {
def loadFromDir(dir: File) =
Path.allSubpaths(dir).filter(_._1.isFile).map(x => PatchFile(x._2, IO.readBytes(x._1))).toSeq
val copiedFiles = loadFromDir(baseDir / "src" / "patch" / "static")

val patchFiles =
for (binary <- nativeFiles if !binary.getName.endsWith(".build-id")) yield {
log.info(s"[MPPatch] Including native binary: ${binary.getName}")
PatchFile(s"native/${binary.getName}", IO.readBytes(binary))
}

val versionDataInfo = versionData.toSeq.sorted
.map(x => s"_mpPatch.version.info[${LuaUtils.quote(x._1)}] = ${LuaUtils.quote(x._2)}")
.mkString("\n")
val buildIdInfo = nativeFiles
.filter(x => x.getName.endsWith(".build-id"))
.sorted
.map { x =>
val platform = x.getName match {
case "mppatch_core.dll.build-id" => "win32"
case "mppatch_core.dylib.build-id" => "macos"
case "mppatch_core.so.build-id" => "linux"
}
s"_mpPatch.version.buildId[${LuaUtils.quote(platform)}] = ${LuaUtils.quote(IO.read(x))}"
}
.mkString("\n")
val versionInfo = PatchFile(
"ui/lib/mppatch_version.lua",
s"""-- Generated from PatchBuild.scala
|_mpPatch.version = {}
|
|_mpPatch.version.buildId = {}
|$buildIdInfo
|
|_mpPatch.version.info = {}
|$versionDataInfo
|
|_mpPatch.version.loaded = true
""".stripMargin.trim
)

val versionFileEntry = PatchFile("version.properties", IO.readBytes(versionFile))

// Final generated files list
(versionInfo +: versionFileEntry +: (patchFiles ++ copiedFiles)).toMap
}

val settings = Seq(
Keys.nativesDir := crossTarget.value / "native-bin",

// buildDylibDir: Only used when building from source (not in CI with pre-built natives)
Keys.buildDylibDir := {
// create the native-patch directory
val dir = Keys.nativesDir.value
val log = streams.value.log
IO.delete(dir)
IO.createDirectory(dir)

// copy native-patch files to the directory
val log = streams.value.log
log.log(Level.Info, "[MPPatch] Building natives from source...")
for (luajitBin <- LuaJITBuild.Keys.luajitFiles.value) {
log.log(Level.Info, s"Copying $luajitBin to output directory.")
log.log(Level.Info, s"[MPPatch] Copying $luajitBin to output directory.")
IO.copyFile(luajitBin.file, dir / luajitBin.file.getName)
}
for (nativeBin <- NativePatchBuild.Keys.nativeVersions.value) {
log.log(Level.Info, s"Copying $nativeBin to output directory.")
log.log(Level.Info, s"[MPPatch] Copying $nativeBin to output directory.")
IO.copyFile(nativeBin.file, dir / nativeBin.name)
IO.write(dir / s"${nativeBin.name}.build-id", nativeBin.buildId)
}
for (wrapperBin <- NativePatchBuild.Keys.win32Wrapper.value) {
log.log(Level.Info, s"Copying $wrapperBin to output directory.")
log.log(Level.Info, s"[MPPatch] Copying $wrapperBin to output directory.")
IO.copyFile(wrapperBin, dir / wrapperBin.getName)
}

// return directory
dir
},
Keys.patchFiles := {
val log = streams.value.log

def loadFromDir(dir: File) =
Path.allSubpaths(dir).filter(_._1.isFile).map(x => PatchFile(x._2, IO.readBytes(x._1))).toSeq
val copiedFiles = loadFromDir(baseDirectory.value / "src" / "patch" / "static")
// patchFiles: Use Def.taskDyn to conditionally choose between pre-built and from-source
// This is critical - sbt only resolves dependencies for the RETURNED task, not all code paths
Keys.patchFiles := Def.taskDyn {
val log = streams.value.log
val prebuiltDir = target.value / "native-bin"

val nativeDirFiles = Keys.nativesDir.value.listFiles()
if (nativeDirFiles == null) sys.error("native-bin does not exist!")
val patchFiles =
for (binary <- nativeDirFiles if !binary.getName.endsWith(".build-id")) yield {
log.info(s"Found native binary file: $binary")
PatchFile(s"native/${binary.getName}", IO.readBytes(binary))
// Check at task-selection time (before returning the task)
if (prebuiltDir.exists() && prebuiltDir.listFiles() != null && prebuiltDir.listFiles().nonEmpty) {
log.log(Level.Info, s"[MPPatch] Found pre-built natives in $prebuiltDir")
// Return a task that uses pre-built natives - NO dependency on buildDylibDir
Def.task {
val nativeFiles = prebuiltDir.listFiles()
log.log(Level.Info, s"[MPPatch] Using ${nativeFiles.length} pre-built native files")
buildPatchFilesMap(
log,
nativeFiles,
baseDirectory.value,
InstallerResourceBuild.Keys.versionData.value,
InstallerResourceBuild.Keys.versionFile.value
)
}

val versionDataInfo = InstallerResourceBuild.Keys.versionData.value.toSeq.sorted
.map(x => s"_mpPatch.version.info[${LuaUtils.quote(x._1)}] = ${LuaUtils.quote(x._2)}")
.mkString("\n")
val buildIdInfo = nativeDirFiles
.filter(x => x.getName.endsWith(".build-id"))
.sorted
.map { x =>
val platform = x.getName match {
case "mppatch_core.dll.build-id" => "win32"
case "mppatch_core.dylib.build-id" => "macos"
case "mppatch_core.so.build-id" => "linux"
} else {
log.log(Level.Info, "[MPPatch] No pre-built natives found, will build from source")
// Return a task that builds from source - HAS dependency on buildDylibDir
Def.task {
val nativesDir = Keys.buildDylibDir.value
val nativeFiles = nativesDir.listFiles()
if (nativeFiles == null || nativeFiles.isEmpty) {
sys.error(s"[MPPatch] native-bin directory is empty after build!")
}
s"_mpPatch.version.buildId[${LuaUtils.quote(platform)}] = ${LuaUtils.quote(IO.read(x))}"
log.log(Level.Info, s"[MPPatch] Built ${nativeFiles.length} native files")
buildPatchFilesMap(
log,
nativeFiles,
baseDirectory.value,
InstallerResourceBuild.Keys.versionData.value,
InstallerResourceBuild.Keys.versionFile.value
)
}
.mkString("\n")
val versionInfo = PatchFile(
"ui/lib/mppatch_version.lua",
s"""-- Generated from PatchBuild.scala
|_mpPatch.version = {}
|
|_mpPatch.version.buildId = {}
|$buildIdInfo
|
|_mpPatch.version.info = {}
|$versionDataInfo
|
|_mpPatch.version.loaded = true
""".stripMargin.trim
)

val versionFile = PatchFile("version.properties", IO.readBytes(InstallerResourceBuild.Keys.versionFile.value))

// Final generated files list
(versionInfo +: versionFile +: (patchFiles ++ copiedFiles)).toMap
},
}
}.value,

Compile / resourceGenerators += Def.task {
val basePath = (Compile / resourceManaged).value
val packagePath = basePath / "moe" / "lymia" / "mppatch" / s"builtin_patch"

streams.value.log.info(s"Writing patch package files to $packagePath")
streams.value.log.info(s"[MPPatch] Writing patch package files to $packagePath")
if (packagePath.exists) IO.delete(packagePath)

for ((name, data) <- Keys.patchFiles.value.toSeq) yield {
Expand Down