Commit 6706daaa authored by David McFarland's avatar David McFarland
Browse files

godot-mono: use wrapper to find dotnet-sdk

The wrapper allows overriding like:

godot-mono.overrideAttrs { dotnet-sdk = dotnet-sdk_9; }

The unwrapped version is available at godot-mono.unwrapped, and can be
manually used with a dotnet SDK from $PATH.
parent d1343182
Loading
Loading
Loading
Loading
+472 −412
Original line number Diff line number Diff line
@@ -67,12 +67,13 @@ let

  arch = stdenv.hostPlatform.linuxArch;

  dotnet-sdk = dotnetCorePackages.sdk_8_0-source;
  dotnet-sdk = if withMono then dotnetCorePackages.sdk_8_0-source else null;
  dotnet-sdk_alt = if withMono then dotnetCorePackages.sdk_9_0-source else null;

  dottedVersion = lib.replaceStrings [ "-" ] [ "." ] version + lib.optionalString withMono ".mono";

  attrsForTarget =
    target: finalAttrs:
  mkTarget =
    target:
    let
      editor = target == "editor";
      suffix = lib.optionalString withMono "-mono" + lib.optionalString (!editor) "-template";
@@ -86,8 +87,209 @@ let
        ++ [ arch ]
        ++ lib.optional withMono "mono"
      );

      mkTests =
        pkg: dotnet-sdk:
        {
          version = testers.testVersion {
            package = pkg;
            version = dottedVersion;
          };
        }
        // lib.optionalAttrs (editor) (
          let
            project-src =
              runCommand "${pkg.name}-project-src"
                {
                  nativeBuildInputs = [ pkg ] ++ lib.optional (dotnet-sdk != null) dotnet-sdk;
                }
                (
                  ''
                    mkdir "$out"
                    cd "$out"
                    touch project.godot

                    cat >create-scene.gd <<'EOF'
                    extends SceneTree

                    func _initialize():
                      var node = Node.new()
                      var script = ResourceLoader.load("res://test.gd")
                      node.set_script(script)
                  ''
                  + lib.optionalString withMono ''
                    ${""}
                      var monoNode = Node.new()
                      var monoScript = ResourceLoader.load("res://Test.cs")
                      monoNode.set_script(monoScript)
                      node.add_child(monoNode)
                      monoNode.owner = node
                  ''
                  + ''
                      var scene = PackedScene.new()
                      var scenePath = "res://test.tscn"
                      scene.pack(node)
                      node.free()
                      var x = ResourceSaver.save(scene, scenePath)
                      ProjectSettings["application/run/main_scene"] = scenePath
                      ProjectSettings.save()
                      quit()
                    EOF

                    cat >test.gd <<'EOF'
                    extends Node
                    func _ready():
                      print("Hello, World!")
                      get_tree().quit()
                    EOF

                    cat >export_presets.cfg <<'EOF'
                    [preset.0]
                    name="build"
                    platform="Linux"
                    runnable=true
                    export_filter="all_resources"
                    include_filter=""
                    exclude_filter=""
                    [preset.0.options]
                    binary_format/architecture="${arch}"
                    EOF
                  ''
                  + lib.optionalString withMono ''
                    cat >Test.cs <<'EOF'
                    using Godot;
                    using System;

                    public partial class Test : Node
                    {
                      public override void _Ready()
                      {
                        GD.Print("Hello, Mono!");
                        GetTree().Quit();
                      }
                    }
                    EOF

                    sdk_version=$(basename ${pkg}/share/nuget/packages/godot.net.sdk/*)
                    cat >UnnamedProject.csproj <<EOF
                    <Project Sdk="Godot.NET.Sdk/$sdk_version">
                      <PropertyGroup>
                        <TargetFramework>net${lib.versions.majorMinor (lib.defaultTo pkg.dotnet-sdk dotnet-sdk).version}</TargetFramework>
                        <EnableDynamicLoading>true</EnableDynamicLoading>
                      </PropertyGroup>
                    </Project>
                    EOF

                    configureNuget

                    dotnet new sln -n UnnamedProject
                    message=$(dotnet sln add UnnamedProject.csproj)
                    echo "$message"
                    # dotnet sln doesn't return an error when it fails to add the project
                    [[ $message == "Project \`UnnamedProject.csproj\` added to the solution." ]]

                    rm nuget.config
                  ''
                );

            export-tests = lib.makeExtensible (final: {
              inherit (pkg) export-template;

              export = stdenvNoCC.mkDerivation {
                name = "${final.export-template.name}-export";

                nativeBuildInputs = [ pkg ] ++ lib.optional (dotnet-sdk != null) dotnet-sdk;

                src = project-src;

                buildPhase = ''
                  runHook preBuild

                  export HOME=$(mktemp -d)
                  mkdir -p $HOME/.local/share/godot/
                  ln -s "${final.export-template}"/share/godot/export_templates "$HOME"/.local/share/godot/

                  godot${suffix} --headless --build-solutions -s create-scene.gd

                  runHook postBuild
                '';

                installPhase = ''
                  runHook preInstall

                  mkdir -p "$out"/bin
                  godot${suffix} --headless --export-release build "$out"/bin/test

                  runHook postInstall
                '';
              };

              run = runCommand "${final.export.name}-runs" { passthru = { inherit (final) export; }; } (
                ''
                  (
                    set -eo pipefail
                    HOME=$(mktemp -d)
                    "${final.export}"/bin/test --headless | tail -n+3 | (
                ''
                + lib.optionalString withMono ''
                  # indent
                      read output
                      if [[ "$output" != "Hello, Mono!" ]]; then
                        echo "unexpected output: $output" >&2
                        exit 1
                      fi
                ''
                + ''
                      read output
                      if [[ "$output" != "Hello, World!" ]]; then
                        echo "unexpected output: $output" >&2
                        exit 1
                      fi
                    )
                    touch "$out"
                  )
                ''
              );
            });

          in
    rec {
          {
            export-runs = export-tests.run;

            export-bin-runs =
              (export-tests.extend (
                final: prev: {
                  export-template = pkg.export-templates-bin;

                  export = prev.export.overrideAttrs (prev: {
                    nativeBuildInputs = prev.nativeBuildInputs or [ ] ++ [
                      autoPatchelfHook
                    ];

                    # stripping dlls results in:
                    # Failed to load System.Private.CoreLib.dll (error code 0x8007000B)
                    stripExclude = lib.optional withMono [ "*.dll" ];

                    runtimeDependencies =
                      prev.runtimeDependencies or [ ]
                      ++ map lib.getLib [
                        alsa-lib
                        libpulseaudio
                        libX11
                        libXcursor
                        libXext
                        libXi
                        libXrandr
                        udev
                        vulkan-loader
                      ];
                  });
                }
              )).run;
          }
        );

      attrs = finalAttrs: rec {
        pname = "godot${suffix}";
        inherit version;

@@ -128,7 +330,7 @@ let
        # https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-method-get-version-info
        BUILD_NAME = "nixpkgs";

      preConfigure = lib.optionalString withMono ''
        preConfigure = lib.optionalString (editor && withMono) ''
          # TODO: avoid pulling in dependencies of windows-only project
          dotnet sln modules/mono/editor/GodotTools/GodotTools.sln \
            remove modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj
@@ -214,7 +416,7 @@ let
        ];

        buildInputs =
        lib.optionals withMono dotnet-sdk.packages
          lib.optionals (editor && withMono) dotnet-sdk.packages
          ++ lib.optional withAlsa alsa-lib
          ++ lib.optional (withX11 || withWayland) libxkbcommon
          ++ lib.optionals withX11 [
@@ -252,9 +454,9 @@ let
            scons
          ]
          ++ lib.optionals withWayland [ wayland-scanner ]
        ++ lib.optionals withMono [
          dotnet-sdk
          ++ lib.optional (editor && withMono) [
            makeWrapper
            dotnet-sdk
          ];

        postBuild = lib.optionalString (editor && withMono) ''
@@ -283,10 +485,12 @@ let
                installManPage misc/dist/linux/godot.6

                mkdir -p "$out"/share/{applications,icons/hicolor/scalable/apps}
              cp misc/dist/linux/org.godotengine.Godot.desktop "$out/share/applications/org.godotengine.Godot${lib.versions.majorMinor version}${suffix}.desktop"
                cp misc/dist/linux/org.godotengine.Godot.desktop \
                  "$out/share/applications/org.godotengine.Godot${lib.versions.majorMinor version}${suffix}.desktop"

                substituteInPlace "$out/share/applications/org.godotengine.Godot${lib.versions.majorMinor version}${suffix}.desktop" \
                --replace "Exec=godot" "Exec=$out/bin/godot${suffix}" \
                --replace "Godot Engine" "Godot Engine ${
                  --replace-fail "Exec=godot" "Exec=$out/bin/godot${suffix}" \
                  --replace-fail "Godot Engine" "Godot Engine ${
                    lib.versions.majorMinor version + lib.optionalString withMono " (Mono)"
                  }"
                cp icon.svg "$out/share/icons/hicolor/scalable/apps/godot.svg"
@@ -296,13 +500,8 @@ let
                mkdir -p "$out"/share/nuget
                mv "$out"/libexec/GodotSharp/Tools/nupkgs "$out"/share/nuget/source

              wrapProgram $out/libexec/${binary} \
                --set DOTNET_ROOT ${dotnet-sdk}/share/dotnet \
                --prefix PATH : "${
                  lib.makeBinPath [
                    dotnet-sdk
                  ]
                }"
                wrapProgram "$out"/libexec/${binary} \
                  --prefix NUGET_FALLBACK_PACKAGES ';' "$out"/share/nuget/packages/
              ''
            else
              let
@@ -333,204 +532,12 @@ let
        passthru =
          {
            inherit updateScript;

            tests =
            {
              version = testers.testVersion {
                package = finalAttrs.finalPackage;
                version = dottedVersion;
              mkTests finalAttrs.finalPackage dotnet-sdk
              // lib.optionalAttrs (editor && withMono) {
                sdk-override = mkTests finalAttrs.finalPackage dotnet-sdk_alt;
              };
          }
            // lib.optionalAttrs (editor) (
              let
                pkg = finalAttrs.finalPackage;

                project-src = runCommand "${pkg.name}-project-src" { } (
                  ''
                    mkdir "$out"
                    cd "$out"
                    touch project.godot

                    cat >create-scene.gd <<'EOF'
                    extends SceneTree

                    func _initialize():
                      var node = Node.new()
                      var script = ResourceLoader.load("res://test.gd")
                      node.set_script(script)
                  ''
                  + lib.optionalString withMono ''
                    ${""}
                      var monoNode = Node.new()
                      var monoScript = ResourceLoader.load("res://Test.cs")
                      monoNode.set_script(monoScript)
                      node.add_child(monoNode)
                      monoNode.owner = node
                  ''
                  + ''
                      var scene = PackedScene.new()
                      var scenePath = "res://test.tscn"
                      scene.pack(node)
                      node.free()
                      var x = ResourceSaver.save(scene, scenePath)
                      ProjectSettings["application/run/main_scene"] = scenePath
                      ProjectSettings.save()
                      quit()
                    EOF

                    cat >test.gd <<'EOF'
                    extends Node
                    func _ready():
                      print("Hello, World!")
                      get_tree().quit()
                    EOF

                    cat >export_presets.cfg <<'EOF'
                    [preset.0]
                    name="build"
                    platform="Linux"
                    runnable=true
                    export_filter="all_resources"
                    include_filter=""
                    exclude_filter=""
                    [preset.0.options]
                    binary_format/architecture="${arch}"
                    EOF
                  ''
                  + lib.optionalString withMono ''
                    cat >Test.cs <<'EOF'
                    using Godot;
                    using System;

                    public partial class Test : Node
                    {
                      public override void _Ready()
                      {
                        GD.Print("Hello, Mono!");
                        GetTree().Quit();
                      }
                    }
                    EOF

                    sdk_version=$(basename ${pkg}/share/nuget/packages/godot.net.sdk/*)
                    cat >UnnamedProject.csproj <<EOF
                    <Project Sdk="Godot.NET.Sdk/$sdk_version">
                      <PropertyGroup>
                        <TargetFramework>net8.0</TargetFramework>
                        <EnableDynamicLoading>true</EnableDynamicLoading>
                      </PropertyGroup>
                    </Project>
                    EOF
                  ''
                );

                export-tests = lib.makeExtensible (final: {
                  inherit (pkg) export-template;

                  export = stdenvNoCC.mkDerivation {
                    name = "${final.export-template.name}-export";

                    nativeBuildInputs = [
                      pkg
                    ] ++ lib.optional withMono dotnet-sdk;

                    src = project-src;

                    postConfigure = lib.optionalString withMono ''
                      dotnet new sln -n UnnamedProject
                      message=$(dotnet sln add UnnamedProject.csproj)
                      echo "$message"
                      # dotnet sln doesn't return an error when it fails to add the project
                      [[ $message == "Project \`UnnamedProject.csproj\` added to the solution." ]]
                    '';

                    buildPhase = ''
                      runHook preBuild

                      export HOME=$(mktemp -d)
                      mkdir -p $HOME/.local/share/godot/
                      ln -s "${final.export-template}"/share/godot/export_templates "$HOME"/.local/share/godot/

                      godot${suffix} --headless --build-solutions -s create-scene.gd

                      runHook postBuild
                    '';

                    installPhase = ''
                      runHook preInstall

                      mkdir -p "$out"/bin
                      godot${suffix} --headless --export-release build "$out"/bin/test

                      runHook postInstall
                    '';
                  };

                  run = runCommand "${final.export.name}-runs" { passthru = { inherit (final) export; }; } (
                    ''
                      (
                        set -eo pipefail
                        HOME=$(mktemp -d)
                        "${final.export}"/bin/test --headless | tail -n+3 | (
                    ''
                    + lib.optionalString withMono ''
                      # indent
                          read output
                          if [[ "$output" != "Hello, Mono!" ]]; then
                            echo "unexpected output: $output" >&2
                            exit 1
                          fi
                    ''
                    + ''
                          read output
                          if [[ "$output" != "Hello, World!" ]]; then
                            echo "unexpected output: $output" >&2
                            exit 1
                          fi
                        )
                        touch "$out"
                      )
                    ''
                  );
                });

              in
              {
                export-runs = export-tests.run;

                export-bin-runs =
                  (export-tests.extend (
                    final: prev: {
                      export-template = pkg.export-templates-bin;

                      export = prev.export.overrideAttrs (prev: {
                        nativeBuildInputs = prev.nativeBuildInputs or [ ] ++ [
                          autoPatchelfHook
                        ];

                        # stripping dlls results in:
                        # Failed to load System.Private.CoreLib.dll (error code 0x8007000B)
                        stripExclude = lib.optional withMono [ "*.dll" ];

                        runtimeDependencies =
                          prev.runtimeDependencies or [ ]
                          ++ map lib.getLib [
                            alsa-lib
                            libpulseaudio
                            libX11
                            libXcursor
                            libXext
                            libXi
                            libXrandr
                            udev
                            vulkan-loader
                          ];
                      });
                    }
                  )).run;
              }
            );
        }
          // lib.optionalAttrs editor {
            export-template = mkTarget "template_release";
            export-templates-bin = (
@@ -564,13 +571,8 @@ let
        };
      };

  mkTarget =
    target:
    let
      attrs = attrsForTarget target;
    in
    stdenv.mkDerivation (
      if withMono then
      unwrapped = stdenv.mkDerivation (
        if (editor && withMono) then
          dotnetCorePackages.addNuGetDeps {
            inherit nugetDeps;
            overrideFetchAttrs = old: rec {
@@ -583,5 +585,63 @@ let
        else
          attrs
      );

      wrapper =
        if (editor && withMono) then
          stdenv.mkDerivation (finalAttrs: {
            __structuredAttrs = true;

            pname = finalAttrs.unwrapped.pname + "-wrapper";
            inherit (finalAttrs.unwrapped) version outputs meta;
            inherit unwrapped dotnet-sdk;

            dontUnpack = true;
            dontConfigure = true;
            dontBuild = true;

            nativeBuildInputs = [ makeWrapper ];
            strictDeps = true;

            installPhase = ''
              runHook preInstall

              mkdir -p "$out"/{bin,libexec,share/applications,nix-support}

              cp -d "$unwrapped"/bin/* "$out"/bin/
              ln -s "$unwrapped"/libexec/* "$out"/libexec/
              ln -s "$unwrapped"/share/nuget "$out"/share/
              cp "$unwrapped/share/applications/org.godotengine.Godot${lib.versions.majorMinor version}${suffix}.desktop" \
                "$out/share/applications/org.godotengine.Godot${lib.versions.majorMinor version}${suffix}.desktop"

              substituteInPlace "$out/share/applications/org.godotengine.Godot${lib.versions.majorMinor version}${suffix}.desktop" \
                --replace-fail "Exec=$unwrapped/bin/godot${suffix}" "Exec=$out/bin/godot${suffix}"
              ln -s "$unwrapped"/share/icons $out/share/

              # ensure dotnet hooks get run
              echo "${finalAttrs.dotnet-sdk}" >> "$out"/nix-support/propagated-build-inputs

              wrapProgram "$out"/libexec/${binary} \
                --prefix PATH : "${lib.makeBinPath [ finalAttrs.dotnet-sdk ]}"

              runHook postInstall
            '';

            postFixup = lib.concatMapStringsSep "\n" (output: ''
              [[ -e "''$${output}" ]] || ln -s "${unwrapped.${output}}" "''$${output}"
            '') finalAttrs.unwrapped.outputs;

            passthru = unwrapped.passthru // {
              tests = mkTests finalAttrs.finalPackage null // {
                unwrapped = lib.recurseIntoAttrs unwrapped.tests;
                sdk-override = lib.recurseIntoAttrs (
                  mkTests (finalAttrs.finalPackage.overrideAttrs { dotnet-sdk = dotnet-sdk_alt; }) null
                );
              };
            };
          })
        else
          unwrapped;
    in
    wrapper;
in
mkTarget "editor"