dotnet publish is more than the sum of its parts

Posted by Programming Is Moe on Sunday, November 1, 2020

TOC

I always assumed that dotnet publish is nothing more than a combination of dotnet restore, dotnet build and copy. Well as all assumptions I have in my life I’d say I’m wrong and I don’t even know why.

Build servers are my friends

By now I’ve authored at least 10 CI/CD pipelines for different net core projects and debugged pipelines of colleagues a lot. And I really expected that nothing could go wrong for the most basic of the basic pipelines: Do a windows build of my GUI program.

> dotnet restore --runtime win10-x64
> dotnet build {SubProject} --configuration Release --runtime win10-x64 --no-restore
> dotnet publish {SubProject} --configuration Release --runtime win10-x64 --no-restore --no-build --no-self-contained

What do I get? An exe that complains about: Nothing. It just doesn’t start.

So I drop the --no-self-contained: and it works. But the output 88mb instead of 10mb. Which is not acceptable. Now instead of chaining the commands I use just the publish command and drop the --no-restore and --no-build

> dotnet publish {SubProject} --configuration Release --runtime win10-x64 --no-self-contained

AND IT WORKS. So either black magic or dotnet publish without those flags does much more behind the scene than I think it does

Maybe my project is at fault

So let’s try a minimal reproduction:

dotnet version 3.1.403

WhyWonYouWork.csproj

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.1</TargetFramework>
    </PropertyGroup>
</Project>

Program.cs

using System;

namespace WhyWontYouWork
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

You cannot go more basic than this.

> dotnet restore --runtime win10-x64
> dotnet build WhyWontYouWork --configuration Release --runtime win10-x64 --no-restore
> dotnet publish WhyWontYouWork --configuration Release --runtime win10-x64 --no-restore --no-build --no-self-contained
> WhyWontYouWork\bin\Release\netcoreapp3.1\win10-x64\publish\WhyWontYouWork.exe

A fatal error was encountered. The library ‘hostpolicy.dll’ required to execute the application was not found in ‘C:\Program Files\dotnet’.

??????????????????

The files in the folder are

 31718  Nov  1 23:22 WhyWontYouWork.deps.json
  4096  Nov  1 23:22 WhyWontYouWork.dll*
174592  Nov  1 23:22 WhyWontYouWork.exe*
  9356  Nov  1 23:22 WhyWontYouWork.pdb
   185  Nov  1 23:22 WhyWontYouWork.runtimeconfig.json

So let’s try this:

> dotnet publish WhyWontYouWork --configuration Release --runtime win10-x64 --no-self-contained
> WhyWontYouWork\bin\Release\netcoreapp3.1\win10-x64\publish\WhyWontYouWork.exe

Hello World!

The files in the folder of the working build are

   491 Nov  1 23:32 WhyWontYouWork.deps.json         
  4096 Nov  1 23:32 WhyWontYouWork.dll*              
174592 Nov  1 23:32 WhyWontYouWork.exe*              
  9356 Nov  1 23:32 WhyWontYouWork.pdb               
   154 Nov  1 23:32 WhyWontYouWork.runtimeconfig.json

The runtimeconfig.json of the failed build looks like this:

{
    "runtimeOptions": {
        "tfm": "netcoreapp3.1",
        "includedFrameworks": [
            {
                "name": "Microsoft.NETCore.App",
                "version": "3.1.9"
            }
        ]
    }
}

vs from the working one:

{
    "runtimeOptions": {
        "tfm": "netcoreapp3.1",
        "framework": {
            "name": "Microsoft.NETCore.App",
            "version": "3.1.0"
        }
    }
}

And the failed build deps:

{
    "runtimeTarget": {
      "name": ".NETCoreApp,Version=v3.1/win10-x64",
      "signature": ""
    },
    "compilationOptions": {},
    "targets": {
      ".NETCoreApp,Version=v3.1": {},
      ".NETCoreApp,Version=v3.1/win10-x64": {
        "WhyWontYouWork/1.0.0": {
          "dependencies": {
            "runtimepack.Microsoft.NETCore.App.Runtime.win-x64": "3.1.9"
          },
          "runtime": {
            "WhyWontYouWork.dll": {}
          }
        },
        "runtimepack.Microsoft.NETCore.App.Runtime.win-x64/3.1.9": {
          "runtime": {
            "Microsoft.CSharp.dll": {
              "assemblyVersion": "4.0.5.0",
              "fileVersion": "4.700.20.47203"
            },
// + 800 lines more dependencies of "runtimepack.Microsoft.NETCore.App.Runtime.win-x64/3.1.9"

Meanwhile the working builds WHOLE deps

{
    "runtimeTarget": {
      "name": ".NETCoreApp,Version=v3.1/win10-x64",
      "signature": ""
    },
    "compilationOptions": {},
    "targets": {
      ".NETCoreApp,Version=v3.1": {},
      ".NETCoreApp,Version=v3.1/win10-x64": {
        "WhyWontYouWork/1.0.0": {
          "runtime": {
            "WhyWontYouWork.dll": {}
          }
        }
      }
    },
    "libraries": {
      "WhyWontYouWork/1.0.0": {
        "type": "project",
        "serviceable": false,
        "sha512": ""
      }
    }
}

Also that missing ‘hostpolicy.dll’ is listed in the long list of dependencies. So WHAT exactly is the one-liner dotnet publish passing to the restore and build that makes the difference ? I tried reading through the dotnet cli code to see if something jumps out but I just don’t see anything that is suspicious.

After googling around for a while I came across this issue: https://github.com/dotnet/runtime/issues/3569 which doesn’t answer my main question at all: WHY THE FUCK ARE THOSE TWO APPROACHES NOT THE SAME.