using System.CommandLine; using System.Diagnostics; using SimpleExec; using Spectre.Console; using BuildTool.Modules; using Command = System.CommandLine.Command; namespace BuildTool; class Program { /// /// Given a list of modules, returns a list with the order in which they need to be build. /// /// The list of modules we want to build. /// The list will receive the modules in the order in which they need to be build. /// if the modules have been ordered; , if the graph could not be ordered. private static bool ComputeBuildOrder(List requestedModules, List outOrderedModules) { Dictionary indegrees = new(requestedModules.Count); Dictionary> dependees = new(requestedModules.Count); Queue queue = new Queue(requestedModules.Count); // Get in-degrees and put 0-degree nodes into queue foreach (Module module in requestedModules) { int inDegree = module.Dependencies.Count; indegrees[module] = inDegree; if (inDegree == 0) { queue.Enqueue(module); } else { foreach (Module dependency in module.Dependencies) { if (!dependees.TryGetValue(dependency, out List? dependeeList)) { dependees[dependency] = dependeeList = new List(); } dependeeList.Add(module); } } } while (queue.TryDequeue(out Module? currentModule)) { outOrderedModules.Add(currentModule); if (dependees.TryGetValue(currentModule, out List? dependeeList)) { foreach (var dependee in dependeeList) { int dependeeIndegree = --indegrees[dependee]; Debug.Assert(dependeeIndegree >= 0); if (dependeeIndegree == 0) { queue.Enqueue(dependee); } } } } if (outOrderedModules.Count < requestedModules.Count) { // We have a cycle and can't order the dependencies. return false; } return true; } static async Task Main(string[] args) { List modules = [new BeefWorkspaceModule(), new GlitchyEngineHelperModule(), new ScriptCoreModule(), new SetupNethostModule(), new Box2DModule(), new FreetypeStackModule(), new MSDFgenModule(), new AllModule()]; // Find dependencies foreach (Module module in modules) { foreach (Type moduleDependencyType in module.DependencyTypes) { Module dependencyInstance = modules.Single(m => moduleDependencyType.IsInstanceOfType(m)); module.Dependencies.Add(dependencyInstance); } } List orderedModules = new List(modules.Count); if (!ComputeBuildOrder(modules, orderedModules)) { throw new Exception($"Could not compute build order. Are there any cylces? Unsorted modules: {string.Join(", ", modules.Except(orderedModules).Select(m => m.Name))}"); } Option debugOption = new("--debug", "-d") { Description = "Defines that the specified modules should be compiled as debug builds." }; Option releaseOption = new("--release", "-r") { Description = "Defines that the specified modules should be compiled as release builds." }; Option rebuildOption = new("--force-rebuild", "-f") { Description = "Defines that the specified modules should be rebuild." }; Option noDependenciesOption = new("--no-dependencies", "-n") { Description = "Defines that only the specified modules are build, but not their dependencies (unless they are also specified)." }; Option workspaceRootOption = new("--workspace", "-w") { Description = "The relative or absolute path to the root directory of the workspace. If unset, the current working directory will be used.", DefaultValueFactory = _ => new DirectoryInfo(Environment.CurrentDirectory) }; workspaceRootOption.Validators.Add(result => { if (result.GetValueOrDefault() is not DirectoryInfo workspaceRoot) { result.AddError("The workspace directory doesn't exist."); return; } if (!workspaceRoot.Exists) { result.AddError($"The workspace directory '{workspaceRoot.FullName}' doesn't exist."); } // Minimal sanity check: Is it actually a beef workspace? (Does BeefSpace.toml exist) string beefSpaceFile = Path.Combine(workspaceRoot.FullName, "BeefSpace.toml"); if (!File.Exists(beefSpaceFile)) { result.AddError($"The directory '{workspaceRoot.FullName}' doesn't seem to be a workspace. Either invoke it from the root of the workspace, or use the '--workspace' option."); } }); Argument modulesArgument = new("modules") { Description = "The modules to build.", // DefaultValueFactory = _ => [AllModule.ModuleName] }; modulesArgument.Validators.Add(result => { if (result.GetValue(modulesArgument) is string[] modulesToBuild) { foreach (string moduleToBuild in modulesToBuild) { if (!modules.Any(m => m.Name.Equals(moduleToBuild, StringComparison.OrdinalIgnoreCase))) { result.AddError($"The module \"{moduleToBuild}\" doesn't exists."); } } } }); modulesArgument.CompletionSources.Add(completionContext => { string[] alreadySpecifiedModules = completionContext.ParseResult.GetValue(modulesArgument) ?? []; return modules.Select(m => m.Name) // Get available Module names. .Where(moduleName => moduleName.StartsWith(completionContext.WordToComplete)) // Only take the modules that start with the word to complete. .Where(moduleName => !alreadySpecifiedModules.Contains(moduleName)); // Remove the modules that already are specified. }); RootCommand rootCommand = new("Builds the engine and it's modules."); Command buildCommand = new("build", "Builds the specified modules. At least either '--debug' or '--release' or both must be specified."); rootCommand.Add(buildCommand); buildCommand.Options.Add(debugOption); buildCommand.Options.Add(releaseOption); buildCommand.Options.Add(rebuildOption); buildCommand.Options.Add(noDependenciesOption); buildCommand.Options.Add(workspaceRootOption); buildCommand.Arguments.Add(modulesArgument); buildCommand.Validators.Add(result => { bool buildDebug = result.GetValue(debugOption); bool buildRelease = result.GetValue(releaseOption); if (!buildDebug && !buildRelease) { result.AddError("Either \"--debug\" or \"--release\" option, or both, must be specified."); } }); buildCommand.SetAction(parseResult => BuildModules( orderedModules, parseResult.GetValue(modulesArgument)!, parseResult.GetValue(workspaceRootOption)!, parseResult.GetValue(rebuildOption), parseResult.GetValue(noDependenciesOption), parseResult.GetValue(debugOption), parseResult.GetValue(releaseOption))); Command listModulesCommand = new("list"); rootCommand.Add(listModulesCommand); listModulesCommand.SetAction(result => { Console.WriteLine("Following modules can be build:"); foreach (Module module in modules) { Console.WriteLine(module.Name); } Console.WriteLine("Build order:"); Console.WriteLine(string.Join(" -> ", orderedModules.Select(m => m.Name))); }); ParseResult parseResult = rootCommand.Parse(args); return await parseResult.InvokeAsync(); } private static async Task BuildModules(List allModulesOrdered, string[] moduleNamesToBuild, DirectoryInfo workspaceRoot, bool forceRebuild, bool noDependencies, bool debug, bool release) { HashSet modulesToBuild = new HashSet(allModulesOrdered.Count); if (moduleNamesToBuild.Length == 0) { Console.WriteLine("Building all modules."); modulesToBuild.UnionWith(allModulesOrdered); } else { Queue modulesToAdd = new Queue(allModulesOrdered.Where(m => moduleNamesToBuild.Contains(m.Name, StringComparer.OrdinalIgnoreCase))); while (modulesToAdd.TryDequeue(out Module? moduleToAdd)) { if (modulesToBuild.Add(moduleToAdd) && !noDependencies) { foreach (Module dependency in moduleToAdd.Dependencies) { modulesToAdd.Enqueue(dependency); } } } } List acutalModuleOrder = allModulesOrdered.Intersect(modulesToBuild).ToList(); Console.WriteLine($"Building modules: {string.Join(", ", acutalModuleOrder.Select(m => m.Name))}"); await Task.Delay(1000); Console.WriteLine($"Workspace: {workspaceRoot.FullName}"); BuildInfo buildInfo = new(new WorkingDirectoryHistory(workspaceRoot.FullName), forceRebuild, debug, release); foreach (Module module in acutalModuleOrder) { string message = $"Building module: [bold italic blue]{module.Name}[/]"; Console.WriteLine(); Console.WriteLine(new string('-', message.Length)); AnsiConsole.MarkupLine(message); Console.WriteLine(new string('-', message.Length)); Console.WriteLine(); buildInfo.WorkingDirectory.NavigateToIndex(0); try { await module.Run(buildInfo); AnsiConsole.MarkupLine($"\n[green]Module [bold italic]{module.Name}[/] completed successfully.[/]\n"); } catch (ExitCodeException e) { AnsiConsole.MarkupLine($"\n[bold red]Error:[/] Module [bold italic]{module.Name}[/] failed with exit code: [red]{e.ExitCode}[/]\n"); break; } } } }