Skip to content

Commit

Permalink
Improved Nitrox build process (SubnauticaNitrox#1469)
Browse files Browse the repository at this point in the history
* Improved Nitrox build process

- Automatically searches for Subnautica installation on build and caches the result.
- Generates publicized assemblies from game files and references them automatically.

* Added default logger if no Log.Setup is done
  • Loading branch information
Measurity authored Oct 9, 2021
1 parent 9e6120e commit 3da23e8
Show file tree
Hide file tree
Showing 20 changed files with 487 additions and 145 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# Nitrox-specific files
DevVars.targets

# User-specific files
*.rsuser
*.suo
Expand Down
7 changes: 0 additions & 7 deletions DevVars.targets.example

This file was deleted.

30 changes: 26 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" InitialTargets="BeforeResolveReferences">
<!-- Set default properties for all projects (can be overridden per project) -->
<PropertyGroup>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<LangVersion>9</LangVersion>
<TestLibrary>false</TestLibrary>
<NitroxLibrary>false</NitroxLibrary>
<UnityModLibrary>false</UnityModLibrary>
<BuildToolDir>$(SolutionDir)Nitrox.BuildTool\bin\</BuildToolDir>
<BuildGenDir>$(BuildToolDir)generated_files\</BuildGenDir>
<BuildGenDllDir>$(BuildGenDir)publicized_assemblies\</BuildGenDllDir>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>TRACE;DEBUG</DefineConstants>
Expand All @@ -27,26 +34,41 @@
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch($(MSBuildProjectName), '^Nitrox.*$'))">
<NitroxLibrary>true</NitroxLibrary>
</PropertyGroup>
<PropertyGroup Condition="'$(NitroxLibrary)' == 'true' and '$(MSBuildProjectName)' != 'NitroxModel' and '$(MSBuildProjectName)' != 'NitroxServer' and '$(MSBuildProjectName)' != 'Nitrox.Bootloader'">
<PropertyGroup Condition="'$(NitroxLibrary)' == 'true' and '$(MSBuildProjectName)' != 'NitroxModel' and '$(MSBuildProjectName)' != 'NitroxServer' and '$(MSBuildProjectName)' != 'Nitrox.Bootloader' and '$(MSBuildProjectName)' != 'Nitrox.BuildTool'">
<UnityModLibrary>true</UnityModLibrary>
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'NitroxTest'">
<TestLibrary>true</TestLibrary>
</PropertyGroup>

<ItemGroup>
<ItemGroup Condition="'$(NitroxLibrary)' == 'true'">
<!-- Include common assembly info if project name starts with Nitrox -->
<Compile Condition="'$(NitroxLibrary)' == 'true'" Include="$(SolutionDir)AssemblyInfoCommon.cs">
<Compile Include="$(SolutionDir)AssemblyInfoCommon.cs">
<Link>Properties\AssemblyInfoCommon.cs</Link>
</Compile>
</ItemGroup>

<!-- Include default project references to all other "Nitrox*" projects -->
<Choose>
<When Condition="'$(UnityModLibrary)' == 'true'">
<ItemGroup>
<!-- Require other Nitrox projects (that need game DLLs) to wait on BuildTool. -->
<ProjectReference Include="$(SolutionDir)Nitrox.BuildTool\Nitrox.BuildTool.csproj">
<Project>{15C4C9C4-683C-4EF0-9E0F-0664A3BDA0CE}</Project>
<Name>Nitrox.BuildTool</Name>
</ProjectReference>
<ProjectReference Include="$(SolutionDir)NitroxModel\NitroxModel.csproj">
<Project>{B16F4DE7-21AD-4FEF-955B-0A5A365FA4E3}</Project>
<Name>NitroxModel</Name>
</ProjectReference>
</ItemGroup>
</When>
</Choose>

<!-- Tell developer that it needs to build the Nitrox.BuildTool to fetch the game assemblies. -->
<Target Name="PrepareForModding" BeforeTargets="BeforeResolveReferences" Condition="'$(UnityModLibrary)' == 'true' and !Exists('$(BuildGenDir)publicized_assemblies')">
<Error Text="Run the Nitrox.BuildTool project to fetch the assemblies, before building other Nitrox projects." />
</Target>
<!-- Include generated build properties. -->
<Import Project="$(BuildGenDir)game.props" Condition="Exists('$(BuildGenDir)game.props')"/>
</Project>
155 changes: 85 additions & 70 deletions Directory.Build.targets

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions Nitrox.BuildTool/Nitrox.BuildTool.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{15C4C9C4-683C-4EF0-9E0F-0664A3BDA0CE}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>BuildTool</RootNamespace>
<AssemblyName>Nitrox.BuildTool</AssemblyName>
<FileAlignment>512</FileAlignment>
<!-- BuildTool always builds for debugging. -->
<OutputPath>bin\</OutputPath>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Publicizer.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil">
<Version>0.11.3</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NitroxModel\NitroxModel.csproj" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
96 changes: 96 additions & 0 deletions Nitrox.BuildTool/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using NitroxModel.Discovery;

