How to Conditionally Target WinExe
Tags: csharp, dotnet, gamedev
Published: 2019-11-28 22:35:59

When creating cross-platform games in .NET Core, you'll typically use the `dotnet new console` template for the project. When creating a console app, you end up with a project file like this:

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

</Project>
```

Running this application under Windows results in a console window showing up even if the bulk of the application runs inside a graphical window (created by SDL or some other middleware). The console window is fine for development but no good when it comes time to ship the application. Unlike macOS or Linux, applications in Windows have a flag embedded into the binary itself dictating whether it should spawn a console window.

Before .NET Core 3.0 was released, I could only find one real option to deal with this: the tool `editbin.exe`, which ships with Visual Studio (not VS Code).

```
editbin.exe /subsystem:windows yourapp.exe
```

And this works, but I hate that my build environment requires `editbin.exe` be installed and added to my `PATH`. It's just more stuff that can go wrong; I wanna keep things simple.

Thankfully, .NET Core 3.0 introduced a new way to solve this problem: simply change your `OutputType` to `WinExe` in your `.csproj` file! I much prefer this solution because it brings me back to only needing the .NET Core SDK and nothing else. Complexity is the enemy, and I will slay it whenever I can.

However, permanently changing my `OutputType` complicates things: I _want_ that console window almost all the time during development. Not only does it let me see my console debug output, it gives me another handy way to close the application in case it hangs or something: I can just close the console window itself. So, ideally, I want the `OutputType` to be `Exe` _except_ when publishing my application.

So, initially, I created my script `publish.cmd` to contain this:

```
dotnet publish -c Release -r win-x64 -p:CopyOutputSymbolsToPublishDirectory=false -p:OutputType=WinExe
```

And this works... but not really. The problem is that the moment you introduce a dependency to your project, this path fails. Let's say you are building a shared library the same time you are building your desktop application.

```xml
<ItemGroup>
  <ProjectReference Include="..\Kelly.Shared\Kelly.Shared.csproj" />
</ItemGroup>
```

You will suddenly run into this error:

```
CSC : error CS5001: Program does not contain a static 'Main' method suitable for an entry point [C:\Users\kelly\Documents\Kelly.Shared\Kelly.Shared.csproj]

```

What is going on here? It turns out that when you specify `-p:OutputType=WinExe`, it happens *globally*. The property is propagated to _all_ projects being built. So, your shared library is no longer happy to just be a library; it wants to be an executable! And that just can't happen because, as the error points out, it has no `Main` method.

So, back to the drawing board. How can I clearly indicate that I want _only_ `Kelly.Client` to be `WinExe`? Well, I solved it by introducing a property into my project that no other project cares about.

```xml
<PropertyGroup>
  <KellyClientOutputType>Exe</KellyClientOutputType>
  <OutputType>$(KellyClientOutputType)</OutputType>
  <TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
```

Now my project looks to the property `KellyClientOutputType` to know what the output type is. This means I can alter my script to change this other property, and it will not affect any dependencies.

```
rmdir /s /q bin\Release
rmdir /s /q obj\Release
dotnet publish -c Release -r win-x64 -p:CopyOutputSymbolsToPublishDirectory=false -p:KellyClientOutputType=WinExe
```

Voila. Now I can just run this script whenever I wish to make a clean build that I want to share with outside parties.