refactor(partnerctl-init): extract source build/install flow into reusable support helper

This commit is contained in:
2026-05-03 21:50:25 +08:00
parent 6c74de2126
commit adab925e7d
2 changed files with 117 additions and 80 deletions

View File

@@ -1,100 +1,42 @@
package work.slhaf.partner.ctl.commands.init
import work.slhaf.partner.ctl.support.CommandInterrupted
import work.slhaf.partner.ctl.support.inheritCommand
import work.slhaf.partner.ctl.support.runCommand
import work.slhaf.partner.ctl.support.SourceBuildInstallSpec
import work.slhaf.partner.ctl.support.buildAndInstallFromSource
import work.slhaf.partner.ctl.ui.Prompt
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.Paths
import kotlin.io.path.isDirectory
import kotlin.io.path.name
private const val PARTNER_REPO_URL = "https://gitea.slhaf.work/slhaf/Partner.git"
fun buildFromSource(home: Path, prompt: Prompt) {
checkTool()
val tempDir = Files.createTempDirectory("partnerctl-build-")
val sourceDir = tempDir.resolve("Partner")
val repoUrl = "https://gitea.slhaf.work/slhaf/Partner.git"
val targetJar = home.resolve("resource").resolve("partner-core.jar")
try {
prompt.info("Cloning Partner source from $repoUrl")
val cloneExitCode = inheritCommand(
command = listOf("git", "clone", "--depth", "1", repoUrl, sourceDir.toString()),
buildAndInstallFromSource(
home = home,
prompt = prompt,
spec = SourceBuildInstallSpec(
displayName = "Partner runtime",
repoUrl = PARTNER_REPO_URL,
sourceDirName = "Partner",
buildCommand = listOf("mvn", "-pl", "Partner-Core", "-am", "package", "-DskipTests=true"),
artifactDirectory = Paths.get("Partner-Core", "target"),
artifactSelector = ::findLargestJar,
installRelativePath = Paths.get("resource", "partner-core.jar"),
),
)
if (cloneExitCode != 0) {
throw CommandInterrupted("Failed to clone Partner source from $repoUrl")
}
prompt.info("Building Partner-Core and required modules")
val buildExitCode = inheritCommand(
command = listOf("mvn", "-pl", "Partner-Core", "-am", "package", "-DskipTests=true"),
workingDirectory = sourceDir,
)
if (buildExitCode != 0) {
throw CommandInterrupted("Failed to build Partner runtime. Please make sure Maven is using JDK 21.")
}
val builtJar = findPartnerCoreJar(sourceDir)
Files.createDirectories(targetJar.parent)
Files.copy(builtJar, targetJar, StandardCopyOption.REPLACE_EXISTING)
prompt.success("Partner runtime installed at $targetJar")
} finally {
tempDir.toFile().deleteRecursively()
}
}
private fun checkTool() {
private fun findLargestJar(directory: Path): Path? {
if (!directory.isDirectory()) return null
fun buildToolLackedMessage(lack: Map<String, String>): String {
return buildString {
appendLine("Missing required build tools:")
appendLine()
lack.forEach { (tool, reason) ->
appendLine("$tool:")
appendLine(" $reason")
}
appendLine()
appendLine("Install the missing tools, then rerun:")
appendLine(" partnerctl init")
}.trimEnd()
}
val lack = mutableMapOf<String, String>()
if (runCommand(listOf("java", "--version")).exitCode != 0) {
lack["java"] = "Required to run Maven and Partner runtime. Command failed: java --version"
}
if (runCommand(listOf("javac", "--version")).exitCode != 0) {
lack["javac"] =
"Required to compile Partner from source. Install a JDK, not just a JRE. Command failed: javac --version"
}
if (runCommand(listOf("git", "--version")).exitCode != 0) {
lack["git"] = "Required to clone Partner source. Command failed: git --version"
}
if (runCommand(listOf("mvn", "--version")).exitCode != 0) {
lack["mvn"] = "Required to build Partner from source. Command failed: mvn --version"
}
if (lack.isNotEmpty()) {
throw CommandInterrupted(buildToolLackedMessage(lack))
}
}
private fun findPartnerCoreJar(sourceDir: Path): Path {
val targetDir = sourceDir.resolve("Partner-Core").resolve("target")
if (!targetDir.isDirectory()) {
throw CommandInterrupted("Partner-Core target directory does not exist: $targetDir")
}
return Files.list(targetDir).use { stream ->
return Files.list(directory).use { stream ->
stream
.filter { it.name.endsWith(".jar") }
.filter { !it.name.startsWith("original-") }
.filter { !it.name.endsWith("-sources.jar") }
.filter { !it.name.endsWith("-javadoc.jar") }
.max(Comparator.comparingLong { Files.size(it) })
.orElseThrow { CommandInterrupted("Could not find built Partner Core jar in $targetDir") }
.orElse(null)
}
}

View File

@@ -0,0 +1,95 @@
package work.slhaf.partner.ctl.support
import work.slhaf.partner.ctl.ui.Prompt
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
data class SourceBuildInstallSpec(
val displayName: String,
val repoUrl: String,
val sourceDirName: String,
val buildCommand: List<String>,
val artifactDirectory: Path,
val artifactSelector: (Path) -> Path?,
val installRelativePath: Path,
)
fun buildAndInstallFromSource(
home: Path,
prompt: Prompt,
spec: SourceBuildInstallSpec,
) {
require(spec.sourceDirName.isNotBlank()) { "sourceDirName must not be blank" }
require(spec.buildCommand.isNotEmpty()) { "buildCommand must not be empty" }
checkTool()
val tempDir = Files.createTempDirectory("partnerctl-build-")
val sourceDir = tempDir.resolve(spec.sourceDirName)
val targetPath = home.resolve(spec.installRelativePath)
try {
prompt.info("Cloning ${spec.displayName} source from ${spec.repoUrl}")
val cloneExitCode = inheritCommand(
command = listOf("git", "clone", "--depth", "1", spec.repoUrl, sourceDir.toString()),
)
if (cloneExitCode != 0) {
throw CommandInterrupted("Failed to clone ${spec.displayName} source from ${spec.repoUrl}")
}
prompt.info("Building ${spec.displayName}")
val buildExitCode = inheritCommand(
command = spec.buildCommand,
workingDirectory = sourceDir,
)
if (buildExitCode != 0) {
throw CommandInterrupted("Failed to build ${spec.displayName}.")
}
val artifactDir = sourceDir.resolve(spec.artifactDirectory)
val artifact = spec.artifactSelector(artifactDir)
?: throw CommandInterrupted("Could not find built ${spec.displayName} artifact in $artifactDir")
Files.createDirectories(targetPath.parent)
Files.copy(artifact, targetPath, StandardCopyOption.REPLACE_EXISTING)
prompt.success("${spec.displayName} installed at $targetPath")
} finally {
runCatching { tempDir.toFile().deleteRecursively() }
}
}
private fun checkTool() {
fun buildToolLackedMessage(lack: Map<String, String>): String {
return buildString {
appendLine("Missing required build tools:")
appendLine()
lack.forEach { (tool, reason) ->
appendLine("$tool:")
appendLine(" $reason")
}
appendLine()
appendLine("Install the missing tools, then rerun:")
appendLine(" partnerctl init")
}.trimEnd()
}
val lack = mutableMapOf<String, String>()
if (runCommand(listOf("java", "--version")).exitCode != 0) {
lack["java"] = "Required to run Maven. Command failed: java --version"
}
if (runCommand(listOf("javac", "--version")).exitCode != 0) {
lack["javac"] =
"Required to compile Partner from source. Install a JDK, not just a JRE. Command failed: javac --version"
}
if (runCommand(listOf("git", "--version")).exitCode != 0) {
lack["git"] = "Required to clone Partner source. Command failed: git --version"
}
if (runCommand(listOf("mvn", "--version")).exitCode != 0) {
lack["mvn"] = "Required to build Partner from source. Command failed: mvn --version"
}
if (lack.isNotEmpty()) {
throw CommandInterrupted(buildToolLackedMessage(lack))
}
}