namespace BuildTool
{
/// <summary>
/// Entry point of the build automation project.
/// 1. Search for Subnautica install.
/// 2. Publicize the .NET dlls and persist for subsequent Nitrox builds.
/// </summary>
public static class Program
{
private static readonly Lazy<string> processDir =
new(() => Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location ?? Directory.GetCurrentDirectory()));

public static string ProcessDir => processDir.Value;

public static string GeneratedOutputDir => Path.Combine(ProcessDir, "generated_files");

public static async Task Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(eventArgs.ExceptionObject);
Console.ResetColor();

Exit((eventArgs.ExceptionObject as Exception)?.HResult ?? 1);
};

GameInstallData game = await Task.Factory.StartNew(EnsureGame).ConfigureAwait(false);
Console.WriteLine($"Found game at {game.InstallDir}");
await Task.Factory.StartNew(() => EnsurePublicizedAssemblies(game)).ConfigureAwait(false);

Exit();
}

private static void Exit(int exitCode = 0)
{
Console.WriteLine();
Console.WriteLine("Press any key to continue . . .");
Console.ReadKey(true);
Environment.Exit(exitCode);
}

private static GameInstallData EnsureGame()
{
static bool ValidateUnityGame(GameInstallData game, out string error)
{
if (!File.Exists(Path.Combine(game.InstallDir, "UnityPlayer.dll")))
{
error = $"Game at: '{game.InstallDir}' is not a Unity game";
return false;
}
if (!Directory.Exists(game.ManagedDllsDir))
{
error = $"Invalid Unity managed DLLs directory: {game.ManagedDllsDir}";
return false;
}

error = null;
return true;
}

string cacheFile = Path.Combine(GeneratedOutputDir, "game.props");
if (GameInstallData.TryFrom(cacheFile, out GameInstallData game) && !ValidateUnityGame(game, out string error))
{
throw new Exception(error);
}

game ??= new GameInstallData(GameInstallationFinder.Instance.FindGame());
game.TrySave(cacheFile);
return game;
}

private static void EnsurePublicizedAssemblies(GameInstallData game)
{
if (Directory.Exists(Path.Combine(GeneratedOutputDir, "publicized_assemblies")))
{
Console.WriteLine("Assemblies are already publicized.");
return;
}

string[] dllsToPublicize = Directory.GetFiles(game.ManagedDllsDir, "Assembly-*.dll");
foreach (string publicizedDll in Publicizer.Execute(dllsToPublicize,
"",
Path.Combine(GeneratedOutputDir, "publicized_assemblies")))
{
Console.WriteLine($"Publicized dll: {publicizedDll}");
}
}
}
}
124 changes: 124 additions & 0 deletions Nitrox.BuildTool/Publicizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Mono.Cecil;

