Dotnet MAUI workload

.NET has the concept of workloads, which you can install alongside the SDK using dotnet workload install. This doesn’t work in Nix-land, because the SDK is not writeable.

There’s a very simple change one could make to install a workload such as the MAUI workload: put the following in build-dotnet.nix.

  postInstall = ''
    if [ ! -w "$HOME" ]; then
      export HOME=$(mktemp -d) # Dotnet expects a writable home directory for its configuration files
    fi
    $out/bin/dotnet workload install maui
  '';

But this is very impure. What happens behind the scenes is a whole bunch of NuGet downloads (at arbitrary versions, all specified in a WorkloadManifest.json file which is itself contained within a NuGet package), installing into ${dotnet}/packs.

It’s going to be a massive pain to do this in a version-locked way, I think, because dotnet workload install simply doesn’t care about reproducibility. I can just about create a list of [some of the] required packs and their hashes - e.g. I produced this text file with this shell script - but at this point I’m basically rewriting dotnet workload install in Bash, and I know I haven’t found every edge case yet (since for example the list I generated is not as big as the list that dotnet workload install actually ends up installing).

I guess what I’ll end up doing is making an overlay which adds the above impure post-install script and then move on with my life, but can anyone think of an easier/principled way to do this, e.g. that will allow it to go into nixpkgs?

I did proof of concept a few months that allowed you to declaratively install workloads. It lost momentum because there is a ton of JSON you have to parse to find all the dependencies, and it lost momentum. I can post a gist of the attempt if someone is interested in it. The hard part is taking the manifests and turning them into nix expressions. You also can’t use the current capability to compose .NET environments because it will try to resolve them relative to the .NET installation path and not the symlinked environment that was created.

As a workaround, if the .NET packages in nix added a file named userlocal at at /metadata/workloads/<sdkfeatureband>/userlocal, all workload installs would go to your home folder. See this PR for the change that added support for local installs.

1 Like

Here is the gist. It’s a flake.nix and pretty hacky. I had wanted to try out Xamarin.Mac for .NET 6. The part it doesn’t do, and the part that made me give up on it because of the scope was parsing the manifests.

What you have to do is run dotnet workload update --print-download-link-only --sdk-version <manifest version> to get the list of workload manifests for your .NET SDK. You then have to download the NuGet packages, extract their manifests, and do that recursively to find all of the dependencies. None of this is impossible to do, but I gave up because trying to parse JSON in bash sucks (even with jq), and I just wanted to learn Mac/iOS development (I ended up going with SwiftUI).

1 Like

WIP is Proof of concept by Smaug123 · Pull Request #1 · Smaug123/nix-maui · GitHub - thanks very much for your gist, that has been extremely helpful!

To my extreme surprise, this is now in a (very tenuously) working state: I successfully built a MAUI example for every OS except Android. Android was always going to be a non-starter because nixpkgs lacks an Android SDK on darwin-aarch64.

The next steps are to productionise and document that PR.

hi @Patrick thanks for your effort! do you think it would be possible to build MAUI Android on NixOS with your PR? Looking to shift towards NixOS on my new workstation, but part of my workflow is tied to MAUI at the moment, so looking for advice before I dive in.

Yeah, this should Just Work, although there are a number of extremely rough edges (not to mention what seems to be an actual bug in composition of workloads). The only reason I can’t build Android is because I can’t get the SDK on darwin-aarch64.

@reckenrode Do you mind me MIT-licensing my attempt? It’s a derivative work of your gist.

@reckenrode Do you mind me MIT-licensing my attempt? It’s a derivative work of your gist.

Go right ahead; that’s perfectly fine. I updated the gist with a SPDX line to indicate that it’s MIT-licensed.

1 Like

