Files
snowy/lib/mkMod.nix
2026-08-12 07:09:10 -05:00

183 lines
7.5 KiB
Nix

{ pkgs, lib }:
let
# Static per-Minecraft-version pins. Real "latest-recommended" resolution
# would need network access at eval time, so for now every supported
# targetVersion needs an entry here.
versionTable = {
"1.21.1" = {
neoVersion = "21.1.244";
minecraftVersionRange = "[1.21.1]";
loaderVersionRange = "[1,)";
parchmentMinecraftVersion = "1.21.1";
parchmentMappingsVersion = "2024.11.17";
templateDir = "1.21.x";
};
};
trace = value: builtins.trace value value;
in
{ modPath }:
let
evaluated = lib.evalModules {
modules = [ ../modules/mod-meta.nix ../modules/mod-blocks.nix modPath ];
};
cfg = evaluated.config;
versionInfo =
versionTable.${cfg.targetVersion} or (throw
"snowy: unsupported targetVersion '${cfg.targetVersion}'. Supported versions: ${lib.concatStringsSep ", " (lib.attrNames versionTable)}");
resolvedNeoVersion =
if cfg.frameworkVersion == "latest-recommended"
then versionInfo.neoVersion
else cfg.frameworkVersion;
# Hand-written files (customClass, blocks.*.customClass, ...) are given as plain
# strings, e.g. "./src/CreateSilicon.java" -- snowy never reads their content or
# location, it only needs the class name (the file's basename) to generate the
# matching Base class and wire up imports/registration on the generated side. The
# actual file is expected to already live at the real Gradle-conventional path
# (src/main/java/{package}/...) in the project directory the shellHook runs from.
classNameFromPath = p: if p == null then null else lib.removeSuffix ".java" (baseNameOf p);
splitBlockId = id:
let parts = lib.splitString ":" id;
in
assert lib.assertMsg (builtins.length parts == 2)
"snowy: block id '${id}' must be of the form '<namespace>:<path>'";
{ namespace = builtins.elemAt parts 0; path = builtins.elemAt parts 1; };
blocksList = lib.mapAttrsToList
(id: blockCfg:
let
split = splitBlockId id;
hasEnoughTextures = blockCfg.textures != null && (
blockCfg.textures.all != null
|| (blockCfg.textures.top != null && blockCfg.textures.bottom != null
&& (blockCfg.textures.side != null
|| (blockCfg.textures.north != null && blockCfg.textures.south != null
&& blockCfg.textures.east != null && blockCfg.textures.west != null)))
);
in
assert lib.assertMsg (split.namespace == cfg.modid)
"snowy: block '${id}' namespace must match modid '${cfg.modid}'";
assert lib.assertMsg (!(blockCfg.textures != null && blockCfg.customModel))
"snowy: block '${id}': textures and customModel are mutually exclusive";
assert lib.assertMsg (blockCfg.customModel || hasEnoughTextures)
"snowy: block '${id}': textures must set 'all', or 'top'+'bottom'+('side' or all four side faces), unless customModel is set";
assert lib.assertMsg (!(blockCfg.item.texture != null && blockCfg.item.customModel))
"snowy: block '${id}': item.texture and item.customModel are mutually exclusive";
{
inherit id;
inherit (split) path;
displayName = blockCfg.displayName;
properties = blockCfg.properties;
customClassName = classNameFromPath blockCfg.customClass;
textures = blockCfg.textures;
customModel = blockCfg.customModel;
item = blockCfg.item;
blockEntity =
if blockCfg.blockEntity == null then null
else {
customClassName = classNameFromPath blockCfg.blockEntity.class;
tickerClient = blockCfg.blockEntity.ticker.client;
tickerServer = blockCfg.blockEntity.ticker.server;
};
})
cfg.blocks;
capitalizedModid = lib.toUpper (lib.substring 0 1 cfg.modid) + lib.substring 1 (-1) cfg.modid;
templateContext = cfg // {
neoVersion = resolvedNeoVersion;
inherit (versionInfo) minecraftVersionRange loaderVersionRange parchmentMinecraftVersion parchmentMappingsVersion;
customClassName = classNameFromPath cfg.customClass;
customClientClassName = classNameFromPath cfg.customClientClass;
blocks = blocksList;
blocksClassName = "${capitalizedModid}Blocks";
blockEntitiesClassName = "${capitalizedModid}BlockEntities";
};
configJson = pkgs.writeText "snowy-mod-config-${cfg.modid}.json" (builtins.toJSON templateContext);
packagePath = builtins.replaceStrings [ "." ] [ "/" ] cfg.javaPackage;
pythonEnv = pkgs.python3.withPackages (ps: [ ps.jinja2 ]);
generated = pkgs.runCommand "snowy-generated-${cfg.modid}" {
nativeBuildInputs = [ pythonEnv ];
} ''
mkdir -p $out
python3 ${../codegen/main.py} \
--config ${configJson} \
--templates ${../templates}/${versionInfo.templateDir} \
--out $out
'';
# The `gradle wrapper` task validates its distribution URL over the network,
# which the (non-fixed-output) Nix build sandbox blocks. Instead, fetch the
# three wrapper artifacts verbatim (fixed-output derivations may use the
# network) from the upstream NeoForge MDK, which already pins Gradle 9.2.1,
# and write gradle-wrapper.properties ourselves.
gradleWrapperVersion = "9.2.1";
mdkWrapperBase = "https://raw.githubusercontent.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle/main";
gradlew = pkgs.fetchurl {
url = "${mdkWrapperBase}/gradlew";
sha256 = "0284687nawivy3gz16r4zcssv61a3g1ssvyxmxh5g7vl5v2v91xi";
};
gradlewBat = pkgs.fetchurl {
url = "${mdkWrapperBase}/gradlew.bat";
sha256 = "1kg2qjh904cz6da71l30pa1sfbdb707lr4lp4c1jvclgxc9jf44l";
};
gradleWrapperJar = pkgs.fetchurl {
url = "${mdkWrapperBase}/gradle/wrapper/gradle-wrapper.jar";
sha256 = "0wk157gn9nh2d7v9j7mfy46csb31iv5qxsx4qsdvachwvv24lfkx";
};
gradleWrapperProperties = pkgs.writeText "gradle-wrapper.properties" ''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-${gradleWrapperVersion}-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
'';
gradleWrapper = pkgs.runCommand "snowy-gradle-wrapper" { } ''
mkdir -p $out/gradle/wrapper
install -m755 ${gradlew} $out/gradlew
install -m755 ${gradlewBat} $out/gradlew.bat
install -m644 ${gradleWrapperJar} $out/gradle/wrapper/gradle-wrapper.jar
install -m644 ${gradleWrapperProperties} $out/gradle/wrapper/gradle-wrapper.properties
'';
in
assert lib.assertMsg (cfg.targetFramework == [ "neoforge" ])
"snowy: only targetFramework = [ \"neoforge\" ] is currently supported, got: [ ${lib.concatStringsSep " " cfg.targetFramework} ]";
pkgs.mkShell {
packages = [ pkgs.jdk21 pkgs.gradle ];
shellHook = ''
echo "snowy: refreshing generated NeoForge project files in $(pwd)"
# Only ever touch this explicit, framework-owned set of paths -- this is the
# real project root now (not an isolated throwaway directory), so it may also
# contain the mod author's own unrelated files (.git, flake.nix, an IDE config,
# hand-written sources outside src/main/java/**/generated, ...) that must never
# be swept up by this cleanup.
rm -rf build.gradle settings.gradle gradle.properties .gitignore \
gradlew gradlew.bat gradle \
src/main/resources/META-INF \
"src/main/resources/assets/${cfg.modid}/blockstates" \
"src/main/java/${packagePath}/generated"
cp -r --no-preserve=mode,ownership ${generated}/. ./
cp -r --no-preserve=mode,ownership ${gradleWrapper}/. ./
chmod +x gradlew
echo "snowy: done. ./gradlew build"
'';
}