DEV Community

Kelly Brown
Kelly Brown

Posted on

How to Conditionally Target WinExe

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:

<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.

<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.

<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.

Top comments (1)

Collapse
 
cartblanche profile image
Dominique Louis

Hi Kelly,
I'm in a similar situation trying to create a dotnet binary that will run on Windows, Linux and MacOS for my upcoming game. I was wondering if you'd tried tweaking MSBuild, maybe using a conditional, so that on/for Windows it uses WinExe (except for UWP apps) and for the other platforms it uses Exe?

D.