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();
string cacheFilePath = Path.Combine(workspaceRoot.FullName, "build", ".build_tool.json");
ModuleCache moduleCache = ModuleCache.Load(cacheFilePath);
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);
// create a copy of the build info updated with the module's watched sources and cache info (last build time etc.)
BuildInfo moduleBuildInfo = module.GetBuildInfo(buildInfo, moduleCache);
if (!moduleBuildInfo.BuildDebug && !moduleBuildInfo.BuildRelease)
{
AnsiConsole.MarkupLine($"\n[green]Module [bold italic]{module.Name}[/] is up to date, skipping.[/]\n");
continue;
}
try
{
await module.Run(moduleBuildInfo);
AnsiConsole.MarkupLine($"\n[green]Module [bold italic]{module.Name}[/] completed successfully.[/]\n");
module.UpdateCacheInfo(moduleBuildInfo, moduleCache, true);
}
catch (ExitCodeException e)
{
AnsiConsole.MarkupLine(
$"\n[bold red]Error:[/] Module [bold italic]{module.Name}[/] failed with exit code: [red]{e.ExitCode}[/]\n");
module.UpdateCacheInfo(moduleBuildInfo, moduleCache, false);
break;
}
finally
{
moduleCache.Save(cacheFilePath);
}
}
}
}