* This resolves some issues where if you are building the actual C# dlls you also need them to be x64 based if you are targeting an x64 based target. This is a little complicated here because we set all this up ahead of time assuming that all the targets are compatible but in this case they are not so, just following what other places in the code are doing around this specifically dotnet AOT.
418 lines
21 KiB
C#
418 lines
21 KiB
C#
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Flax.Build.Graph;
|
|
using Flax.Build.NativeCpp;
|
|
using Flax.Deploy;
|
|
|
|
namespace Flax.Build
|
|
{
|
|
static partial class Builder
|
|
{
|
|
public static event Action<TaskGraph, BuildData, BuildOptions, Task, IGrouping<string, Module>> BuildDotNetAssembly;
|
|
|
|
private static void BuildTargetDotNet(RulesAssembly rules, TaskGraph graph, Target target, Platform platform, TargetConfiguration configuration)
|
|
{
|
|
// Check if use custom project file
|
|
if (!string.IsNullOrEmpty(target.CustomExternalProjectFilePath))
|
|
{
|
|
// Use msbuild to compile it
|
|
var task = graph.Add<Task>();
|
|
task.WorkingDirectory = Globals.Root;
|
|
task.InfoMessage = "Building " + Path.GetFileName(target.CustomExternalProjectFilePath);
|
|
task.Cost = 100;
|
|
task.DisableCache = true;
|
|
task.CommandPath = VCEnvironment.MSBuildPath;
|
|
task.CommandArguments = $"\"{target.CustomExternalProjectFilePath}\" /m /p:BuildProjectReferences=false /t:Restore,Build /p:Configuration=\"{configuration}\" /p:RestorePackagesConfig=True /p:Platform=AnyCPU /nologo {VCEnvironment.Verbosity}";
|
|
if (task.CommandPath.EndsWith(" msbuild"))
|
|
{
|
|
// Special case when using dotnet CLI as msbuild
|
|
task.CommandPath = task.CommandPath.Substring(0, task.CommandPath.Length - 8);
|
|
task.CommandArguments = "msbuild " + task.CommandArguments;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Warn if target has no valid modules
|
|
if (target.Modules.Count == 0)
|
|
Log.Warning(string.Format("Target {0} has no modules to build", target.Name));
|
|
|
|
// Pick a project
|
|
var project = Globals.Project;
|
|
if (target is ProjectTarget projectTarget)
|
|
project = projectTarget.Project;
|
|
if (project == null)
|
|
throw new Exception($"Cannot build target {target.Name}. The project file is missing (.flaxproj located in the folder above).");
|
|
|
|
// Setup build environment for the target
|
|
var targetBuildOptions = GetBuildOptions(target, platform, null, TargetArchitecture.AnyCPU, configuration, project.ProjectFolderPath);
|
|
using (new ProfileEventScope("PreBuild"))
|
|
{
|
|
// Pre build
|
|
target.PreBuild(graph, targetBuildOptions);
|
|
PreBuild?.Invoke(graph, targetBuildOptions);
|
|
|
|
// Ensure that target build directories exist
|
|
if (!target.IsPreBuilt && !Directory.Exists(targetBuildOptions.IntermediateFolder))
|
|
Directory.CreateDirectory(targetBuildOptions.IntermediateFolder);
|
|
if (!target.IsPreBuilt && !Directory.Exists(targetBuildOptions.OutputFolder))
|
|
Directory.CreateDirectory(targetBuildOptions.OutputFolder);
|
|
}
|
|
|
|
// Setup building common data container
|
|
var buildData = new BuildData
|
|
{
|
|
Project = project,
|
|
Graph = graph,
|
|
Rules = rules,
|
|
Target = target,
|
|
TargetOptions = targetBuildOptions,
|
|
Platform = platform,
|
|
Architecture = TargetArchitecture.AnyCPU,
|
|
Configuration = configuration,
|
|
};
|
|
|
|
// Collect all modules
|
|
using (new ProfileEventScope("CollectModules"))
|
|
{
|
|
foreach (var moduleName in target.Modules)
|
|
{
|
|
var module = rules.GetModule(moduleName);
|
|
if (module != null)
|
|
{
|
|
CollectModules(buildData, module, true);
|
|
}
|
|
else
|
|
{
|
|
Log.Warning(string.Format("Missing module {0} (or invalid name specified)", moduleName));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build all modules from target binary modules but in order of collecting (from independent to more dependant ones)
|
|
var sourceFiles = new List<string>();
|
|
using (new ProfileEventScope("BuildModules"))
|
|
{
|
|
foreach (var module in buildData.ModulesOrderList)
|
|
{
|
|
if (buildData.BinaryModules.Any(x => x.Contains(module)))
|
|
{
|
|
var moduleOptions = BuildModule(buildData, module);
|
|
|
|
// Get source files
|
|
sourceFiles.AddRange(moduleOptions.SourceFiles.Where(x => x.EndsWith(".cs")));
|
|
|
|
// Merge module into target environment
|
|
buildData.TargetOptions.LinkEnv.InputFiles.AddRange(moduleOptions.OutputFiles);
|
|
buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles);
|
|
buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles);
|
|
buildData.TargetOptions.Libraries.AddRange(moduleOptions.Libraries);
|
|
buildData.TargetOptions.DelayLoadLibraries.AddRange(moduleOptions.DelayLoadLibraries);
|
|
buildData.TargetOptions.ScriptingAPI.Add(moduleOptions.ScriptingAPI);
|
|
buildData.TargetOptions.ExternalModules.AddRange(moduleOptions.ExternalModules);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build
|
|
var outputTargetFilePath = target.GetOutputFilePath(targetBuildOptions);
|
|
var outputPath = Path.GetDirectoryName(outputTargetFilePath);
|
|
using (new ProfileEventScope("Build"))
|
|
{
|
|
// Cleanup source files
|
|
sourceFiles.RemoveAll(x => x.EndsWith(BuildFilesPostfix));
|
|
sourceFiles.Sort();
|
|
|
|
// Build assembly
|
|
BuildDotNet(graph, buildData, targetBuildOptions, target.OutputName, sourceFiles);
|
|
}
|
|
|
|
// Deploy files
|
|
if (!target.IsPreBuilt)
|
|
{
|
|
using (new ProfileEventScope("DeployFiles"))
|
|
{
|
|
foreach (var srcFile in targetBuildOptions.OptionalDependencyFiles.Where(File.Exists).Union(targetBuildOptions.DependencyFiles))
|
|
{
|
|
var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
|
|
graph.AddCopyFile(dstFile, srcFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
using (new ProfileEventScope("PostBuild"))
|
|
{
|
|
// Post build
|
|
PostBuild?.Invoke(graph, targetBuildOptions);
|
|
target.PostBuild(graph, targetBuildOptions);
|
|
}
|
|
}
|
|
|
|
private static void BuildDotNet(TaskGraph graph, BuildData buildData, BuildOptions buildOptions, string name, List<string> sourceFiles, HashSet<string> fileReferences = null, IGrouping<string, Module> binaryModule = null)
|
|
{
|
|
// Setup build options
|
|
var buildPlatform = Platform.BuildTargetPlatform;
|
|
var outputPath = Path.GetDirectoryName(buildData.Target.GetOutputFilePath(buildOptions));
|
|
var outputFile = Path.Combine(outputPath, name + ".dll");
|
|
var outputDocFile = Path.Combine(outputPath, name + ".xml");
|
|
var outputGeneratedFiles = Path.Combine(buildOptions.IntermediateFolder);
|
|
string cscPath, referenceAssemblies;
|
|
#if USE_NETCORE
|
|
var dotnetSdk = DotNetSdk.Instance;
|
|
if (!dotnetSdk.IsValid)
|
|
throw new Exception("Cannot compile C# without .NET SDK");
|
|
string dotnetPath = "dotnet", referenceAnalyzers;
|
|
string[] runtimeVersionNameParts = dotnetSdk.RuntimeVersionName.Split('.');
|
|
string runtimeVersionShort = runtimeVersionNameParts[0] + '.' + runtimeVersionNameParts[1];
|
|
#else
|
|
string monoRoot, monoPath;
|
|
#endif
|
|
switch (buildPlatform)
|
|
{
|
|
case TargetPlatform.Windows:
|
|
{
|
|
#if USE_NETCORE
|
|
dotnetPath = Path.Combine(dotnetSdk.RootPath, "dotnet.exe");
|
|
cscPath = Path.Combine(dotnetSdk.RootPath, @$"sdk\{dotnetSdk.VersionName}\Roslyn\bincore\csc.dll");
|
|
referenceAssemblies = Path.Combine(dotnetSdk.RootPath, @$"packs\Microsoft.NETCore.App.Ref\{dotnetSdk.RuntimeVersionName}\ref\net{runtimeVersionShort}\");
|
|
referenceAnalyzers = Path.Combine(dotnetSdk.RootPath, @$"packs\Microsoft.NETCore.App.Ref\{dotnetSdk.RuntimeVersionName}\analyzers\dotnet\cs\");
|
|
#else
|
|
monoRoot = Path.Combine(Globals.EngineRoot, "Source", "Platforms", "Editor", "Windows", "Mono");
|
|
monoPath = Path.Combine(monoRoot, "bin", "mono.exe");
|
|
cscPath = Path.Combine(Path.GetDirectoryName(VCEnvironment.MSBuildPath), "Roslyn", "csc.exe");
|
|
if (!File.Exists(cscPath))
|
|
cscPath = Path.Combine(monoRoot, "lib", "mono", "4.5", "csc.exe");
|
|
referenceAssemblies = Path.Combine(monoRoot, "lib", "mono", "4.5-api");
|
|
#endif
|
|
break;
|
|
}
|
|
case TargetPlatform.Linux:
|
|
{
|
|
#if USE_NETCORE
|
|
cscPath = Path.Combine(dotnetSdk.RootPath, $"sdk/{dotnetSdk.VersionName}/Roslyn/bincore/csc.dll");
|
|
referenceAssemblies = Path.Combine(dotnetSdk.RootPath, $"packs/Microsoft.NETCore.App.Ref/{dotnetSdk.RuntimeVersionName}/ref/net{runtimeVersionShort}/");
|
|
referenceAnalyzers = Path.Combine(dotnetSdk.RootPath, $"packs/Microsoft.NETCore.App.Ref/{dotnetSdk.RuntimeVersionName}/analyzers/dotnet/cs/");
|
|
#else
|
|
monoRoot = Path.Combine(Globals.EngineRoot, "Source", "Platforms", "Editor", "Linux", "Mono");
|
|
monoPath = Path.Combine(monoRoot, "bin", "mono");
|
|
cscPath = Path.Combine(monoRoot, "lib", "mono", "4.5", "csc.exe");
|
|
referenceAssemblies = Path.Combine(monoRoot, "lib", "mono", "4.5-api");
|
|
#endif
|
|
break;
|
|
}
|
|
case TargetPlatform.Mac:
|
|
{
|
|
#if USE_NETCORE
|
|
dotnetPath = Path.Combine(dotnetSdk.RootPath, "dotnet");
|
|
|
|
cscPath = Path.Combine(dotnetSdk.RootPath, $"sdk/{dotnetSdk.VersionName}/Roslyn/bincore/csc.dll");
|
|
referenceAssemblies = Path.Combine(dotnetSdk.RootPath, $"packs/Microsoft.NETCore.App.Ref/{dotnetSdk.RuntimeVersionName}/ref/net{runtimeVersionShort}/");
|
|
referenceAnalyzers = Path.Combine(dotnetSdk.RootPath, $"packs/Microsoft.NETCore.App.Ref/{dotnetSdk.RuntimeVersionName}/analyzers/dotnet/cs/");
|
|
#else
|
|
monoRoot = Path.Combine(Globals.EngineRoot, "Source", "Platforms", "Editor", "Mac", "Mono");
|
|
monoPath = Path.Combine(monoRoot, "bin", "mono");
|
|
cscPath = Path.Combine(monoRoot, "lib", "mono", "4.5", "csc.exe");
|
|
referenceAssemblies = Path.Combine(monoRoot, "lib", "mono", "4.5-api");
|
|
#endif
|
|
break;
|
|
}
|
|
default: throw new InvalidPlatformException(buildPlatform);
|
|
}
|
|
|
|
if (fileReferences == null)
|
|
fileReferences = buildOptions.ScriptingAPI.FileReferences;
|
|
else
|
|
fileReferences.AddRange(buildOptions.ScriptingAPI.FileReferences);
|
|
|
|
// Setup C# compiler arguments
|
|
var args = new List<string>();
|
|
args.Clear();
|
|
args.Add("/nologo");
|
|
args.Add("/target:library");
|
|
args.Add("/platform:AnyCPU");
|
|
args.Add("/debug+");
|
|
args.Add("/debug:portable");
|
|
args.Add("/errorreport:prompt");
|
|
args.Add("/preferreduilang:en-US");
|
|
args.Add("/highentropyva+");
|
|
args.Add("/deterministic");
|
|
args.Add("/nostdlib+");
|
|
args.Add("/errorendlocation");
|
|
args.Add("/utf8output");
|
|
args.Add("/warn:4");
|
|
args.Add("/unsafe");
|
|
args.Add("/fullpaths");
|
|
args.Add("/filealign:512");
|
|
#if USE_NETCORE
|
|
args.Add("/langversion:11.0");
|
|
args.Add(string.Format("/nullable:{0}", buildOptions.ScriptingAPI.CSharpNullableReferences.ToString().ToLowerInvariant()));
|
|
if (buildOptions.ScriptingAPI.CSharpNullableReferences == CSharpNullableReferences.Disable)
|
|
args.Add("-nowarn:8632"); // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
|
|
#else
|
|
args.Add("/langversion:7.3");
|
|
#endif
|
|
if (buildOptions.ScriptingAPI.IgnoreMissingDocumentationWarnings)
|
|
args.Add("-nowarn:1591");
|
|
|
|
// Optimizations prevent debugging, only enable in release builds
|
|
var optimize = buildData.Configuration == TargetConfiguration.Release;
|
|
if (buildData.TargetOptions.ScriptingAPI.Optimization.HasValue)
|
|
optimize = buildData.TargetOptions.ScriptingAPI.Optimization.Value;
|
|
args.Add(optimize ? "/optimize+" : "/optimize-");
|
|
#if !USE_NETCORE
|
|
args.Add(string.Format("/reference:\"{0}mscorlib.dll\"", referenceAssemblies));
|
|
#endif
|
|
args.Add(string.Format("/out:\"{0}\"", outputFile));
|
|
args.Add(string.Format("/doc:\"{0}\"", outputDocFile));
|
|
#if USE_NETCORE
|
|
args.Add(string.Format("/generatedfilesout:\"{0}\"", outputGeneratedFiles));
|
|
#endif
|
|
if (buildOptions.ScriptingAPI.Defines.Count != 0)
|
|
args.Add("/define:" + string.Join(";", buildOptions.ScriptingAPI.Defines));
|
|
if (buildData.Configuration == TargetConfiguration.Debug)
|
|
args.Add("/define:DEBUG");
|
|
foreach (var reference in buildOptions.ScriptingAPI.SystemReferences)
|
|
args.Add(string.Format("/reference:\"{0}{1}.dll\"", referenceAssemblies, reference));
|
|
foreach (var reference in fileReferences)
|
|
args.Add(string.Format("/reference:\"{0}\"", reference));
|
|
#if USE_NETCORE
|
|
foreach (var systemAnalyzer in buildOptions.ScriptingAPI.SystemAnalyzers)
|
|
args.Add(string.Format("/analyzer:\"{0}{1}.dll\"", referenceAnalyzers, systemAnalyzer));
|
|
foreach (var analyzer in buildOptions.ScriptingAPI.Analyzers)
|
|
args.Add(string.Format("/analyzer:\"{0}\"", analyzer));
|
|
#endif
|
|
foreach (var sourceFile in sourceFiles)
|
|
args.Add("\"" + sourceFile + "\"");
|
|
|
|
#if USE_NETCORE
|
|
// Inject some assembly metadata (similar to msbuild in Visual Studio)
|
|
var assemblyAttributesPath = Path.Combine(buildOptions.IntermediateFolder, name + ".AssemblyAttributes.cs");
|
|
File.WriteAllText(assemblyAttributesPath, $"[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(\".NETCoreApp,Version=v{runtimeVersionShort}\", FrameworkDisplayName = \".NET {runtimeVersionShort}\")]\n", Encoding.UTF8);
|
|
args.Add("\"" + assemblyAttributesPath + "\"");
|
|
#endif
|
|
|
|
// Generate response file with source files paths and compilation arguments
|
|
string responseFile = Path.Combine(buildOptions.IntermediateFolder, name + ".response");
|
|
Utilities.WriteFileIfChanged(responseFile, string.Join(Environment.NewLine, args));
|
|
|
|
// Create C# compilation task
|
|
var task = graph.Add<Task>();
|
|
task.PrerequisiteFiles.Add(responseFile);
|
|
task.PrerequisiteFiles.AddRange(sourceFiles);
|
|
task.PrerequisiteFiles.AddRange(fileReferences);
|
|
task.ProducedFiles.Add(outputFile);
|
|
task.WorkingDirectory = buildData.TargetOptions.WorkingDirectory;
|
|
task.InfoMessage = "Compiling " + outputFile;
|
|
task.Cost = task.PrerequisiteFiles.Count;
|
|
|
|
// The "/shared" flag enables the compiler server support:
|
|
// https://github.com/dotnet/roslyn/blob/main/docs/compilers/Compiler%20Server.md
|
|
#if USE_NETCORE
|
|
task.CommandPath = dotnetPath;
|
|
task.CommandArguments = $"exec \"{cscPath}\" /noconfig /shared @\"{responseFile}\"";
|
|
#else
|
|
if (monoPath != null)
|
|
{
|
|
task.CommandPath = monoPath;
|
|
task.CommandArguments = $"\"{cscPath}\" /noconfig @\"{responseFile}\"";
|
|
}
|
|
else
|
|
{
|
|
task.CommandPath = cscPath;
|
|
task.CommandArguments = $"/noconfig /shared @\"{responseFile}\"";
|
|
}
|
|
#endif
|
|
|
|
BuildDotNetAssembly?.Invoke(graph, buildData, buildOptions, task, binaryModule);
|
|
|
|
// Copy referenced assemblies
|
|
foreach (var srcFile in buildOptions.ScriptingAPI.FileReferences)
|
|
{
|
|
var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
|
|
if (dstFile == srcFile || graph.HasCopyTask(dstFile, srcFile))
|
|
continue;
|
|
graph.AddCopyFile(dstFile, srcFile);
|
|
|
|
var srcPdb = Path.ChangeExtension(srcFile, "pdb");
|
|
if (File.Exists(srcPdb))
|
|
graph.AddCopyFile(Path.ChangeExtension(dstFile, "pdb"), srcPdb);
|
|
|
|
var srcXml = Path.ChangeExtension(srcFile, "xml");
|
|
if (File.Exists(srcXml))
|
|
graph.AddCopyFile(Path.ChangeExtension(dstFile, "xml"), srcXml);
|
|
}
|
|
}
|
|
|
|
private static void BuildTargetBindings(TaskGraph graph, BuildData buildData)
|
|
{
|
|
var sourceFiles = new List<string>();
|
|
var fileReferences = new HashSet<string>();
|
|
var buildOptions = buildData.TargetOptions;
|
|
var outputPath = Path.GetDirectoryName(buildData.Target.GetOutputFilePath(buildOptions));
|
|
foreach (var binaryModule in buildData.BinaryModules)
|
|
{
|
|
if (binaryModule.All(x => !x.BuildCSharp))
|
|
continue;
|
|
var binaryModuleName = binaryModule.Key;
|
|
using (new ProfileEventScope(binaryModuleName))
|
|
{
|
|
// TODO: add support for extending this code and support generating bindings projects for other scripting languages
|
|
var project = GetModuleProject(binaryModule.First(), buildData);
|
|
|
|
// Get source files
|
|
sourceFiles.Clear();
|
|
foreach (var module in binaryModule)
|
|
sourceFiles.AddRange(buildData.Modules[module].SourceFiles.Where(x => x.EndsWith(".cs")));
|
|
sourceFiles.RemoveAll(x => x.EndsWith(BuildFilesPostfix));
|
|
var moduleGen = Path.Combine(project.ProjectFolderPath, "Source", binaryModuleName + ".Gen.cs");
|
|
if (!sourceFiles.Contains(moduleGen))
|
|
sourceFiles.Add(moduleGen);
|
|
sourceFiles.Sort();
|
|
|
|
// Get references
|
|
fileReferences.Clear();
|
|
foreach (var module in binaryModule)
|
|
{
|
|
if (!buildData.Modules.TryGetValue(module, out var moduleBuildOptions))
|
|
continue;
|
|
|
|
// Find references based on the modules dependencies
|
|
foreach (var dependencyName in moduleBuildOptions.PublicDependencies.Concat(moduleBuildOptions.PrivateDependencies))
|
|
{
|
|
var dependencyModule = buildData.Rules.GetModule(dependencyName);
|
|
if (dependencyModule != null &&
|
|
!string.IsNullOrEmpty(dependencyModule.BinaryModuleName) &&
|
|
dependencyModule.BuildCSharp &&
|
|
dependencyModule.BinaryModuleName != binaryModuleName &&
|
|
buildData.Modules.TryGetValue(dependencyModule, out var dependencyModuleOptions))
|
|
{
|
|
foreach (var x in buildData.BinaryModules)
|
|
{
|
|
if (x.Key == null || x.Key != dependencyModule.BinaryModuleName)
|
|
continue;
|
|
|
|
// Reference module output binary
|
|
fileReferences.Add(Path.Combine(outputPath, dependencyModule.BinaryModuleName + ".CSharp.dll"));
|
|
}
|
|
var referencedBuild = buildData.FinReferenceBuildModule(dependencyModule.BinaryModuleName);
|
|
if (referencedBuild != null && !string.IsNullOrEmpty(referencedBuild.ManagedPath))
|
|
{
|
|
// Reference binary module build build for referenced target
|
|
fileReferences.Add(referencedBuild.ManagedPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build assembly
|
|
BuildDotNet(graph, buildData, buildOptions, binaryModuleName + ".CSharp", sourceFiles, fileReferences, binaryModule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|