Installing Godot Using MSBuild

Posted On: 2025-06-02

By Mark

The primary workflows for Godot center around its editor: opening the editor launches a project-selection window, assets are arranged and modified using editor features, and there are buttons in the editor to both build and run the project. When working with GDScript, even writing code is performed in the editor.

When using C# in Godot, however, one spends a great deal of time outside the editor. C# coding occurs in an external IDE, and (in large/established projects) it's entirely possible for a developer to contribute significantly to a project without ever opening the editor directly. Unfortunately, all the tooling required to make that possible (ie. launch scripts, debugger settings, etc.) require that the Godot editor reside in a specific location (or be configured to be globally available) and thus every developer must manually download the correct version of Godot for their specific project and put it in the correct place.

While that is by no means a tall order, it is still an obstacle - especially when it involves a custom build of Godot. Fortunately, there are ways to automate this initial setup. Today's post will explore one way to do so: by leveraging the MSBuild features covered in the previous post.

High-Level Design

In order to get the Godot editor to the correct location, there are three main steps:

  1. Define the parameters (version number, location, etc.)
  2. Remove any incorrect versions from the well-known location
  3. Download and install the correct version into the well-known location

These can be done manually, of course, but they can also be neatly automated using MSBuild. To simplify this post, all these steps will be in a single .targets file, but it's certainly possible to organize it differently, if desired.

Defining Parameters

MSBuild provides properties that can be used to define things (numbers, strings, paths, etc.) that will be later used by the build process. This is a natural place to store parameters, either all together in one file, or by breaking them out into separate files (ie. keeping variables in one file and static things in another.)

While there are many ways to organize the properties, most any approach will need to track the following:

As an example, to use version 4.4.1 of the official Godot engine, the script will use the following set of parameters:

   <PropertyGroup>
       <EditorVersion>4.4.1</EditorVersion>
       <ZipFileHash>2B137E69C9190CE653A5CA3A7FEE1F33DBE1FEBD7B63AC6BABD52968C6DDCEC3</ZipFileHash>
       
       <!-- Note: the paths specified should match the launchSettings.json and be excluded from source control -->
       <EditorDirectory>$(SolutionDir)\editor</EditorDirectory>
       <EditorExe>$(EditorDirectory)\Godot.exe</EditorExe>
       <OriginalEditorExe>$(EditorDirectory)\Godot_v$(EditorVersion)-stable_mono_win64.exe</OriginalEditorExe>
       <EditorVersionDetectionPath>$(EditorDirectory)\Godot_v$(EditorVersion)-stable_mono_win64_console.exe</EditorVersionDetectionPath>
       
       <ZipFileName>Godot_v$(EditorVersion)-stable_mono_win64.zip</ZipFileName>
       <SourceURL>https://github.com/godotengine/godot/releases/download/$(EditorVersion)-stable/$(ZipFileName)</SourceURL>
       <DownloadZipPath>$(EditorDirectory)\.zip</DownloadZipPath>
       <ZipFile>$(DownloadZipPath)\$(ZipFileName)</ZipFile>
       <ZipIntermediaryFolder>$(DownloadZipPath)\Godot_v$(EditorVersion)-stable_mono_win64</ZipIntermediaryFolder>
   </PropertyGroup>

Removing Incorrect Versions

When automatically downloading to a known location, things can become a bit messy if that location already contains an (incorrect) version of Godot. Since the script described here downloads a seperate editor for each project (rather than, say, downloading it into a common Programs folder), it's best to outright delete the incorrect version at the start of the build.

