diff --git a/.claude/settings.json b/.claude/settings.json index 1112fd095..751b8ef20 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,4 +1,5 @@ { + "worktree": { "bgIsolation": "none" }, "hooks": { "SessionStart": [ { diff --git a/build.gradle.kts b/build.gradle.kts index 9bf320bec..f1509856d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,8 @@ +import com.hypherionmc.modfusioner.plugin.FusionerExtension import net.fabricmc.loom.api.LoomGradleExtensionAPI import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.bundling.AbstractArchiveTask import org.jetbrains.kotlin.konan.properties.loadProperties plugins { @@ -46,6 +48,10 @@ version = "mod_version".prop ?: "0.0.1-SNAPSHOT" group = "mod_group".prop ?: "net.kernelpanicsoft" subprojects { + // Stonecutter's tree/branch anchors (e.g. `:core`) are synthetic container projects with real + // leaf projects nested under them - they must not get build plugins applied to them directly. + if (subprojects.isNotEmpty()) return@subprojects + apply(plugin = "dev.architectury.loom") apply(plugin = "net.kernelpanicsoft.actualizer") @@ -115,12 +121,20 @@ subprojects { }) compileOnly("org.jetbrains:annotations:24.1.0") + + // Gradle 9 stopped bundling its own copy of the JUnit Platform launcher for + // useJUnitPlatform() - every module needs this on the test runtime classpath now. + "testRuntimeOnly"(rootProject.libs.junit.platform.launcher) } // One MavenPublication per module, published to kernelpanicsoft.net's Reposilite - archie-core/ // -datagen/-gametest are real consumable libraries; archie-test is a dev playground, never // published (matches fusioner/dokka's own product/test split above). - if (!project.name.startsWith("archie-test-")) { + // + // Under Stonecutter, `project.name` is just the version segment ("1.21.1") for every leaf in + // every tree - it no longer distinguishes "test" from the rest. `project.path` still does + // (":test:common:1.21.1" etc.), since Stonecutter nests leaves under their tree name. + if (!project.path.startsWith(":test:")) { // allprojects{} (below) is what normally applies these, but it's declared after this // subprojects{} block and hasn't run for this project yet - apply is idempotent, so // re-applying here just guarantees ordering for the components["java"]/publishing{} access @@ -128,28 +142,35 @@ subprojects { apply(plugin = "java") apply(plugin = "maven-publish") - extensions.configure("publishing") { - publications { - create("maven") { - artifactId = base.archivesName.get() - from(components["java"]) + // base.archivesName only reaches its final "archie-core-fabric"-style value once this leaf's + // own build.gradle.kts runs (module scripts execute after this subprojects{} block, and + // allprojects{} - which seeds the "archie-core" prefix - runs after it too) - reading it here + // would still see the base plugin's raw default ("1.21.1", from project.name). Defer until + // this project has finished configuring. + afterEvaluate { + extensions.configure("publishing") { + publications { + create("maven") { + artifactId = base.archivesName.get() + from(components["java"]) + } } - } - - repositories { - mavenLocal() - maven { - name = "Reposilite" - val releasesUrl = "https://maven.kernelpanicsoft.net/releases" - val snapshotsUrl = "https://maven.kernelpanicsoft.net/snapshots" - - url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) - credentials { - username = localProperties?.getProperty("reposilite.username") - ?: System.getenv("REPOSILITE_USERNAME") - password = localProperties?.getProperty("reposilite.password") - ?: System.getenv("REPOSILITE_PASSWORD") + repositories { + mavenLocal() + maven { + name = "Reposilite" + val releasesUrl = "https://maven.kernelpanicsoft.net/releases" + val snapshotsUrl = "https://maven.kernelpanicsoft.net/snapshots" + + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) + + credentials { + username = localProperties?.getProperty("reposilite.username") + ?: System.getenv("REPOSILITE_USERNAME") + password = localProperties?.getProperty("reposilite.password") + ?: System.getenv("REPOSILITE_PASSWORD") + } } } } @@ -158,6 +179,11 @@ subprojects { } allprojects { + // Stonecutter's tree/branch anchors (e.g. `:core`) are synthetic container projects with real + // leaf projects nested under them - they must not get build plugins applied to them directly. + // The true root project still needs this block (e.g. for its own `publish` task). + if (this != rootProject && subprojects.isNotEmpty()) return@allprojects + apply(plugin = "java") apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.plugin.serialization") @@ -200,14 +226,38 @@ fusioner { jarVersion = project.version.toString() outputDirectory = "build/artifacts" + // modfusioner finds each side's source project by bare Project.name (case-insensitive), searched + // across the *entire* build - but under Stonecutter every tree (core/datagen/gametest/test) has a + // leaf literally named "fabric" and one named "neoforge", so no name resolves uniquely to core's. + // ":core" is an existing, globally-unique container project name - point both sides at it just to + // satisfy modfusioner's "did we find >= 2 projects" check; `inputFile` (set below, once every + // project has finished configuring) overrides where the actual jar is read from, resolved relative + // to that anchor project's directory. fabric { - projectName = "archie-core-fabric" - inputTaskName = "remapJar" + projectName = "core" } neoforge { - projectName = "archie-core-neoforge" - inputTaskName = "remapJar" + projectName = "core" + } +} + +// modfusioner reads `inputFile` as `File(, inputFile)` - `projectName` above +// is just an anchor, so compute the real remapJar output path here (deferred to gradle.projectsEvaluated +// so base.archivesName - and therefore the jar's real filename - has reached its final value) and +// express it relative to :core's directory. +gradle.projectsEvaluated { + val mcVersion = libs.versions.minecraft.get() + val coreDir = project(":core").projectDir + + fun remapJarFile(path: String) = + (project(path).tasks.named("remapJar").get() as AbstractArchiveTask).archiveFile.get().asFile + + project.extensions.getByType().let { fusionerExtension -> + fusionerExtension.fabricConfiguration.inputFile = + remapJarFile(":core:fabric:$mcVersion").relativeTo(coreDir).path + fusionerExtension.neoforgeConfiguration.inputFile = + remapJarFile(":core:neoforge:$mcVersion").relativeTo(coreDir).path } } @@ -224,7 +274,7 @@ publisher { projectVersion = "${libs.versions.minecraft.get()}-${project.version}" displayName = "Archie-Merged-${projectVersion.get()}" - gameVersions = listOf("1.21.1") + gameVersions = listOf(libs.versions.minecraft.get()) loaders = listOf("neoforge", "fabric") curseEnvironment = "both" versionType = "alpha" @@ -243,15 +293,12 @@ publisher { } dependencies { - dokka(project(":archie-core-common")) { isTransitive = false } - dokka(project(":archie-core-fabric")) { isTransitive = false } - dokka(project(":archie-core-neoforge")) { isTransitive = false } - dokka(project(":archie-datagen-common")) { isTransitive = false } - dokka(project(":archie-datagen-fabric")) { isTransitive = false } - dokka(project(":archie-datagen-neoforge")) { isTransitive = false } - dokka(project(":archie-gametest-common")) { isTransitive = false } - dokka(project(":archie-gametest-fabric")) { isTransitive = false } - dokka(project(":archie-gametest-neoforge")) { isTransitive = false } + val mcVersion = libs.versions.minecraft.get() + listOf("core", "datagen", "gametest").forEach { tree -> + listOf("common", "fabric", "neoforge").forEach { branch -> + dokka(project(":$tree:$branch:$mcVersion")) { isTransitive = false } + } + } } tasks { @@ -269,20 +316,8 @@ tasks { group = "publishing" val tag = rootProject.version.toString().substringBeforeLast(".") workingDir = rootDir - // --alias-type redirect: mike's default ("symlink") writes the "latest" alias as an - // actual symlink into the gh-pages branch, which GitHub's own automatic Pages - // build-and-deploy (triggered whenever gh-pages is pushed, separate from this task) - // rejects outright ("content does not contain any hard links, symlinks"). "redirect" - // makes the alias a small HTML redirect page instead - no symlink, same effect for - // visitors. commandLine("mike", "deploy", "--push", "--update-aliases", "--alias-type", "redirect", tag, "latest") } - // modpublisher's changelog reads CHANGELOG.md straight off disk when a publish task runs - it - // doesn't know about git tags or PRs. .github/workflows/release-notes.yaml (reactive, post-tag) - // can't help here: by the time it would generate this release's entry, the publish task attached - // to the tag has already read (and shipped) whatever was on disk before. This task closes that - // gap by generating CHANGELOG.md synchronously - see .github/scripts/generate_release_notes.py's - // module docstring for the two call shapes. register("generateChangelog") { group = "publishing" workingDir = rootDir diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 6bb4f71fb..17df7c227 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -21,10 +21,13 @@ val sharedProperties = kotlin.runCatching { val String.prop: String? get() = sharedProperties?.get(this)?.toString() +val branchDir = projectDir.parentFile.parentFile + loom { - accessWidenerPath = file("src/main/resources/${"mod_id".prop}.accesswidener") + accessWidenerPath = branchDir.resolve("src/main/resources/${"mod_id".prop}.accesswidener") } + dependencies { compileOnly(kotlin("reflect")) implementation(libs.junit.jupiter.api) @@ -39,19 +42,10 @@ dependencies { api(libs.kotlinx.serialization.json5) { isTransitive = false } api(libs.kotlinx.serialization.cbor) { isTransitive = false } api(compose.runtime) - // Used only for the fabric @Environment annotations + mixin deps. Do NOT use other classes - // from fabric loader from common code. modImplementation(libs.fabric.loader) modApi(libs.rei.common) modCompileOnly(libs.clothConfig.common) - // Cloth Config's own transitive dependency, kept visible at compile time only (like Cloth - // Config itself) since the config system exposes `Color` directly in its own public API - - // NOT bundled: Cloth Config's own distributed jar already jar-in-jars this and exports - // `me.shedaniel.math` itself, so embedding a second copy makes NeoForge's ModLauncher refuse - // to even build its module layer ("Modules basic.math and cloth_config export package - // me.shedaniel.math") the moment both are present - confirmed by actually hitting that crash. - // [ColorSerializer]/[SColor] stay gated behind isClothConfigLoaded instead, same as ModifierKeyCode. compileOnlyApi(libs.cloth.basic.math) modApi(libs.architectury.common) modApi(libs.storage.common) @@ -61,20 +55,20 @@ dependencies { tasks { base.archivesName.set(base.archivesName.get() + "-common") - val verifyGuiSpriteAssets by registering { + val verifyGuiSpriteAssets = register("verifyGuiSpriteAssets") { group = "verification" description = "Verifies GUI sprite metadata files have matching PNG assets." doLast { - val spritesDir = file("src/main/resources/assets/archie/textures/gui/sprites") + val spritesDir = branchDir.resolve("src/main/resources/assets/archie/textures/gui/sprites") if (!spritesDir.exists()) return@doLast val missingPng = spritesDir .walkTopDown() .filter { it.isFile && it.name.endsWith(".png.mcmeta") } - .map { it to file(it.path.removeSuffix(".mcmeta")) } + .map { it to File(it.path.removeSuffix(".mcmeta")) } .filter { (_, png) -> !png.exists() } - .map { (meta, _) -> meta.relativeTo(projectDir).invariantSeparatorsPath } + .map { (meta, _) -> meta.relativeTo(branchDir).invariantSeparatorsPath } .toList() if (missingPng.isNotEmpty()) { diff --git a/core/fabric/build.gradle.kts b/core/fabric/build.gradle.kts index c54d928de..5d3caca17 100644 --- a/core/fabric/build.gradle.kts +++ b/core/fabric/build.gradle.kts @@ -1,3 +1,4 @@ +import dev.kikugie.stonecutter.build.StonecutterBuildExtension import net.kernelpanicsoft.archie.plugin.bundleMod import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary import net.kernelpanicsoft.archie.plugin.runtimeLibrary @@ -12,8 +13,13 @@ architectury { fabric() } +val commonNode = requireNotNull(extensions.getByType().node.sibling("common")) { + "No common project for $project" +} +val common: Project = commonNode.project + actualizer { - actualizes(project(":archie-core-common")) + actualizes(common) } configurations { @@ -26,7 +32,7 @@ configurations { } loom { - accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + accessWidenerPath.set(common.loom.accessWidenerPath) mods { maybeCreate("main").apply { @@ -75,8 +81,8 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) runtimeLibrary(libs.kotlinx.coroutines.test) - "common"(project(":archie-core-common", "namedElements")) { isTransitive = false } - "shadowCommon"(project(":archie-core-common", "transformProductionFabric")) { isTransitive = false } + "common"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + "shadowCommon"(files(common.tasks.named("jar").flatMap { it.archiveFile })) } modResources { @@ -91,7 +97,7 @@ tasks { } processResources { - from(project(":archie-core-common").sourceSets.main.get().resources) { + from(common.sourceSets.main.get().resources) { include("assets/archie/**") include("data/archie/**") include("archie-common.mixins.json") @@ -124,11 +130,11 @@ tasks { jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(project(":archie-core-common").sourceSets.main.get().output) + from(common.sourceSets.main.get().output) } sourcesJar { - val commonSources = project(":archie-core-common").tasks.sourcesJar + val commonSources = common.tasks.sourcesJar dependsOn(commonSources) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(commonSources.get().archiveFile.map { zipTree(it) }) diff --git a/core/neoforge/build.gradle.kts b/core/neoforge/build.gradle.kts index a94b0843f..37307559d 100644 --- a/core/neoforge/build.gradle.kts +++ b/core/neoforge/build.gradle.kts @@ -1,3 +1,4 @@ +import dev.kikugie.stonecutter.build.StonecutterBuildExtension import net.kernelpanicsoft.archie.plugin.bundleMod import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary import net.kernelpanicsoft.archie.plugin.runtimeLibrary @@ -12,8 +13,13 @@ architectury { neoForge() } +val commonNode = requireNotNull(extensions.getByType().node.sibling("common")) { + "No common project for $project" +} +val common: Project = commonNode.project + actualizer { - actualizes(project(":archie-core-common")) + actualizes(common) } configurations { @@ -31,16 +37,12 @@ configurations { } loom { - accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + accessWidenerPath.set(common.loom.accessWidenerPath) mods { maybeCreate("main").apply { sourceSet(sourceSets.main.get()) - // actualizer only merges Kotlin expect/actual source into this project's own - // compilation - plain Java files in archie-core-common (e.g. mixin classes with no - // actual/expect involvement) never get copied in, so they're invisible to FML's - // dev-mode module layer unless their sourceSet is also registered here directly. - sourceSet(project(":archie-core-common").sourceSets.main.get()) + sourceSet(common.sourceSets.main.get()) } } @@ -72,9 +74,6 @@ dependencies { bundleRuntimeLibrary(libs.kotlinx.serialization.json5) bundleRuntimeLibrary(libs.kotlinx.serialization.cbor) bundleRuntimeLibrary(compose.runtime) - // compose.runtime's own transitive deps; Loom's dev-run GAMELIBRARY discovery doesn't walk - // transitive deps of a bundled library the way production JarJar packaging does, so each needs - // its own explicit declaration to be visible during runClient/runClientNeoForge. bundleRuntimeLibrary(libs.androidx.annotation) bundleRuntimeLibrary(libs.androidx.collection) bundleRuntimeLibrary(libs.okio) @@ -92,8 +91,8 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) runtimeLibrary(libs.kotlinx.coroutines.test) - "common"(project(":archie-core-common", "namedElements")) { isTransitive = false } - "shadowCommon"(project(":archie-core-common", "transformProductionNeoForge")) { isTransitive = false } + "common"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + "shadowCommon"(files(common.tasks.named("jar").flatMap { it.archiveFile })) } modResources { @@ -108,7 +107,7 @@ tasks { } processResources { - from(project(":archie-core-common").sourceSets.main.get().resources) { + from(common.sourceSets.main.get().resources) { include("assets/archie/**") include("data/archie/**") include("archie-common.mixins.json") @@ -142,11 +141,11 @@ tasks { jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(project(":archie-core-common").sourceSets.main.get().output) + from(common.sourceSets.main.get().output) } sourcesJar { - val commonSources = project(":archie-core-common").tasks.sourcesJar + val commonSources = common.tasks.sourcesJar dependsOn(commonSources) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(commonSources.get().archiveFile.map { zipTree(it) }) diff --git a/core/stonecutter.gradle.kts b/core/stonecutter.gradle.kts new file mode 100644 index 000000000..501dc597c --- /dev/null +++ b/core/stonecutter.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("dev.kikugie.stonecutter") +} + +stonecutter active "1.21.1" diff --git a/datagen/common/build.gradle.kts b/datagen/common/build.gradle.kts index fd20fb2e3..3c26db2fc 100644 --- a/datagen/common/build.gradle.kts +++ b/datagen/common/build.gradle.kts @@ -6,13 +6,28 @@ actualizer { stubUnfulfilledExpects() } +// Cross-tree reference: datagen and core are separate Stonecutter trees, so node.sibling() (which +// only searches within the current tree) doesn't reach core - resolve the path directly instead. +val coreCommon = rootProject.project(":core:common:${stonecutter.current.version}") + loom { - accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + accessWidenerPath.set(coreCommon.loom.accessWidenerPath) } dependencies { - api(project(":archie-core-common", "namedElements")) + api(files(coreCommon.tasks.named("jar").flatMap { it.archiveFile })) + api(libs.kotlinx.serialization) + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.serialization.nbt) { isTransitive = false } + api(libs.kotlinx.serialization.toml) { isTransitive = false } + api(libs.kotlinx.serialization.json5) { isTransitive = false } + api(libs.kotlinx.serialization.cbor) { isTransitive = false } + api(compose.runtime) + modCompileOnly(libs.clothConfig.common) + compileOnlyApi(libs.cloth.basic.math) modApi(libs.architectury.common) + modApi(libs.storage.common) + modApi(libs.storage.resources.common) compileOnly(kotlin("reflect")) implementation(libs.junit.jupiter.api) diff --git a/datagen/fabric/build.gradle.kts b/datagen/fabric/build.gradle.kts index 9045cddfe..a161112f9 100644 --- a/datagen/fabric/build.gradle.kts +++ b/datagen/fabric/build.gradle.kts @@ -1,4 +1,6 @@ -import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary +import dev.kikugie.stonecutter.build.StonecutterBuildExtension +import net.kernelpanicsoft.archie.plugin.runtimeLibrary +import org.gradle.api.tasks.bundling.Jar plugins { alias(libs.plugins.archie) @@ -9,8 +11,19 @@ architectury { fabric() } +// Same-tree sibling. See core/fabric/build.gradle.kts for why node.sibling() is used here. +val commonNode = requireNotNull(extensions.getByType().node.sibling("common")) { + "No common project for $project" +} +val common: Project = commonNode.project + +// Cross-tree references: datagen and core are separate Stonecutter trees, so node.sibling() +// (which only searches within the current tree) doesn't reach core - resolve the path directly. +val coreCommon = rootProject.project(":core:common:${stonecutter.current.version}") +val coreFabric = rootProject.project(":core:fabric:${stonecutter.current.version}") + actualizer { - actualizes(project(":archie-datagen-common")) + actualizes(common) } configurations { @@ -22,7 +35,7 @@ configurations { } loom { - accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + accessWidenerPath.set(coreCommon.loom.accessWidenerPath) mods { maybeCreate("main").apply { @@ -73,8 +86,13 @@ dependencies { testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) - "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } - api(project(":archie-core-fabric", "namedElements")) + "common"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + api(files(coreFabric.tasks.named("jar").flatMap { it.archiveFile })) + modApi(libs.architectury.fabric) + runtimeLibrary(libs.kotlinx.serialization.nbt) + runtimeLibrary(libs.kotlinx.serialization.toml) + runtimeLibrary(libs.kotlinx.serialization.json5) + runtimeLibrary(compose.runtime) } modResources { @@ -100,7 +118,7 @@ tasks { } sourcesJar { - val commonSources = project(":archie-datagen-common").tasks.sourcesJar + val commonSources = common.tasks.sourcesJar dependsOn(commonSources) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(commonSources.get().archiveFile.map { zipTree(it) }) diff --git a/datagen/neoforge/build.gradle.kts b/datagen/neoforge/build.gradle.kts index 58194507f..3101b7510 100644 --- a/datagen/neoforge/build.gradle.kts +++ b/datagen/neoforge/build.gradle.kts @@ -1,5 +1,8 @@ +import dev.kikugie.stonecutter.build.StonecutterBuildExtension import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary +import net.kernelpanicsoft.archie.plugin.runtimeLibrary import org.gradle.api.tasks.bundling.AbstractArchiveTask +import org.gradle.api.tasks.bundling.Jar plugins { alias(libs.plugins.archie) @@ -10,8 +13,19 @@ architectury { neoForge() } +// Same-tree sibling. See core/fabric/build.gradle.kts for why node.sibling() is used here. +val commonNode = requireNotNull(extensions.getByType().node.sibling("common")) { + "No common project for $project" +} +val common: Project = commonNode.project + +// Cross-tree references: datagen and core are separate Stonecutter trees, so node.sibling() +// (which only searches within the current tree) doesn't reach core - resolve the path directly. +val coreCommon = rootProject.project(":core:common:${stonecutter.current.version}") +val coreNeoforge = rootProject.project(":core:neoforge:${stonecutter.current.version}") + actualizer { - actualizes(project(":archie-datagen-common")) + actualizes(common) } configurations { @@ -27,7 +41,7 @@ configurations { } loom { - accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + accessWidenerPath.set(coreCommon.loom.accessWidenerPath) mods { maybeCreate("main").apply { @@ -72,8 +86,12 @@ dependencies { testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) - "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } - api(project(":archie-core-neoforge", "namedElements")) + "common"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + api(files(coreNeoforge.tasks.named("jar").flatMap { it.archiveFile })) + runtimeLibrary(libs.kotlinx.serialization.nbt) + runtimeLibrary(libs.kotlinx.serialization.toml) + runtimeLibrary(libs.kotlinx.serialization.json5) + runtimeLibrary(compose.runtime) } modResources { @@ -99,7 +117,7 @@ tasks { } sourcesJar { - val commonSources = project(":archie-datagen-common").tasks.sourcesJar + val commonSources = common.tasks.sourcesJar dependsOn(commonSources) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(commonSources.get().archiveFile.map { zipTree(it) }) diff --git a/datagen/stonecutter.gradle.kts b/datagen/stonecutter.gradle.kts new file mode 100644 index 000000000..501dc597c --- /dev/null +++ b/datagen/stonecutter.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("dev.kikugie.stonecutter") +} + +stonecutter active "1.21.1" diff --git a/gametest/common/build.gradle.kts b/gametest/common/build.gradle.kts index 9817344b3..2da4e76bc 100644 --- a/gametest/common/build.gradle.kts +++ b/gametest/common/build.gradle.kts @@ -8,12 +8,25 @@ actualizer { stubUnfulfilledExpects() } +// Cross-tree reference: gametest and core are separate Stonecutter trees, so node.sibling() (which +// only searches within the current tree) doesn't reach core - resolve the path directly instead. +val coreCommon = rootProject.project(":core:common:${stonecutter.current.version}") + loom { - accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + accessWidenerPath.set(coreCommon.loom.accessWidenerPath) } dependencies { - api(project(":archie-core-common", "namedElements")) + // See core/fabric/build.gradle.kts and datagen/common/build.gradle.kts for why this depends on + // core-common's "jar" task output directly, and why the Compose dependency is repeated below. + api(files(coreCommon.tasks.named("jar").flatMap { it.archiveFile })) + api(compose.runtime) + api(libs.kotlinx.serialization) + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.serialization.nbt) { isTransitive = false } + api(libs.kotlinx.serialization.toml) { isTransitive = false } + api(libs.kotlinx.serialization.json5) { isTransitive = false } + api(libs.kotlinx.serialization.cbor) { isTransitive = false } modApi(libs.architectury.common) modApi(libs.storage.common) modApi(libs.storage.resources.common) diff --git a/gametest/fabric/build.gradle.kts b/gametest/fabric/build.gradle.kts index b64d41606..5573badc6 100644 --- a/gametest/fabric/build.gradle.kts +++ b/gametest/fabric/build.gradle.kts @@ -1,3 +1,8 @@ +import dev.kikugie.stonecutter.build.StonecutterBuildExtension +import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary +import net.kernelpanicsoft.archie.plugin.runtimeLibrary +import org.gradle.api.tasks.bundling.Jar + plugins { alias(libs.plugins.archie) } @@ -7,8 +12,19 @@ architectury { fabric() } +// Same-tree sibling. See core/fabric/build.gradle.kts for why node.sibling() is used here. +val commonNode = requireNotNull(extensions.getByType().node.sibling("common")) { + "No common project for $project" +} +val common: Project = commonNode.project + +// Cross-tree references: gametest and core are separate Stonecutter trees, so node.sibling() +// (which only searches within the current tree) doesn't reach core - resolve the path directly. +val coreCommon = rootProject.project(":core:common:${stonecutter.current.version}") +val coreFabric = rootProject.project(":core:fabric:${stonecutter.current.version}") + actualizer { - actualizes(project(":archie-gametest-common")) + actualizes(common) } configurations { @@ -20,7 +36,7 @@ configurations { } loom { - accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + accessWidenerPath.set(coreCommon.loom.accessWidenerPath) mods { maybeCreate("main").apply { @@ -66,11 +82,19 @@ dependencies { modLocalRuntime(libs.clothConfig.fabric) implementation(libs.junit.jupiter.api) + implementation(libs.kotlinx.coroutines.test) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) - "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } - api(project(":archie-core-fabric", "namedElements")) + "common"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + api(files(coreFabric.tasks.named("jar").flatMap { it.archiveFile })) + modApi(libs.architectury.fabric) + modImplementation(libs.storage.common) + modImplementation(libs.storage.resources.common) + runtimeLibrary(libs.kotlinx.serialization.nbt) + runtimeLibrary(libs.kotlinx.serialization.toml) + runtimeLibrary(libs.kotlinx.serialization.json5) + runtimeLibrary(compose.runtime) } modResources { @@ -96,7 +120,7 @@ tasks { } sourcesJar { - val commonSources = project(":archie-gametest-common").tasks.sourcesJar + val commonSources = common.tasks.sourcesJar dependsOn(commonSources) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(commonSources.get().archiveFile.map { zipTree(it) }) diff --git a/gametest/neoforge/build.gradle.kts b/gametest/neoforge/build.gradle.kts index 0831174c5..301a1be2a 100644 --- a/gametest/neoforge/build.gradle.kts +++ b/gametest/neoforge/build.gradle.kts @@ -1,4 +1,8 @@ +import dev.kikugie.stonecutter.build.StonecutterBuildExtension +import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary +import net.kernelpanicsoft.archie.plugin.runtimeLibrary import org.gradle.api.tasks.bundling.AbstractArchiveTask +import org.gradle.api.tasks.bundling.Jar plugins { alias(libs.plugins.archie) @@ -9,8 +13,19 @@ architectury { neoForge() } +// Same-tree sibling. See core/fabric/build.gradle.kts for why node.sibling() is used here. +val commonNode = requireNotNull(extensions.getByType().node.sibling("common")) { + "No common project for $project" +} +val common: Project = commonNode.project + +// Cross-tree references: gametest and core are separate Stonecutter trees, so node.sibling() +// (which only searches within the current tree) doesn't reach core - resolve the path directly. +val coreCommon = rootProject.project(":core:common:${stonecutter.current.version}") +val coreNeoforge = rootProject.project(":core:neoforge:${stonecutter.current.version}") + actualizer { - actualizes(project(":archie-gametest-common")) + actualizes(common) } configurations { @@ -26,7 +41,7 @@ configurations { } loom { - accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + accessWidenerPath.set(coreCommon.loom.accessWidenerPath) mods { maybeCreate("main").apply { @@ -76,11 +91,17 @@ dependencies { modApi(libs.architectury.neoforge) implementation(libs.junit.jupiter.api) + implementation(libs.kotlinx.coroutines.test) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) - "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } - api(project(":archie-core-neoforge", "namedElements")) + "common"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + api(files(coreNeoforge.tasks.named("jar").flatMap { it.archiveFile })) + modImplementation(libs.storage.neoforge) { exclude(group = "curse.maven") } + runtimeLibrary(libs.kotlinx.serialization.nbt) + runtimeLibrary(libs.kotlinx.serialization.toml) + runtimeLibrary(libs.kotlinx.serialization.json5) + runtimeLibrary(compose.runtime) } modResources { @@ -106,7 +127,7 @@ tasks { } sourcesJar { - val commonSources = project(":archie-gametest-common").tasks.sourcesJar + val commonSources = common.tasks.sourcesJar dependsOn(commonSources) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(commonSources.get().archiveFile.map { zipTree(it) }) diff --git a/gametest/stonecutter.gradle.kts b/gametest/stonecutter.gradle.kts new file mode 100644 index 000000000..501dc597c --- /dev/null +++ b/gametest/stonecutter.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("dev.kikugie.stonecutter") +} + +stonecutter active "1.21.1" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9f00784d..e0cc8ca2c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ parchment = "2024.11.17" architectury = "13.0.8" architectury-plugin = { require = "[3.4, 3.5[" } -architectury-loom = { require = "1.13.469" } +architectury-loom = { require = "1.17.491" } architectury-kotlin = "2.0.0" modfusioner = "1.0.12" @@ -51,7 +51,7 @@ quilt-parsers = "0.2.1" knbt = "0.11.4" tomlkt = "0.3.7" json5k = "0.3.0" -shadow = "8.1.1" +shadow = "9.6.1" compose = "1.7.1" dokka-mkdocs = "0.6.1" junit = "5.11.4" @@ -124,6 +124,7 @@ neoforge = { module = "net.neoforged:neoforge", version.ref = "neoforge" } modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-platform" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } @@ -137,7 +138,7 @@ architectury = { id = "architectury-plugin", version.ref = "architectury-plugin" architectury-loom = { id = "dev.architectury.loom", version.ref = "architectury-loom" } architectury-kotlin = { id = "net.kernelpanicsoft.architectury.kotlin", version.ref = "architectury-kotlin" } -shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } modfusioner = { id = "com.hypherionmc.modutils.modfusioner", version.ref = "modfusioner" } modpublisher = { id = "com.hypherionmc.modutils.modpublisher", version.ref = "modpublisher" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793a..c42672d95 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 4cd9a8e67..f168a6fe2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,3 @@ -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -rootProject.name = "Archie" - pluginManagement { repositories { maven("https://maven.fabricmc.net/") @@ -22,21 +18,16 @@ pluginManagement { } } -includeModule("core") - -includeModule("datagen") - -includeModule("gametest") - -includeModule("test") - -fun includeModulePlatform(name: String, platform: String) { - include("$name/$platform") - project(":$name/$platform").name = "archie-$name-$platform" +plugins { + id("dev.kikugie.stonecutter") version "0.9.7" } -fun includeModule(name: String) { - includeModulePlatform(name, "common") - includeModulePlatform(name, "fabric") - includeModulePlatform(name, "neoforge") +stonecutter { + for (tree in listOf("core", "datagen", "gametest", "test")) create(tree) { + branch("common") { versions("1.21.1") } + branch("fabric") { versions("1.21.1") } + branch("neoforge") { versions("1.21.1") } + } } + +rootProject.name = "Archie" diff --git a/test/common/build.gradle.kts b/test/common/build.gradle.kts index 2efc4d2e5..2a23270c8 100644 --- a/test/common/build.gradle.kts +++ b/test/common/build.gradle.kts @@ -6,16 +6,39 @@ actualizer { stubUnfulfilledExpects() } +// Cross-tree references: test, core, datagen and gametest are separate Stonecutter trees, so +// node.sibling() (which only searches within the current tree) doesn't reach them - resolve the +// paths directly instead. +val coreCommon = rootProject.project(":core:common:${stonecutter.current.version}") +val datagenCommon = rootProject.project(":datagen:common:${stonecutter.current.version}") +val gametestCommon = rootProject.project(":gametest:common:${stonecutter.current.version}") + +// Stonecutter's real projectDir for a node is its `versions//` folder, two levels below +// this branch's own directory (where the shared `src/` this build script's paths mean actually +// lives) - branchDir undoes that so plain file(...)-style paths below resolve correctly. +val branchDir = projectDir.parentFile.parentFile + loom { log4jConfigs.from(rootDir.resolve("log4j-dev.xml")) - accessWidenerPath = file("src/main/resources/archie_test.accesswidener") + accessWidenerPath = branchDir.resolve("src/main/resources/archie_test.accesswidener") enableTransitiveAccessWideners = true } dependencies { - api(project(":archie-core-common", "namedElements")) - api(project(":archie-datagen-common", "namedElements")) - api(project(":archie-gametest-common", "namedElements")) + // See core/fabric/build.gradle.kts and datagen/common/build.gradle.kts for why these depend on + // each sibling's "jar" task output directly rather than through a project(path) reference, and + // why the Compose/serialization deps are repeated below - files() dependencies carry no + // transitive module metadata, and core-common's own `api` surface is needed here too. + api(files(coreCommon.tasks.named("jar").flatMap { it.archiveFile })) + api(files(datagenCommon.tasks.named("jar").flatMap { it.archiveFile })) + api(files(gametestCommon.tasks.named("jar").flatMap { it.archiveFile })) + api(compose.runtime) + api(libs.kotlinx.serialization) + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.serialization.nbt) { isTransitive = false } + api(libs.kotlinx.serialization.toml) { isTransitive = false } + api(libs.kotlinx.serialization.json5) { isTransitive = false } + api(libs.kotlinx.serialization.cbor) { isTransitive = false } testImplementation(libs.junit.jupiter.api) testImplementation(kotlin("reflect")) diff --git a/test/fabric/build.gradle.kts b/test/fabric/build.gradle.kts index fa0fe9dff..0b314b8ef 100644 --- a/test/fabric/build.gradle.kts +++ b/test/fabric/build.gradle.kts @@ -1,4 +1,7 @@ +import dev.kikugie.stonecutter.build.StonecutterBuildExtension import net.kernelpanicsoft.archie.plugin.bundleMod +import net.kernelpanicsoft.archie.plugin.runtimeLibrary +import org.gradle.api.tasks.bundling.Jar plugins { alias(libs.plugins.shadow) @@ -10,8 +13,22 @@ architectury { fabric() } +// Same-tree sibling. See core/fabric/build.gradle.kts for why node.sibling() is used here. +val commonNode = requireNotNull(extensions.getByType().node.sibling("common")) { + "No common project for $project" +} +val common: Project = commonNode.project + +// Cross-tree references: test, core, datagen and gametest are separate Stonecutter trees, so +// node.sibling() (which only searches within the current tree) doesn't reach them - resolve the +// paths directly instead. +val coreCommon = rootProject.project(":core:common:${stonecutter.current.version}") +val coreFabric = rootProject.project(":core:fabric:${stonecutter.current.version}") +val datagenFabric = rootProject.project(":datagen:fabric:${stonecutter.current.version}") +val gametestFabric = rootProject.project(":gametest:fabric:${stonecutter.current.version}") + actualizer { - actualizes(project(":archie-test-common")) + actualizes(common) } configurations { @@ -24,8 +41,8 @@ configurations { } loom { - log4jConfigs.from(project(":archie-test-common").loom.log4jConfigs) - accessWidenerPath.set(project(":archie-test-common").loom.accessWidenerPath) + log4jConfigs.from(common.loom.log4jConfigs) + accessWidenerPath.set(common.loom.accessWidenerPath) mods { maybeCreate("main").apply { @@ -93,16 +110,32 @@ dependencies { bundleMod(libs.storage.fabric) implementation(libs.junit.jupiter.api) + implementation(libs.kotlinx.coroutines.test) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) - "common"(project(":archie-test-common", "namedElements")) { isTransitive = false } - "shadowCommon"(project(":archie-test-common", "transformProductionFabric")) { isTransitive = false } - api(project(":archie-core-fabric", "namedElements")) - api(project(":archie-datagen-fabric", "namedElements")) - api(project(":archie-gametest-fabric", "namedElements")) - // See the matching comment in gametest/fabric/build.gradle.kts. - runtimeOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } + // See core/fabric/build.gradle.kts and gametest/fabric/build.gradle.kts for why these depend on + // the sibling's "jar" task output directly, and why the Compose/storage/coroutines deps above + // and below are repeated - the actualizer merges test-common's own source files into this + // project's own compilation, so it needs test-common's compile-time deps directly too, not just + // its output. + "common"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + "shadowCommon"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + api(files(coreFabric.tasks.named("jar").flatMap { it.archiveFile })) + api(files(datagenFabric.tasks.named("jar").flatMap { it.archiveFile })) + api(files(gametestFabric.tasks.named("jar").flatMap { it.archiveFile })) + runtimeOnly(files(coreCommon.tasks.named("jar").flatMap { it.archiveFile })) + modImplementation(libs.storage.common) + modImplementation(libs.storage.resources.common) + // files() dependencies carry no runtime GAMELIBRARY discovery either - the serialization format + // add-ons core-fabric bundles at runtime (nbt/toml/json5, needed by Archie's own Config system + // at init) don't propagate, so they're repeated here too. Confirmed missing live: a + // NoClassDefFoundError for io.github.xn32.json5k.ConfigBuilder when actually launching this + // project. + runtimeLibrary(libs.kotlinx.serialization.nbt) + runtimeLibrary(libs.kotlinx.serialization.toml) + runtimeLibrary(libs.kotlinx.serialization.json5) + runtimeLibrary(compose.runtime) } modResources { @@ -117,7 +150,7 @@ tasks { } processResources { - from(project(":archie-test-common").sourceSets.main.get().resources) { + from(common.sourceSets.main.get().resources) { include("assets/archie_test/**") include("data/archie_test/**") include("archie_test.common.json") @@ -148,7 +181,7 @@ tasks { jar.get().archiveClassifier.set("dev") sourcesJar { - val commonSources = project(":archie-test-common").tasks.sourcesJar + val commonSources = common.tasks.sourcesJar dependsOn(commonSources) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(commonSources.get().archiveFile.map { zipTree(it) }) diff --git a/test/neoforge/build.gradle.kts b/test/neoforge/build.gradle.kts index e8f6213de..944915af2 100644 --- a/test/neoforge/build.gradle.kts +++ b/test/neoforge/build.gradle.kts @@ -1,4 +1,7 @@ +import dev.kikugie.stonecutter.build.StonecutterBuildExtension import net.kernelpanicsoft.archie.plugin.bundleMod +import net.kernelpanicsoft.archie.plugin.runtimeLibrary +import org.gradle.api.tasks.bundling.Jar plugins { alias(libs.plugins.shadow) @@ -10,8 +13,21 @@ architectury { neoForge() } +// Same-tree sibling. See core/fabric/build.gradle.kts for why node.sibling() is used here. +val commonNode = requireNotNull(extensions.getByType().node.sibling("common")) { + "No common project for $project" +} +val common: Project = commonNode.project + +// Cross-tree references: test, core, datagen and gametest are separate Stonecutter trees, so +// node.sibling() (which only searches within the current tree) doesn't reach them - resolve the +// paths directly instead. +val coreNeoforge = rootProject.project(":core:neoforge:${stonecutter.current.version}") +val datagenNeoforge = rootProject.project(":datagen:neoforge:${stonecutter.current.version}") +val gametestNeoforge = rootProject.project(":gametest:neoforge:${stonecutter.current.version}") + actualizer { - actualizes(project(":archie-test-common")) + actualizes(common) } configurations { @@ -29,8 +45,8 @@ configurations { } loom { - log4jConfigs.from(project(":archie-test-common").loom.log4jConfigs) - accessWidenerPath.set(project(":archie-test-common").loom.accessWidenerPath) + log4jConfigs.from(common.loom.log4jConfigs) + accessWidenerPath.set(common.loom.accessWidenerPath) mods { maybeCreate("main").apply { @@ -104,14 +120,28 @@ dependencies { bundleMod(libs.storage.neoforge) { exclude(group = "curse.maven") } implementation(libs.junit.jupiter.api) + implementation(libs.kotlinx.coroutines.test) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) - "common"(project(":archie-test-common", "namedElements")) { isTransitive = false } - "shadowCommon"(project(":archie-test-common", "transformProductionNeoForge")) { isTransitive = false } - api(project(":archie-core-neoforge", "namedElements")) - api(project(":archie-datagen-neoforge", "namedElements")) - api(project(":archie-gametest-neoforge", "namedElements")) + // See core/fabric/build.gradle.kts and gametest/neoforge/build.gradle.kts for why these depend + // on the sibling's "jar" task output directly, and why the Compose/coroutines deps above and + // below are repeated - the actualizer merges test-common's own source files into this project's + // own compilation, so it needs test-common's compile-time deps directly too, not just its output. + "common"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + "shadowCommon"(files(common.tasks.named("jar").flatMap { it.archiveFile })) + api(files(coreNeoforge.tasks.named("jar").flatMap { it.archiveFile })) + api(files(datagenNeoforge.tasks.named("jar").flatMap { it.archiveFile })) + api(files(gametestNeoforge.tasks.named("jar").flatMap { it.archiveFile })) + // files() dependencies carry no runtime GAMELIBRARY discovery either - the serialization format + // add-ons core-neoforge bundles at runtime (nbt/toml/json5, needed by Archie's own Config system + // at init) don't propagate, so they're repeated here too. Confirmed missing live: a + // NoClassDefFoundError for io.github.xn32.json5k.ConfigBuilder when actually launching this + // project. + runtimeLibrary(libs.kotlinx.serialization.nbt) + runtimeLibrary(libs.kotlinx.serialization.toml) + runtimeLibrary(libs.kotlinx.serialization.json5) + runtimeLibrary(compose.runtime) } modResources { @@ -126,7 +156,7 @@ tasks { } processResources { - from(project(":archie-test-common").sourceSets.main.get().resources) { + from(common.sourceSets.main.get().resources) { include("assets/archie_test/**") include("data/archie_test/**") include("archie_test.common.json") @@ -159,11 +189,11 @@ tasks { jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(project(":archie-test-common").sourceSets.main.get().output) + from(common.sourceSets.main.get().output) } sourcesJar { - val commonSources = project(":archie-test-common").tasks.sourcesJar + val commonSources = common.tasks.sourcesJar dependsOn(commonSources) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(commonSources.get().archiveFile.map { zipTree(it) }) diff --git a/test/stonecutter.gradle.kts b/test/stonecutter.gradle.kts new file mode 100644 index 000000000..501dc597c --- /dev/null +++ b/test/stonecutter.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("dev.kikugie.stonecutter") +} + +stonecutter active "1.21.1"