using System; using System.Reflection; namespace Editor; public static partial class EditorUtility { public static partial class Projects { /// public static async Task Compile( Project project, Action logOutput ) => await Compile( project, project.Config.GetCompileSettings(), logOutput ); /// /// Compiled a project ready to be published. Will return the compiled result and any CodeArchives /// internal static async Task Compile( Project project, Compiler.Configuration compilerSettings, Action logOutput ) { bool isGame = project.Config.Type == "game"; var codePath = project.GetCodePath(); if ( !System.IO.Directory.Exists( codePath ) ) return default; using CompileGroup compileGroup = new CompileGroup( "Publish" ); compileGroup.AccessControl = PackageManager.AccessControl; compilerSettings.IgnoreFolders.Add( "editor" ); compilerSettings.IgnoreFolders.Add( "unittest" ); compilerSettings.ReleaseMode = Compiler.ReleaseMode.Release; compilerSettings.StripDisabledTextTrivia = true; var compiler = compileGroup.CreateCompiler( $"{project.Config.Org}.{project.Config.Ident}", codePath, compilerSettings ); compiler.UseAbsoluteSourcePaths = false; //Log.Info( $"Code Path: {addon.GetCodePath()}" ); bool hasBase = false; // // Install any libraries (unless we are a library) // if ( project.Config.Type != "library" ) { foreach ( var (library, references) in SortLibrariesForCompilation( Project.Libraries, logOutput ) ) { await PackageManager.InstallAsync( new PackageLoadOptions( library.Package.FullIdent, "publish" ) ); var compilerName = GetLibraryCompilerName( library ); // Compile library { var compileSettings = new Compiler.Configuration(); compileSettings.Clean(); compileSettings.ReleaseMode = Compiler.ReleaseMode.Release; var libCompiler = compileGroup.CreateCompiler( compilerName, library.GetCodePath(), compileSettings ); libCompiler.UseAbsoluteSourcePaths = false; libCompiler.GeneratedCode.AppendLine( "global using static Sandbox.Internal.GlobalGameNamespace;" ); // Required by razor libCompiler.GeneratedCode.AppendLine( "global using Microsoft.AspNetCore.Components;" ); libCompiler.GeneratedCode.AppendLine( "global using Microsoft.AspNetCore.Components.Rendering;" ); libCompiler.AddReference( "package.base" ); foreach ( var reference in references ) { libCompiler.AddReference( "package." + GetLibraryCompilerName( reference ) ); } } // add a reference to it from the main compiler { compiler.AddReference( "package." + compilerName ); } } } // // if we're a game or an addon then put the base code in the base // if ( !hasBase ) { var baseSettings = new Compiler.Configuration(); baseSettings.Clean(); baseSettings.ReleaseMode = Compiler.ReleaseMode.Release; logOutput?.Invoke( "Adding package.base to compiler" ); var baseCompiler = compileGroup.CreateCompiler( "base", EngineFileSystem.Root.GetFullPath( "/addons/base/code/" ), baseSettings ); baseCompiler.UseAbsoluteSourcePaths = false; baseCompiler.GeneratedCode.AppendLine( "global using static Sandbox.Internal.GlobalGameNamespace;" ); // Required by razor baseCompiler.GeneratedCode.AppendLine( "global using Microsoft.AspNetCore.Components;" ); baseCompiler.GeneratedCode.AppendLine( "global using Microsoft.AspNetCore.Components.Rendering;" ); // reference this from the main compiler compiler.AddReference( "package.base" ); } logOutput?.Invoke( $"Creating package.{project.Config.Org}.{project.Config.Ident} compiler" ); logOutput?.Invoke( $"Compile path is {project.GetCodePath()}" ); logOutput?.Invoke( $"Generating code.." ); compiler.GeneratedCode.AppendLine( "global using static Sandbox.Internal.GlobalGameNamespace;" ); // Required by razor compiler.GeneratedCode.AppendLine( "global using Microsoft.AspNetCore.Components;" ); compiler.GeneratedCode.AppendLine( "global using Microsoft.AspNetCore.Components.Rendering;" ); foreach ( var c in compileGroup.Compilers ) { // important - this is what is used by the error system to determine which addon it came from // we really only want to set this metadata on assemblies that are being published, that way // we can skip reporting errors in addons that are being developed locally, and only get the // count of "in the wild" errors. c.GeneratedCode.AppendLine( $"[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", {project.Config.Title.QuoteSafe()} )]" ); c.GeneratedCode.AppendLine( $"[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", {project.Config.Ident.QuoteSafe()} )]" ); c.GeneratedCode.AppendLine( $"[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", {project.Config.Org.QuoteSafe()} )]" ); c.GeneratedCode.AppendLine( $"[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", {project.Config.FullIdent.QuoteSafe()} )]" ); c.GeneratedCode.AppendLine( $"[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", {System.DateTime.UtcNow.ToString().QuoteSafe()} )]" ); c.GeneratedCode.AppendLine( $"[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", {Sandbox.Engine.Protocol.Api.ToString().QuoteSafe()} )]" ); c.GeneratedCode.AppendLine( $"[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", {1.ToString().QuoteSafe()} )]" ); } logOutput?.Invoke( $"Building.." ); await compileGroup.BuildAsync(); PackageManager.UnmountTagged( "publish" ); logOutput?.Invoke( $"Done.." ); if ( !compiler.BuildSuccess ) { logOutput?.Invoke( $"COMPILE FAILED!!" ); logOutput?.Invoke( $"" ); logOutput?.Invoke( compileGroup.BuildResult.BuildDiagnosticsString() ); logOutput?.Invoke( $"" ); throw new System.InvalidOperationException( "Compiling failed. Can't continue." ); } logOutput?.Invoke( $"Success!" ); return compileGroup.BuildResult.Output.ToArray(); } /// /// Resolve a compiler from an assembly, using the assembly name /// public static Compiler ResolveCompiler( Assembly assembly ) { return Project.ResolveCompiler( assembly ); } private static string GetLibraryCompilerName( Project library ) { return "library." + library.Package.GetIdent( false, false ); } /// /// Finds the compilation order for any of the given libraries containing code, /// based on which libraries reference others. /// private static IReadOnlyList<(Project Project, IReadOnlyList References)> SortLibrariesForCompilation( IEnumerable libraries, Action logOutput ) { libraries = libraries.Where( x => x.HasCodePath() ); var identMap = libraries .DistinctBy( x => x.Package.GetIdent( false, false ) ) .ToDictionary( x => x.Package.GetIdent( false, false ), x => x, StringComparer.OrdinalIgnoreCase ); IReadOnlyList FindReferences( Project project ) { return project.Package.PackageReferences .Select( refName => identMap.TryGetValue( refName, out var reference ) ? reference : null ) .Where( x => x is not null ) .ToArray(); } var remaining = identMap.Values .Select( project => (Project: project, References: FindReferences( project )) ) .ToList(); var sorted = new List<(Project Project, IReadOnlyList References)>(); var added = new HashSet(); while ( remaining.Any() ) { var next = remaining.MinBy( x => x.References.Count( y => !added.Contains( y ) ) ); var cyclicReferences = next.References .Where( y => !added.Contains( y ) ) .ToArray(); if ( cyclicReferences.Any() ) { logOutput?.Invoke( $"Cyclic library dependency: {next.Project.Package.FullIdent}, " + $"{string.Join( ", ", cyclicReferences.Select( x => x.Package.FullIdent ) )}" ); } sorted.Add( next ); remaining.Remove( next ); added.Add( next.Project ); } return sorted; } } }