This can be done by creating a new target, with a Condition that only occurs when there is an incorrect version (the expected executable exists, but it's accompanied by the wrong version files.) Deleting files and folders via MSBuild is a bit quirky compared to more procedural approaches (ie. calling File.Delete ). Lists of files are abstracting using ItemGroups - which use a mix of paths and (optionally) wildcards to select any number of files. Those files can then be enumerated by the Delete task, which will remove any that it receives. For reasons I don't (yet) understand myself, when no files match, some kind of special item is passed to the task instead (rather than, say, skipping the task entirely.) | The Delete Task documentation specifically points out that mistakes in the arguments can delete important files (ie. wildcards on an empty path can wipe a whole drive) - so it's best to put safeguards in place to prevent calling the Delete in incorrect scenarios.

The example uses the Error task to accomplish this, which will stop exeuction when its condition is met. It uses this to catch some known bad situations, such trying to any of the files being in the root of the drive. When combined with thoughtfully chosen ItemGroup entries, it should behave consistently across a variety of situations and setups.

   <Target Name="RemoveOldGodot" AfterTargets="build" Condition="Exists($(EditorExe)) AND !Exists($(EditorVersionDetectionPath))">
       <!-- Trying to install on top of an old version may leave outdated dlls hanging around. This cleans those up first. -->
       <Message Text="Removing Old Godot Editor" Importance="high" />
       <ItemGroup>
           <OutdatedEditorFiles Include="$(EditorDirectory)\GodotSharp\**\*" />
           <OutdatedEditorFiles Include="$(EditorDirectory)\Godot_v*-stable_mono_win64_console.exe" />
           <OutdatedEditorFiles Include="$(EditorExe)" />
       </ItemGroup>
       <!-- A rogue delete can cause mayhem - interrupt the build if the state is questionable. -->
       <Message Text="Removing files '@(OutdatedEditorFiles)'" Importance="low" />
       <Error Condition="$(EditorDirectory) == ''" 
              Text="Using an empty EditorDirectory is not supported"/>
       <Error Condition="%(OutdatedEditorFiles.Directory) == ''" 
              Text="The editor cannot be stored in the root of the drive. Fix the configuration so that the editor is in its own folder." />
       <Delete Files="@(OutdatedEditorFiles)" />
   </Target>

Installing the Correct Version

Once the incorrect version is removed, it's time to install the correct version. This can be done using a new target that is set to run after the removal target (note that RunAfter only defines the order of targets - the install will run even if the removal was skipped.) Installation first requires using the DownloadFile task to get the zip, then (optionally) the VerifyFileHash to make sure the file is correct, followed by an UnZip task to extract the contents. Once unzipped, combining ItemGroups and the Move task (leveraging the %(RecursiveDir) metadata) can move the contents of the zip to the correct location. Finally, since the Godot editor includes the version number in its file name, the Move task can be used to rename it to something more predictable (which is easier and more reliable than manually updating every script whenever the version number changes.)

Once installed, the leftover zip file will remain. If that's unwanted then a Delete task (with appropriate precautions) can resolve that, but I like to keep it around just in case I need it (based on my own testing, the DownloadFile task will succeed - even without internet - if the requisite zip file is already in place.)

   <Target Name="InstallGodot" AfterTargets="RemoveOldGodot" Condition="!Exists($(EditorExe)) OR !Exists($(EditorVersionDetectionPath))">
       <Message Text="Restoring Godot Editor" Importance="high" />
       <MakeDir Directories="$(EditorDirectory)"/>
       <DownloadFile DestinationFolder="$(DownloadZipPath)" SourceUrl="$(SourceURL)" />
       
       <!-- Make sure the expected file was downloaded -->
       <VerifyFileHash File="$(ZipFile)" Hash="$(ZipFileHash)"></VerifyFileHash>
       
       <Unzip SourceFiles="$(ZipFile)" DestinationFolder="$(DownloadZipPath)"/>
       <ItemGroup>
           <!-- per https://stackoverflow.com/a/15720600 and documented https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-well-known-item-metadata?view=vs-2022
               When copying via the wildcard \**\*, it's necessary to leverage the %(RecursiveDir) to make sure it ends up in the right folder -->
           <ZipContents Include="$(ZipIntermediaryFolder)\**\*" />
       </ItemGroup>
       <Move SourceFiles="@(ZipContents)" DestinationFolder="$(EditorDirectory)\%(RecursiveDir)"/>
       
       <!-- Rename the editor so it can be used via automation scripts -->
       <Move SourceFiles="$(OriginalEditorExe)" DestinationFiles="$(EditorExe)"/>
       
       <!-- Note: For simplicity, the zip files will be retained (also speeds up restore if the exe is lost/corrupted.) -->
   </Target>

Wiring It All Up

Once the requisite properties and targets are in place, it's still necessary to tell the project to use them. Fortunately this is simple to do: use an Import (as described in the previous post) to include the file in the .csproj file. Once that's complete, you can build the project - and Godot will be downloaded to a well-known location, ready for use.

The workflow can be even further polished by setting up (IDE-specific) settings to automatically launch Godot when run, but such is beyond the scope of this (already quite long) post. If a post on that topic is of interest to you, please let me know.

Complete Code

The complete code for this post is included below, for simplicity:

<Project>
   <PropertyGroup>
       <EditorVersion>4.4.1</EditorVersion>
       <ZipFileHash>2B137E69C9190CE653A5CA3A7FEE1F33DBE1FEBD7B63AC6BABD52968C6DDCEC3</ZipFileHash>
       
       <!-- Note: the paths specified should match the launchSettings.json and be excluded from source control -->
       <EditorDirectory>$(SolutionDir)\editor</EditorDirectory>
       <EditorExe>$(EditorDirectory)\Godot.exe</EditorExe>
       <OriginalEditorExe>$(EditorDirectory)\Godot_v$(EditorVersion)-stable_mono_win64.exe</OriginalEditorExe>
       <EditorVersionDetectionPath>$(EditorDirectory)\Godot_v$(EditorVersion)-stable_mono_win64_console.exe</EditorVersionDetectionPath>
       
       <ZipFileName>Godot_v$(EditorVersion)-stable_mono_win64.zip</ZipFileName>
       <SourceURL>https://github.com/godotengine/godot/releases/download/$(EditorVersion)-stable/$(ZipFileName)</SourceURL>
       <DownloadZipPath>$(EditorDirectory)\.zip</DownloadZipPath>
       <ZipFile>$(DownloadZipPath)\$(ZipFileName)</ZipFile>
       <ZipIntermediaryFolder>$(DownloadZipPath)\Godot_v$(EditorVersion)-stable_mono_win64</ZipIntermediaryFolder>
   </PropertyGroup>
   <Target Name="RemoveOldGodot" AfterTargets="build" Condition="Exists($(EditorExe)) AND !Exists($(EditorVersionDetectionPath))">
       <!-- Trying to install on top of an old version may leave outdated dlls hanging around. This cleans those up first. -->
       <Message Text="Removing Old Godot Editor" Importance="high" />
       <ItemGroup>
           <OutdatedEditorFiles Include="$(EditorDirectory)\GodotSharp\**\*" />
           <OutdatedEditorFiles Include="$(EditorDirectory)\Godot_v*-stable_mono_win64_console.exe" />
           <OutdatedEditorFiles Include="$(EditorExe)" />
       </ItemGroup>
       <!-- A rogue delete can cause mayhem - interrupt the build if the state is questionable. -->
       <Message Text="Removing files '@(OutdatedEditorFiles)'" Importance="low" />
       <Error Condition="$(EditorDirectory) == ''" 
              Text="Using an empty EditorDirectory is not supported"/>
       <Error Condition="%(OutdatedEditorFiles.Directory) == ''" 
              Text="The editor cannot be stored in the root of the drive. Fix the configuration so that the editor is in its own folder." />
       <Delete Files="@(OutdatedEditorFiles)" />
   </Target>
   <Target Name="InstallGodot" AfterTargets="RemoveOldGodot" Condition="!Exists($(EditorExe)) OR !Exists($(EditorVersionDetectionPath))">
       <Message Text="Restoring Godot Editor" Importance="high" />
       <MakeDir Directories="$(EditorDirectory)"/>
       <DownloadFile DestinationFolder="$(DownloadZipPath)" SourceUrl="$(SourceURL)" />
       
       <!-- Make sure the expected file was downloaded -->
       <VerifyFileHash File="$(ZipFile)" Hash="$(ZipFileHash)"></VerifyFileHash>
       
       <Unzip SourceFiles="$(ZipFile)" DestinationFolder="$(DownloadZipPath)"/>
       <ItemGroup>
           <!-- per https://stackoverflow.com/a/15720600 and documented https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-well-known-item-metadata?view=vs-2022
               When copying via the wildcard \**\*, it's necessary to leverage the %(RecursiveDir) to make sure it ends up in the right folder -->
           <ZipContents Include="$(ZipIntermediaryFolder)\**\*" />
       </ItemGroup>
       <Move SourceFiles="@(ZipContents)" DestinationFolder="$(EditorDirectory)\%(RecursiveDir)"/>
       
       <!-- Rename the editor so it can be used via automation scripts -->
       <Move SourceFiles="$(OriginalEditorExe)" DestinationFiles="$(EditorExe)"/>
       
       <!-- Note: For simplicity, the zip files will be retained (also speeds up restore if the exe is lost/corrupted.) -->
   </Target>
</Project>