diff --git a/README.md b/README.md index f14c5bca..644f48fd 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ As of currently, it packages: - Various tools - `nix-modrinth-prefetch` - `fetchPackwizModpack` + - `fetchModrinthModpack` Check out this video by vimjoyer that provides a brief overview of how to use the flake: https://youtu.be/Fph7SMldxpI @@ -249,6 +250,36 @@ in **Note**: Using `manifest`, by default, will cause [IFD](https://nixos.wiki/wiki/Import_From_Derivation). If you want to avoid IFD while still having access to `manifest`, simply pass a `manifestHash` to the `fetchPackwizModpack` function, it will then fetch the manifest through `builtins.fetchurl`. +### `fetchModrinthModpack` + +[Source](./pkgs/tools/fetchModrinthModpack) + +This function packages a Modrinth `.mrpack` modpack from either a URL, a local `.mrpack` file, or a local source directory containing `index.json` and optional overrides. + +```nix +let + modpack = pkgs.fetchModrinthModpack { + url = "https://cdn.modrinth.com/data/PROJECT_ID/versions/VERSION_ID/modpack.mrpack"; + packHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + side = "server"; + }; +in +{ + services.minecraft-servers.servers.cool-modpack = { + enable = true; + package = pkgs.fabricServers.fabric-1_21_5.override { loaderVersion = "0.16.14"; }; + symlinks = { + "mods" = "${modpack}/mods"; + }; + files = { + "config" = "${modpack}/config"; + }; + }; +} +``` + +The resulting derivation contains downloaded pack files and applied overrides. Like `fetchPackwizModpack`, it also exposes a `manifest` attribute and an `addFiles` helper. + ### Others All of these packages are also available under `packages`, not just `legacyPackages`. diff --git a/pkgs/tools/fetchModrinthModpack/default.nix b/pkgs/tools/fetchModrinthModpack/default.nix new file mode 100644 index 00000000..9e3ac242 --- /dev/null +++ b/pkgs/tools/fetchModrinthModpack/default.nix @@ -0,0 +1,188 @@ +{ + lib, + stdenvNoCC, + jq, + moreutils, + curl, + cacert, + unzip, + coreutils, +}: + +let + fetchModrinthModpack = + { + # Provide a path to the modpack source directory (containing index.json), + # a local .mrpack archive, or a URL to a .mrpack archive. + src ? null, + url ? null, + packHash ? "", + # Either 'server', 'client' or 'both' (to get all files) + side ? "server", + ... + }@args: + let + srcNull = src == null; + urlNull = url == null; + srcPath = if srcNull then "" else src; + urlPath = if urlNull then "" else url; + drv = fetchModrinthModpack args; + in + + assert lib.assertMsg ( + srcNull != urlNull # equivalent of (src != null) xor (url != null) + ) "Either 'src' or 'url' must be provided to fetchModrinthModpack"; + + assert lib.assertMsg (builtins.elem side [ + "server" + "client" + "both" + ]) "'side' must be one of: server, client, both"; + + stdenvNoCC.mkDerivation ( + { + pname = args.pname or "modrinth-pack"; + version = args.version or ""; + + dontUnpack = true; + + buildInputs = [ + jq + moreutils + curl + cacert + unzip + coreutils + ]; + + buildPhase = '' + set -euo pipefail + runHook preBuild + + mkdir -p pack-src + + if [ "${if !urlNull then "1" else "0"}" = "1" ]; then + curl -L "${urlPath}" > modpack.mrpack + unzip -q modpack.mrpack -d pack-src + elif [ -d "${srcPath}" ]; then + cp -r "${srcPath}"/. pack-src/ + else + unzip -q "${srcPath}" -d pack-src + fi + + test -f pack-src/index.json + + while IFS= read -r file; do + if [ "${side}" != "both" ]; then + envState=$(echo "$file" | jq -r --arg side "${side}" '.env[$side] // "required"') + if [ "$envState" = "unsupported" ]; then + continue + fi + fi + + path=$(echo "$file" | jq -r '.path') + url=$(echo "$file" | jq -r '.downloads[0]') + mkdir -p "$(dirname "$path")" + curl -L "$url" > "$path" + + if echo "$file" | jq -e '.hashes.sha512 != null' > /dev/null; then + expected=$(echo "$file" | jq -r '.hashes.sha512') + actual=$(${coreutils}/bin/sha512sum "$path" | cut -d' ' -f1) + elif echo "$file" | jq -e '.hashes.sha1 != null' > /dev/null; then + expected=$(echo "$file" | jq -r '.hashes.sha1') + actual=$(${coreutils}/bin/sha1sum "$path" | cut -d' ' -f1) + else + echo "No supported hash for '$path' (supported: sha512, sha1)" >&2 + exit 1 + fi + + if [ "$actual" != "$expected" ]; then + echo "Hash mismatch for '$path'" >&2 + echo "expected: $expected" >&2 + echo "actual: $actual" >&2 + exit 1 + fi + done < <(jq -c '.files[]' pack-src/index.json) + + if [ -d pack-src/overrides ]; then + cp -r pack-src/overrides/. . + fi + + if [ "${side}" = "server" ] && [ -d pack-src/server-overrides ]; then + cp -r pack-src/server-overrides/. . + fi + + if [ "${side}" = "client" ] && [ -d pack-src/client-overrides ]; then + cp -r pack-src/client-overrides/. . + fi + + if [ "${side}" = "both" ]; then + if [ -d pack-src/server-overrides ]; then + cp -r pack-src/server-overrides/. . + fi + if [ -d pack-src/client-overrides ]; then + cp -r pack-src/client-overrides/. . + fi + fi + + # Keep the source manifest in output for passthru consumers. + cp pack-src/index.json ./index.json + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + # Fix non-determinism + rm -rf env-vars + jq -Sc '.' index.json | sponge index.json + + mkdir -p "$out" + cp * -r "$out/" + + runHook postInstall + ''; + + passthru = { + # Modrinth index manifest as a nix expression. + manifest = builtins.fromJSON (builtins.readFile "${drv}/index.json"); + + # Adds an attribute set of files to the derivation. + # Useful to add server-specific mods not part of the pack. + addFiles = + files: + stdenvNoCC.mkDerivation { + inherit (drv) pname version; + src = null; + dontUnpack = true; + dontConfig = true; + dontBuild = true; + dontFixup = true; + + installPhase = '' + cp -as "${drv}" $out + chmod u+w -R $out + '' + + lib.concatLines ( + lib.mapAttrsToList (name: file: '' + mkdir -p "$out/$(dirname "${name}")" + cp -as "${file}" "$out/${name}" + '') files + ); + + passthru = { inherit (drv) manifest; }; + meta = drv.meta or { }; + }; + }; + + dontFixup = true; + + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + outputHash = packHash; + } + // args + ); +in +fetchModrinthModpack diff --git a/tests/modrinth-modpack-from-src/default.nix b/tests/modrinth-modpack-from-src/default.nix new file mode 100644 index 00000000..9265a989 --- /dev/null +++ b/tests/modrinth-modpack-from-src/default.nix @@ -0,0 +1,25 @@ +{ + fetchModrinthModpack, + stdenvNoCC, +}: +let + pack = fetchModrinthModpack { + src = ./sample-pack; + packHash = "sha256-OAJrZEVTZx1QZe2ubrLfK/XvRusfIN8cWMbj21TYhms="; + }; +in +stdenvNoCC.mkDerivation { + name = "modrinth-modpack-from-src-check"; + doCheck = true; + phases = [ + "checkPhase" + "installPhase" + ]; + checkPhase = '' + set -euo pipefail + test -f '${pack}/index.json' + test -f '${pack}/mods/lithium-fabric-0.16.2+mc1.21.5.jar' + test -f '${pack}/config/server.properties' + ''; + installPhase = "mkdir $out"; +} diff --git a/tests/modrinth-modpack-from-src/sample-pack/index.json b/tests/modrinth-modpack-from-src/sample-pack/index.json new file mode 100644 index 00000000..52ab8593 --- /dev/null +++ b/tests/modrinth-modpack-from-src/sample-pack/index.json @@ -0,0 +1,27 @@ +{ + "formatVersion": 1, + "game": "minecraft", + "versionId": "sample-pack-1.0.0", + "name": "sample-pack", + "summary": "Sample Modrinth modpack for nix-minecraft tests", + "files": [ + { + "path": "mods/lithium-fabric-0.16.2+mc1.21.5.jar", + "hashes": { + "sha512": "09a68051504bb16069dd6af8901f2bbeadfd08ad5353d8bcc0c4784e814fb293d9197b4fb0a8393be1f2db003cd987a9e4b98391bbe18c50ae181dace20c2fa4" + }, + "downloads": [ + "https://cdn.modrinth.com/data/gvQqBUqZ/versions/VWYoZjBF/lithium-fabric-0.16.2%2Bmc1.21.5.jar" + ], + "fileSize": 769793, + "env": { + "client": "required", + "server": "required" + } + } + ], + "dependencies": { + "minecraft": "1.21.5", + "fabric-loader": "0.16.14" + } +} diff --git a/tests/modrinth-modpack-from-src/sample-pack/overrides/config/server.properties b/tests/modrinth-modpack-from-src/sample-pack/overrides/config/server.properties new file mode 100644 index 00000000..75f9127c --- /dev/null +++ b/tests/modrinth-modpack-from-src/sample-pack/overrides/config/server.properties @@ -0,0 +1 @@ +motd=nix-minecraft test