Ctrl K
ComunidadGuías…

flake.nix

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
{
  description = "Spigot Minecraft server — Docker image";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

    # Shared OCI helpers (createdFromDate, fixOciImageHistory, secondsToNanos).
    nix-utils.url = "github:Team-MaRo/nix-utils";
    nix-utils.inputs.nixpkgs.follows = "nixpkgs";

    # Our PID-1 init (PTY console + named-pipe injection + signal→stop), built
    # from its own repo. The image bakes its binary in and execs it as PID 1.
    mc-server-init.url = "github:Team-MaRo/mc-server-init";
    mc-server-init.inputs.nixpkgs.follows = "nixpkgs";

    # Spigot jars are built + published by their own repo. We consume the
    # finished jar as a hash-pinned fetch (spigot-build pins the hash; we just
    # reference the version). Bump available versions/hashes with
    # `nix flake update spigot-build`.
    spigot-build.url = "github:Team-MaRo/spigot-build";
    spigot-build.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, nixpkgs, nix-utils, mc-server-init, spigot-build }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" ];
      forAllSystems = f: nixpkgs.lib.genAttrs systems
        (system: f (import nixpkgs { inherit system; }));

      dataDir = "/srv/spigot"; # FHS: site-specific data served by this system
      uid = "65532";

      # Per-Minecraft-version runtime JDK. The version→major mapping is owned by
      # spigot-build (`spigot-build.lib.jdkMajorFor`); here we only map the major
      # to a JRE. Major "16" is NOT here: nixpkgs ships no JDK 16 and Spigot
      # 1.17/1.17.1 reject Java 17+ at boot ("Only up to Java 16 is supported"),
      # so it needs a real Java 16 — handled by the `temurin16` fetch below.
      jdkForPkgs = pkgs: {
        "8" = pkgs.jdk8_headless;
        "17" = pkgs.jdk17_headless;
        "21" = pkgs.jdk21_headless;
        "25" = pkgs.jdk25_headless;
      };

      # docker/metadata-action labels (KEY=VAL\n…) serialised to JSON by CI and
      # read via `--impure`. This is the ONLY impure input — image metadata, not a
      # build artifact. Pure builds (no --impure) simply get no labels.
      labelsJson = builtins.getEnv "DOCKER_LABELS_JSON";
      labels = if labelsJson == "" then { } else builtins.fromJSON labelsJson;

      # Newest released version (semver), used for the `default`/`dockerImage`
      # outputs. Per-version images are also exposed as `packages.<sys>."<ver>"`.
      versions = spigot-build.lib.versions;
      latest =
        if versions == [ ] then null
        else nixpkgs.lib.last (builtins.sort (a: b: builtins.compareVersions a b < 0) versions);
    in
    {
      packages = forAllSystems (pkgs:
        let
          system = pkgs.stdenv.hostPlatform.system;
          jdkFor = jdkForPkgs pkgs;

          # Ship a runtime, not the full JDK (the ~680 MB headless JDK is mostly
          # jmods/ + dev tools + debug that a server never uses). Java 9+ → jlink a
          # runtime with EVERY module (ALL-MODULE-PATH) so any plugin still works;
          # only dev tools/jmods/debug are dropped. Java 8 predates jlink, so ship
          # the (comparatively small) headless JDK 8 as-is. Source stays nixpkgs
          # openjdk in both cases (single distributor, clean glibc closure). The
          # headless source keeps java.desktop's AWT libs (no X11) — required for
          # server-icon.png / map rendering.
          # jre_minimal takes both `jdk` (jmods to link) and `jdkOnBuild` (the
          # jlink binary). Override BOTH to our chosen JDK, else jlink defaults to
          # an older JDK and can't read newer jmods ("Unsupported major.minor
          # version"). Native per-arch builds, so jdkOnBuild == jdk is correct.
          #
          # jlink bakes the source JDK's store path into lib/modules, which would
          # drag the full ~680 MB JDK into the runtime closure. That path is
          # vestigial (the jlink image is self-contained; the launcher's
          # interpreter/RPATH point only at glibc/zlib), so scrub it with
          # remove-references-to and assert it's gone via disallowedReferences.
          # Major 8 (predates jlink) and 16 (already a jlinked+patched JRE —
          # temurin16Jre, built below) ship as-is; everything else is a nixpkgs
          # JDK that gets jlinked here to an ALL-MODULE-PATH runtime.
          mkJre = major: jdk:
            if major == "8" || major == "16" then jdk
            else
              let
                raw = pkgs.jre_minimal.override {
                  inherit jdk;
                  jdkOnBuild = jdk;
                  modules = [ "ALL-MODULE-PATH" ];
                };
              in
              pkgs.runCommand "spigot-jre-${major}"
                {
                  nativeBuildInputs = [ pkgs.removeReferencesTo ];
                  disallowedReferences = [ jdk ];
                } ''
                cp -r ${raw} $out
                chmod -R u+w $out
                find $out -type f -exec remove-references-to -t ${jdk} {} +
              '';

          # Our own PID-1 init, from the github:Team-MaRo/mc-server-init flake
          # (Minecraft-server-generic, so the same binary serves a Paper image
          # too). Unlike itzg's mc-server-runner it runs the server behind a PTY,
          # so the JLine console keeps the `>` prompt and line-editing; it still
          # forwards the terminal AND a named pipe into the console (interactive
          # `docker attach` + scripted `console <cmd>`, no RCON) and turns
          # SIGTERM / SIGINT / a typed Ctrl+C into a clean `stop` (graceful save
          # with the "Saving" logs), SIGKILL only after a timeout.
          mcServerInit = mc-server-init.packages.${system}.default;

          # `docker exec <c> console <cmd>` → inject a console command via the
          # named pipe mc-server-init feeds to the server's stdin (no RCON).
          console = pkgs.writeShellScriptBin "console" ''
            printf '%s\n' "$*" > /tmp/console-in
          '';

          inherit (nix-utils.lib.oci) secondsToNanos createdFromDate;
          fixHistoryScript = nix-utils.packages.${system}.fixOciImageHistory;

          # Java 16 for 1.17/1.17.1. nixpkgs (nixos-26.05) ships no JDK 16 and
          # those versions reject Java 17+ at boot ("Only up to Java 16 is
          # supported"). Adoptium published no standalone Temurin 16 JRE, so fetch
          # the JDK, autoPatchelf it to run in the minimal image (a build-time
          # tool only — never shipped), then jlink a proper JRE from it below.
          javaLibs = [
            pkgs.stdenv.cc.cc.lib
            pkgs.zlib
            pkgs.alsa-lib
            pkgs.fontconfig.lib
            pkgs.freetype
            pkgs.cups.lib
            pkgs.xorg.libX11
            pkgs.xorg.libXext
            pkgs.xorg.libXi
            pkgs.xorg.libXrender
            pkgs.xorg.libXtst
          ];
          temurin16Jdk =
            let
              sel = {
                "x86_64-linux" = { arch = "x64"; hash = "sha256-Mj1tdHSjWaKO/33dDfjmW9YVVKjtEu9C/ZNlNJ5XPCw="; };
                "aarch64-linux" = { arch = "aarch64"; hash = "sha256-y3fZ0Sb5eJjf3ItftpTR4OXZPROgpssq7advhjU4Q0A="; };
              }.${system};
            in
            pkgs.stdenv.mkDerivation {
              pname = "temurin-jdk-bin";
              version = "16.0.2+7";
              src = pkgs.fetchurl {
                url = "https://github.com/adoptium/temurin16-binaries/releases/download/jdk-16.0.2%2B7/OpenJDK16U-jdk_${sel.arch}_linux_hotspot_16.0.2_7.tar.gz";
                hash = sel.hash;
              };
              nativeBuildInputs = [ pkgs.autoPatchelfHook ];
              buildInputs = javaLibs;
              dontConfigure = true;
              dontBuild = true;
              installPhase = ''
                runHook preInstall
                mkdir -p $out
                cp -a . $out/
                runHook postInstall
              '';
            };

          # Ship a JRE, not the full JDK: jlink an ALL-MODULE-PATH runtime (every
          # module → any plugin works) from the patched JDK, then autoPatchelf it
          # (the launcher/libs come from the JDK's jmods, so they carry Adoptium's
          # interpreter/RPATH and need re-patching) and scrub the JDK store path
          # jlink bakes into lib/modules, so the full JDK stays out of the closure.
          # This mirrors mkJre's jlink path for the nixpkgs JDKs (9+).
          temurin16Jre = pkgs.stdenv.mkDerivation {
            name = "spigot-jre-16";
            dontUnpack = true;
            # mkDerivation (not runCommand) so the fixup phase runs autoPatchelfHook
            # over the jlink output — runCommand skips fixup, leaving the launcher's
            # interpreter unpatched ("no such file or directory" on exec).
            nativeBuildInputs = [ pkgs.autoPatchelfHook pkgs.removeReferencesTo ];
            buildInputs = javaLibs;
            disallowedReferences = [ temurin16Jdk ];
            installPhase = ''
              runHook preInstall
              # No --compress: jlink bakes the JDK store path into lib/modules,
              # and remove-references-to below rewrites it in place (same length →
              # offsets stay valid). Compressing would put that path inside a
              # compressed blob, so the byte-rewrite corrupts it → SIGSEGV at boot.
              ${temurin16Jdk}/bin/jlink \
                --module-path ${temurin16Jdk}/jmods \
                --add-modules ALL-MODULE-PATH \
                --strip-debug --no-man-pages --no-header-files \
                --output $out
              chmod -R u+w $out
              find $out -type f -exec remove-references-to -t ${temurin16Jdk} {} +
              runHook postInstall
            '';
          };

          # The Nix minimal image ships no distro release files, so the JVM
          # (oshi/JNA, crash reports) logs "File not found or not readable:
          # /etc/os-release" / "/etc/lsb-release" on every boot. nixpkgs has no
          # prebuilt os-release package (only the NixOS module generates one), so
          # provide them declaratively — same writeTextDir approach NixOS uses —
          # with VERSION_ID taken from nixpkgs rather than hardcoded.
          osReleaseFiles = pkgs.symlinkJoin {
            name = "spigot-os-release";
            paths = [
              (pkgs.writeTextDir "etc/os-release" ''
                NAME="Spigot (Nix)"
                ID=spigot
                PRETTY_NAME="d3strukt0r/spigot (Nix-built minimal image)"
                VERSION_ID="${nixpkgs.lib.trivial.release}"
                BUILD_ID="${nixpkgs.lib.trivial.version}"
                HOME_URL="https://github.com/Team-MaRo/docker-spigot"
              '')
              (pkgs.writeTextDir "etc/lsb-release" ''
                DISTRIB_ID=Spigot
                DISTRIB_RELEASE="${nixpkgs.lib.trivial.release}"
                DISTRIB_DESCRIPTION="d3strukt0r/spigot (Nix-built minimal image)"
              '')
            ];
          };

          # Build the image for one Minecraft version. The jar is the pinned fetch
          # from spigot-build (no impure path); the runtime JDK is chosen from the
          # version via spigot-build.lib.jdkMajorFor.
          mkImage = version:
            let
              javaMajor = spigot-build.lib.jdkMajorFor version;
              # Major 16 is already a jlinked JRE (temurin16Jre); mkJre ships it
              # as-is. Everything else is a nixpkgs JDK that mkJre jlinks.
              jdkBase = if javaMajor == "16" then temurin16Jre
                        else jdkFor.${javaMajor} or pkgs.jdk25_headless;
              jre = mkJre javaMajor jdkBase;

              # The entrypoint with its runtime PATH baked in (java/yq/coreutils/
              # grep + mc-server-init). writeShellApplication shellchecks it.
              entrypoint = pkgs.writeShellApplication {
                name = "spigot-entrypoint";
                runtimeInputs = [ jre pkgs.yq-go pkgs.coreutils pkgs.gnugrep pkgs.bashInteractive mcServerInit ];
                text = builtins.readFile ./src/entrypoint.sh;
              };

              # Bake the (pinned) jar at /opt/spigot.jar — an image layer, never in
              # the data volume, so it is not on the host.
              jarLayer = pkgs.runCommand "spigot-jar-layer" { } ''
                mkdir -p $out/opt
                cp ${spigot-build.legacyPackages.${system}.spigotJar.${version}} $out/opt/spigot.jar
              '';

              dockerImageStream = pkgs.dockerTools.streamLayeredImage {
                name = "d3strukt0r/spigot";
                tag = version;
                created = createdFromDate self.lastModifiedDate;

                contents = [
                  pkgs.dockerTools.usrBinEnv
                  (pkgs.dockerTools.fakeNss.override {
                    extraPasswdLines = [ "nonroot:x:65532:65532:nonroot:${dataDir}:/sbin/nologin" ];
                    extraGroupLines = [ "nonroot:x:65532:" ];
                  })
                  pkgs.bashInteractive
                  pkgs.coreutils
                  pkgs.gnugrep
                  pkgs.yq-go
                  jre
                  # libudev for the bundled oshi/JNA. Listed as a content so its lib/
                  # is merged into /lib alongside the JRE's libs (same as every other
                  # package here) — oshi then finds it by name with no env var, flag,
                  # or hand-made symlink. Otherwise it would sit only in /nix/store,
                  # which nothing scans. Without it oshi logs "Did not find udev".
                  pkgs.systemdLibs
                  osReleaseFiles
                  entrypoint
                  console
                  jarLayer
                ];

                # The data dir is the server working directory and the single
                # persistence volume; it must be writable by the nonroot user.
                # /etc/profile.d is made writable too so the entrypoint can drop
                # spigot-env.sh there (the resolved env, for `docker exec … sh -l`);
                # the nix image ships no /etc/profile, so we add one that sources it.
                enableFakechroot = true;
                fakeRootCommands = ''
                  mkdir -p .${dataDir}
                  chown -R ${uid}:${uid} .${dataDir}
                  chown ${uid}:${uid} etc/profile.d
                '';
                extraCommands = ''
                  mkdir -p tmp
                  chmod 1777 tmp
                  mkdir -p etc/profile.d
                  printf '%s\n' 'for f in /etc/profile.d/*.sh; do [ -r "$f" ] && . "$f"; done' > etc/profile
                '';

                config = {
                  User = "${uid}:${uid}";
                  WorkingDir = dataDir;
                  Entrypoint = [ "${entrypoint}/bin/spigot-entrypoint" ];
                  Env = [
                    "HOME=${dataDir}"
                    "JVM_FLAGS_PRESET=aikars"
                    "JAVA_MAJOR=${javaMajor}"
                    "PATH=/bin:/usr/bin"
                    # 🐳 prompt for `docker exec -it … bash`. Bash keeps an inherited
                    # PS1 (only sets its default when PS1 is unset), so no rc file is
                    # needed. <green> user@host <normal> : <blue> dir <normal> $
                    ''PS1=🐳 \e[38;5;10m\u@\h\e[0m:\e[38;5;12m\w\e[0m\$ ''
                  ];
                  ExposedPorts = { "25565/tcp" = { }; };
                  Volumes = { "${dataDir}" = { }; };
                  # /dev/tcp is a bash feature, so invoke bash explicitly rather than
                  # via CMD-SHELL (/bin/sh — which here is only a symlink to bash).
                  # Opening the socket exits 0 if the port is accepting, which proves
                  # the port is open, not that worlds/plugins finished loading.
                  Healthcheck = {
                    Test = [ "CMD" "bash" "-c" "exec 3<>/dev/tcp/localhost/25565" ];
                    Interval = secondsToNanos 30;
                    Timeout = secondsToNanos 5;
                    StartPeriod = secondsToNanos 120;
                  };
                  Labels = labels;
                };
              };
            in
            pkgs.runCommand "spigot-image-${version}.tar" { } ''
              ${dockerImageStream} | ${fixHistoryScript} > $out
            '';

          # One image per released version: `nix build --impure .#"26.1.2"`.
          perVersion = nixpkgs.lib.genAttrs versions mkImage;
        in
        perVersion // nixpkgs.lib.optionalAttrs (latest != null) {
          default = perVersion.${latest};
          dockerImage = perVersion.${latest}; # convenience alias for the newest
        });

      # CI helper: the image build matrix (each released version × its runtime
      # JDK) derived from the PINNED spigot-build input — so it matches exactly
      # what `nix build .#"<ver>"` will build. Read with `nix eval --json .#imageMatrix`.
      imageMatrix = builtins.map (v: { spigot = v; java = spigot-build.lib.jdkMajorFor v; }) versions;
    };
}