Skip to main content
The Oyasai Server Platform leverages Nix flakes for reproducible, declarative builds and deployment. This document explains the flake architecture, custom scope, and build system integration.

Flake Overview

Nix flakes provide:
  • Reproducibility: Locked dependencies ensure identical builds
  • Composability: Modular imports and clean interfaces
  • Developer Experience: Built-in dev shells and formatters

Flake Structure

The root flake.nix defines inputs and outputs:
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
    devshell = {
      url = "github:numtide/devshell";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    flake-parts.url = "github:hercules-ci/flake-parts";
    gradle2nix = {
      url = "github:oyasaiserver/gradle2nix?ref=v2";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    package-lock2nix = {
      url = "github:anteriorcore/package-lock2nix";
      inputs.nixpkgs.follows = "nixpkgs";
      inputs.flake-parts.follows = "flake-parts";
      inputs.treefmt-nix.follows = "treefmt-nix";
      inputs.systems.follows = "systems";
    };
    systems.url = "github:nix-systems/default";
    treefmt-nix = {
      url = "github:numtide/treefmt-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
}

Input Following

Notice the inputs.nixpkgs.follows = "nixpkgs" pattern. This ensures all dependencies use the same nixpkgs version, preventing duplication and version conflicts.
Input following reduces closure size and ensures consistent package versions across all dependencies.

Flake-Parts Architecture

The flake uses flake-parts for modular organization:
flake-parts.lib.mkFlake { inherit inputs; } {
  systems = import systems;
  imports = [
    ./nix/devshells.nix
    ./nix/docker.nix
    ./nix/oyasai-scope.nix
    ./nix/treefmt.nix
    devshell.flakeModule
    flakeAllSystems
    treefmt-nix.flakeModule
  ];
};
Each import provides a specific aspect of functionality:

oyasai-scope.nix

Custom package scope with build logic and package definitions

devshells.nix

Development environment configuration with tools

docker.nix

Docker image generation and registry management

treefmt.nix

Code formatting rules for Nix, Kotlin, and other files

Oyasai Scope

The core of the build system is the custom package scope defined in nix/oyasai-scope.nix:
oyasaiScope = lib.makeScope pkgs.newScope (
  scopeSelf:
  let
    inherit (scopeSelf) callPackage;
  in
  {
    # Java toolchain
    jdk = pkgs.javaPackages.compiler.temurin-bin.jdk-25;
    jre = pkgs.javaPackages.compiler.temurin-bin.jre-25;
    gradle = pkgs.gradle_9.override { java = scopeSelf.jdk; };
    
    # Build tools
    gradle2nix = inputs.gradle2nix.builders.${system};
    package-lock2nix = callPackage inputs.package-lock2nix.lib.package-lock2nix {
      inherit (scopeSelf) nodejs;
    };
    
    # Custom packages
    oyasaiPurpur = callPackage ./oyasai-purpur.nix { };
    oyasaiDockerTools = callPackage ./oyasai-docker-tools.nix { };
    
    # Plugin builds
    inherit plugins-batch;
    plugins = /* ... */;
  }
  // lib.packagesFromDirectoryRecursive {
    inherit callPackage;
    directory = ../packages;
  }
);

Scope Benefits

The scope creates a separate namespace preventing conflicts with nixpkgs packages.
callPackage automatically provides dependencies to package definitions.
Packages only built when referenced, improving evaluation performance.
Packages can reference each other within the scope transparently.

gradle2nix Integration

Gradle builds are integrated via the gradle2nix tool:

Dependency Locking

# Generate gradle.lock from build.gradle.kts
nix develop
gradle2nix > gradle.lock
The lock file captures:
  • All Maven dependencies
  • Transitive dependencies
  • SHA256 hashes for verification

Plugin Build

Plugins are built in batch mode:
plugins-batch = scopeSelf.gradle2nix.buildGradlePackage {
  pname = "plugins";
  version = "0.0.0";
  src = with lib.fileset; toSource {
    root = ../.; 
    fileset = unions [
      ../build.gradle.kts
      ../gradle
      ../plugins
      ../settings.gradle.kts
    ];
  };
  inherit (scopeSelf) gradle;
  buildJdk = scopeSelf.jdk;
  lockFile = ../gradle.lock;
  gradleBuildFlags = [ "build" ];
  installPhase = ''
    runHook preInstall
    mkdir -p $out
    cp plugins/*/build/libs/*.jar $out
    runHook postInstall
  '';
};
1

Source Filtering

Only include necessary files in the build using lib.fileset
2

Hermetic Build

All dependencies fetched from lock file, no network access during build
3

Batch Compilation

All plugins built together for efficiency
4

JAR Extraction

Install phase copies all built JARs to output

Individual Plugin Derivations

Each plugin is exposed as a separate derivation:
plugins = lib.mapAttrs' (
  name: _:
  lib.nameValuePair (lib.toLower name) (
    pkgs.runCommand name { } ''
      mkdir -p $out
      cp ${plugins-batch}/${name}.jar $out
    ''
  )
) (builtins.readDir ../plugins);
This creates lightweight derivations that reference the batch build:
nix build .#legacyPackages.x86_64-linux.oyasai-plugins.oyasaiutilities

Server Package Assembly

Server packages use the oyasaiPurpur function:
# packages/oyasai-minecraft-main.nix
{
  oyasaiPurpur,
  oyasai-plugin-registry,
  lib,
  oyasaiDockerTools,
  stdenv,
}:

let
  final = oyasaiPurpur rec {
    name = "oyasai-minecraft-main";
    version = "1.21.8";
    
    plugins = with (oyasai-plugin-registry.forVersion version); [
      essentialsx
      fastasyncworldedit
      luckperms
      plugmanx
      protocollib
      vault
      nuvotifier
      vertex
    ];
    
    passthru = lib.optionalAttrs stdenv.hostPlatform.isLinux {
      docker = oyasaiDockerTools.buildLayeredImage {
        inherit name;
        config.Cmd = [ "${lib.getExe final}" ];
      };
    };
  };
in
final

oyasaiPurpur Builder

The builder function (nix/oyasai-purpur.nix) creates a complete server package:
{
  name,
  version,
  plugins,
  directory ? ".",
  port ? 25565,
  passthru ? { },
  cleanPlugins ? true,
}:

let
  versions = {
    "1.21.8" = {
      build = 2497;
      hash = "sha256-3XsifyVVYBw5zsCR32eCdfCoH6ftaM6VTsSSS7RXXEY=";
    };
  };
  setup = writeShellApplication {
    name = "${name}-setup";
    runtimeInputs = [ coreutils ];
    text = ''
      echo "eula=true" > eula.txt
      mkdir -p plugins
      ${if cleanPlugins then ''
        rm -rf plugins/.paper-remapped
        rm -f plugins/*.jar
      '' else ""}
      ${lib.concatMapStringsSep "\n" (k: "cp --no-preserve=ownership,mode ${k} plugins") plugins}
    '';
  };
in
stdenvNoCC.mkDerivation {
  # Fetches Purpur server JAR
  src = fetchurl {
    url = "https://api.purpurmc.org/v2/purpur/${version}/${toString build}/download";
    inherit hash;
  };
  
  installPhase = ''
    mkdir -p $out/bin $out/lib/minecraft
    cp -v $src $out/lib/minecraft/server.jar
    
    makeWrapper ${jre}/bin/java $out/bin/minecraft-server \
      --run "mkdir -p ${directory}" \
      --chdir ${directory} \
      --run "${lib.getExe setup}" \
      --add-flags "-jar $out/lib/minecraft/server.jar --nogui --port ${toString port}"
  '';
}
The setup script runs before server start, accepting the EULA and copying plugins into the plugins directory.

Docker Image Generation

Docker images use layered builds for efficient caching:
# From docker.nix
all-docker-image-derivs =
  lib.flatten (
    lib.mapAttrsToList (
      name: value:
      lib.optionals (value ? docker) {
        inherit name;
        image = value.docker;
      }
    ) oyasaiScope
  ) ++ lib.mapAttrsToList (name: image: { inherit name image; }) standalone-docker-images;
This creates:
  • Individual image derivations for each package
  • An aggregate all-docker-images package
  • A derivations file mapping image names to store paths

Building Docker Images

# Build single image
nix build .#oyasai-minecraft-main-docker

# Build all images
nix build .#all-docker-images

# Load image into Docker
docker load < result

Development Shells

The dev shell provides all necessary tools:
# nix/devshells.nix
devshells.default = {
  packages = with oyasaiScope; [
    nodejs        # Node.js 24
    jdk           # Temurin JDK 25
    terraform     # Infrastructure as code
    gradle        # Gradle 9
    gradle2nix-cli # Dependency locking
  ];
};
Enter the shell:
nix develop

# Or with direnv
echo "use flake" > .envrc
direnv allow

Package Lock2nix Integration

For Node.js packages, package-lock2nix provides similar functionality to gradle2nix:
package-lock2nix = callPackage inputs.package-lock2nix.lib.package-lock2nix {
  inherit (scopeSelf) nodejs;
  overrideScope = overlays.package-lock2nix;
};
The overlay handles TypeScript configuration:
overlays.package-lock2nix = final: prev: {
  mkNpmModule = args:
    let orig = prev.mkNpmModule args; in
    orig.overrideAttrs (self:
      lib.optionalAttrs (builtins.pathExists (self.src + "/tsconfig.json")) {
        nativeBuildInputs = self.nativeBuildInputs or [ ] ++ [
          pkgs.jq
          pkgs.moreutils
        ];
        prePatch = orig.prePatch or "" + ''
          jq --arg tsconfig ${../tsconfig.json} '
            if has("extends")
            then .extends = $tsconfig
            else .
            end
          ' tsconfig.json | sponge tsconfig.json
        '';
      }
    );
};

Flake Outputs

The flake exposes multiple output types:
nix build .#oyasai-minecraft-main
nix build .#oyasai-minecraft-minimal
nix build .#oyasai-plugin-registry
nix build .#oyasai-push-nix-images

Cross-System Support

The flake supports multiple systems via the systems input:
systems = import systems; # Imports nix-systems/default

flakeAllSystems = {
  perSystem = { system, ... }: {
    _module.args = {
      pkgs = import nixpkgs {
        inherit system;
        config.allowUnfree = true;
      };
    };
  };
};
Supported systems:
  • x86_64-linux
  • aarch64-linux
  • x86_64-darwin
  • aarch64-darwin
Docker images are only built on Linux systems. The configuration uses lib.mkIf (builtins.elem system lib.platforms.linux) to conditionally enable Docker outputs.

Build Checks

All packages and plugins are exposed as checks:
checks = lib.concatMapAttrs (
  k: v: lib.optionalAttrs (availableOnSystem v) { "build-${k}" = v; }
) (
  lib.filterAttrs (_: lib.isDerivation) (oyasaiScope // oyasaiScope.plugins)
);
Run checks:
nix flake check

Working with the Flake

1

Clone Repository

git clone <repository-url>
cd oyasai-server-platform
2

Enter Dev Shell

nix develop
3

Update Dependencies

gradle2nix > gradle.lock
git add gradle.lock
4

Build Package

nix build .#oyasai-minecraft-main
5

Run Server

./result/bin/minecraft-server

Best Practices

Always use inputs.nixpkgs.follows to ensure consistent package versions across all dependencies.
Use lib.fileset for source filtering instead of builtins.filterSource for better performance and readability.
Never commit flake.lock changes without testing builds across all affected systems.
When adding new packages, use callPackage in the scope to enable automatic dependency injection.

Architecture Overview

Understand the overall system design

Plugin System

Learn how plugins are built and packaged

Build docs developers (and LLMs) love