namespace BuildTool
{
public static class Publicizer
{
public static IEnumerable<string> Execute(IEnumerable<string> files, string outputSuffix = "", string outputPath = null)
{
// Ensure target directory exists.
if (string.IsNullOrWhiteSpace(outputPath))
{
outputPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
}
if (!string.IsNullOrWhiteSpace(outputPath))
{
Directory.CreateDirectory(outputPath);
}

// Create dependency resolve for cecil (needed to write dlls that have other dependencies).
DefaultAssemblyResolver resolver = new();

foreach (string file in files)
{
if (!File.Exists(file))
{
throw new FileNotFoundException("Dll to publicize not found", file);
}
resolver.AddSearchDirectory(Path.GetDirectoryName(file));

string outputName = $"{Path.GetFileNameWithoutExtension(file)}{outputSuffix}{Path.GetExtension(file)}";
string outputFile = Path.Combine(outputPath, outputName);
Publicize(file, resolver).Write(outputFile);
yield return outputFile;
}
}

public static IEnumerable<FieldDefinition> FilterBackingEventFields(List<TypeDefinition> allTypes)
{
List<string> eventNames = allTypes.SelectMany(t => t.Events)
.Select(eventDefinition => eventDefinition.Name)
.ToList();

return allTypes.SelectMany(x => x.Fields)
.Where(fieldDefinition => !eventNames.Contains(fieldDefinition.Name));
}

/// <summary>
/// Method which returns all Types of the given module, including nested ones (recursively).
/// </summary>
/// <param name="moduleDefinition">.NET module to search through for types.</param>
/// <returns>Types found in module.</returns>
public static IEnumerable<TypeDefinition> GetAllTypes(ModuleDefinition moduleDefinition) => GetAllNestedTypes(moduleDefinition.Types);

private static AssemblyDefinition Publicize(string file, BaseAssemblyResolver dllResolver)
{
AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(file,
new ReaderParameters
{
AssemblyResolver = dllResolver
});
List<TypeDefinition> allTypes = GetAllTypes(assembly.MainModule).ToList();
foreach (TypeDefinition type in allTypes)
{
if (type == null)
{
continue;
}

// Publicize type and nested types.
if (!type.IsPublic || !type.IsNestedPublic)
{
if (type.IsNested)
{
type.IsNestedPublic = true;
}
else
{
type.IsPublic = true;
}
}
// Publicize methods on type.
foreach (MethodDefinition method in type.Methods)
{
if (!method?.IsPublic ?? false)
{
method.IsPublic = true;
}
}
}

// Publicize all fields (excludes fields if they would cause name conflicts on a type).
foreach (FieldDefinition field in FilterBackingEventFields(allTypes))
{
if (!field?.IsPublic ?? false)
{
field.IsPublic = true;
}
}

return assembly;
}

/// <summary>
/// Recursive method to get all nested types. Use <see cref="GetAllTypes(ModuleDefinition)" />
/// </summary>
/// <param name="typeDefinitions"></param>
/// <returns></returns>
private static IEnumerable<TypeDefinition> GetAllNestedTypes(IEnumerable<TypeDefinition> typeDefinitions)
{
IEnumerable<TypeDefinition> defs = typeDefinitions as TypeDefinition[] ?? typeDefinitions.ToArray();
if (!defs.Any())
{
return Array.Empty<TypeDefinition>();
}

return defs.Concat(GetAllNestedTypes(defs.SelectMany(t => t.NestedTypes)));
}
}
}
4 changes: 2 additions & 2 deletions Nitrox.Subnautica.Assets/Nitrox.Subnautica.Assets.shproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" InitialTargets="TestBla">
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>{0763D551-B1A8-4960-B88A-98833F957936}</ProjectGuid>
</PropertyGroup>
Expand All @@ -12,4 +12,4 @@
<Content Include="$(MSBuildThisFileDirectory)**" Exclude="**\*.projitems" CopyToOutputDirectory="Always" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
</Project>
7 changes: 6 additions & 1 deletion Nitrox.sln
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
SharedConfig.targets = SharedConfig.targets
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NitroxModel-Subnautica", "NitroxModel-Subnautica\NitroxModel-Subnautica.csproj", "{0A377218-6B36-4522-89A3-A39CFC999209}"
Expand All @@ -33,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitrox.Bootloader", "Nitrox
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Nitrox.Subnautica.Assets", "Nitrox.Subnautica.Assets\Nitrox.Subnautica.Assets.shproj", "{0763D551-B1A8-4960-B88A-98833F957936}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitrox.BuildTool", "Nitrox.BuildTool\Nitrox.BuildTool.csproj", "{15C4C9C4-683C-4EF0-9E0F-0664A3BDA0CE}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
Nitrox.Subnautica.Assets\Nitrox.Subnautica.Assets.projitems*{0763d551-b1a8-4960-b88a-98833f957936}*SharedItemsImports = 13
Expand Down Expand Up @@ -82,6 +83,10 @@ Global
{E4226522-9189-410B-93B2-792942FBD588}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4226522-9189-410B-93B2-792942FBD588}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4226522-9189-410B-93B2-792942FBD588}.Release|Any CPU.Build.0 = Release|Any CPU
{15C4C9C4-683C-4EF0-9E0F-0664A3BDA0CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15C4C9C4-683C-4EF0-9E0F-0664A3BDA0CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15C4C9C4-683C-4EF0-9E0F-0664A3BDA0CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15C4C9C4-683C-4EF0-9E0F-0664A3BDA0CE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading

0 comments on commit 3da23e8

Please # to comment.