I’ve merged Proof of concept (#1) · Smaug123/nix-maui@e79c93f · GitHub into main; it’s still no more than a pile of gruesome hacks, but at least it’s now got the bare minimum documentation you might need to try it yourself.

1 Like

I have created a code snippet for this workaround and a few other tweaks:

{ config, lib, pkgs, ... }:

{
  environment = let 
    dotnet-combined = (with pkgs.dotnetCorePackages; combinePackages [
      sdk_8_0
      sdk_7_0
    ]).overrideAttrs (finalAttrs: previousAttrs: {
      # This is needed to install workload in $HOME
      # https://discourse.nixos.org/t/dotnet-maui-workload/20370/2

      postBuild = (previousAttrs.postBuild or '''') + ''

        for i in $out/sdk/*
        do
          i=$(basename $i)
          mkdir -p $out/metadata/workloads/''${i/-*}
          touch $out/metadata/workloads/''${i/-*}/userlocal
        done
      '';
    });
  in
  {
    sessionVariables = {
      DOTNET_ROOT = "${dotnet-combined}";
    };
    systemPackages = {
      dotnet-combined
    };
  };
}

For some reason I cannot post this in NixOS Wiki directly, so I’ll try posting it in the forum and link it there.

1 Like

Hi, @MakiseKurisu, @Patrick

I am a very very NixOS Noob, and trying to get dotnet workloads to work.
This seems to be the correct place to be looking.

If it is not a diffcult explanation, could you tell me how to include this snipit (either the postInstall or dotnet-combined) into the configuration.nix

You don’t have to include it system wide, you can use shell.nix by modifying it as such

{ pkgs ? import <nixpkgs> { } }:
with pkgs;
let 
    dotnet-combined = (with dotnetCorePackages; combinePackages [
      sdk_8_0
      sdk_7_0
    ]).overrideAttrs (finalAttrs: previousAttrs: {
      # This is needed to install workload in $HOME
      # https://discourse.nixos.org/t/dotnet-maui-workload/20370/2

      postBuild = (previousAttrs.postBuild or '''') + ''

        for i in $out/sdk/*
        do
          i=$(basename $i)
          mkdir -p $out/metadata/workloads/''${i/-*}
          touch $out/metadata/workloads/''${i/-*}/userlocal
        done
      '';
    });
in mkShell {
  packages = [
      dotnet-combined
  ];
  DOTNET_ROOT = "${dotnet-combined}";
}

EDIT: it doesn’t work for me though, opened issue upstream [BUG] 'To build this project, the following workloads must be installed: maui-android', but the workload is already installed · Issue #37706 · dotnet/sdk · GitHub

According to Support installing workloads in user folder by tmds · Pull Request #18823 · dotnet/sdk · GitHub
The last two digits need to be 00 regardless, which this script you’ve added doesn’t achieve, and honestly I’m not sure how to achieve that.

Edit: Actually I just figured it out. replace the postbuild bit above with

    postBuild = (previousAttrs.postBuild or '''') + ''
         for i in $out/sdk/*
         do
           i=$(basename $i)
           length=$(printf "%s" "$i" | wc -c)
           substring=$(printf "%s" "$i" | cut -c 1-$(expr $length - 2))
           i="$substring""00"
           mkdir -p $out/metadata/workloads/''${i/-*}
           touch $out/metadata/workloads/''${i/-*}/userlocal
        done
      '';
4 Likes

It worked flawlessly :smile:

for me it still fails with the same error

Are you using nix shell or installing dotnet globally?

Globally, in configuration.nix, but it also worked for me in a flake.nix(To use it with nix develop)

Problem still exists - i used

environment =
    let
      dotnet-combined = (with pkgs.dotnetCorePackages; combinePackages [
        # First SDK = Used in main system
        sdk_8_0
        sdk_7_0
      ]).overrideAttrs (finalAttrs: previousAttrs: {
        # This is needed to install workload in $HOME
        # https://discourse.nixos.org/t/dotnet-maui-workload/20370/2

        postBuild = (previousAttrs.postBuild or '''') + ''
           for i in $out/sdk/*
           do
             i=$(basename $i)
             length=$(printf "%s" "$i" | wc -c)
             substring=$(printf "%s" "$i" | cut -c 1-$(expr $length - 2))
             i="$substring""00"
             mkdir -p $out/metadata/workloads/''${i/-*}
             touch $out/metadata/workloads/''${i/-*}/userlocal
          done
        '';

      });


    in
    {
      sessionVariables = {
        DOTNET_ROOT = "${dotnet-combined}";
      };

      systemPackages =
        [
          dotnet-combined
        ];
    };

and it work’s, i have a “userlocal” file in my /metadata/workloads// folders, but any dotnet workload update run still results in

Workload installation failed: Read-only file system : '/nix/store/g2mws5h1b3n08mfcxplfa98mf0rgr68p-dotnet-core-combined/metadata/workloads/8.0.300/InstallState'

:pensive:

With this configuration I am also getting the error mentioned above.

Workload installation failed: Read-only file system : '/nix/store/d525zjkpfr6v6f2m1djs6gh8f8zscpvk-dotnet-core-combined/metadata/workloads/8.0.300/InstallState'

With the following shell.nix

{pkgs ? import <nixpkgs> { config.allowUnfree = true; } }:
let
dotnet-combined = (with pkgs.dotnetCorePackages; combinePackages [
    sdk_8_0
    sdk_7_0
  ]).overrideAttrs (finalAttrs: previousAttrs: {
    # This is needed to install workload in $HOME
    # https://discourse.nixos.org/t/dotnet-maui-workload/20370/2
    postBuild = (previousAttrs.postBuild or '''') + ''
       for i in $out/sdk/*
       do
         i=$(basename $i)
         length=$(printf "%s" "$i" | wc -c)
         substring=$(printf "%s" "$i" | cut -c 1-$(expr $length - 2))
         i="$substring""00"
         mkdir -p $out/metadata/workloads/''${i/-*}
         touch $out/metadata/workloads/''${i/-*}/userlocal
      done
    '';
  });
in
pkgs.mkShell {
  name = "dotnet-env";
  packages = [
    pkgs.jetbrains.rider
    dotnet-combined
  ];
  DOTNET_ROOT = "${dotnet-combined}";
}
1 Like
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
    nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = {
    self,
    nixpkgs,
    nixpkgs-unstable,
    flake-utils,
  }:
    flake-utils.lib.eachDefaultSystem (
      system: let
        pkgs = nixpkgs.legacyPackages.${system};
        dotnet-combined =
          (with pkgs.dotnetCorePackages;
            combinePackages [
              sdk_8_0
            ])
          .overrideAttrs (finalAttrs: previousAttrs: {
            # This is needed to install workload in $HOME
            # https://discourse.nixos.org/t/dotnet-maui-workload/20370/2

            postBuild =
              (previousAttrs.postBuild or '''')
              + ''
                for i in $out/sdk/*
                do
                  i=$(basename $i)
                  length=$(printf "%s" "$i" | wc -c)
                  substring=$(printf "%s" "$i" | cut -c 1-$(expr $length - 2))
                  i="$substring""00"
                  mkdir -p $out/metadata/workloads/''${i/-*}
                  touch $out/metadata/workloads/''${i/-*}/userlocal
                done
              '';
          });
      in {
        devShell = pkgs.mkShell {
          nativeBuildInputs = [
            dotnet-combined
            pkgs.reveal-md
            pkgs.nodePackages.mermaid-cli
            pkgs.nodejs_21
            pkgs.mono
          ];
          buildInputs = [];
          shellHook = ''
            export DOTNET_ROOT=${dotnet-combined}
          '';
        };
      }
    );
}

when using this flake, i can run: dotnet workload install aspire, and it install in my home folder under .dotnet. However when i try to run: dotnet build i get error:

  Determining projects to restore...
/nix/store/kwihhgnc34rwx14qrmh281spzlbh8gs9-dotnet-sdk-8.0.101/sdk/8.0.101/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.ImportWorkloads.targets(38,5): error NETSDK1147: To build this project, the following workloads must be installed: aspire [/home/merrinx/Projects/workspace/Hnikt.EpjDocs/src/apps/Hnikt.EpjDocs.Aspire.AppHost/Hnikt.EpjDocs.Aspire.AppHost.csproj]
/nix/store/kwihhgnc34rwx14qrmh281spzlbh8gs9-dotnet-sdk-8.0.101/sdk/8.0.101/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.ImportWorkloads.targets(38,5): error NETSDK1147: To install these workloads, run the following command: dotnet workload restore [/home/merrinx/Projects/workspace/Hnikt.EpjDocs/src/apps/Hnikt.EpjDocs.Aspire.AppHost/Hnikt.EpjDocs.Aspire.AppHost.csproj]

Build FAILED.

It seem to look for the workload in nix store, i do not know how to move past this.
Anyone got build with workload working?

1 Like