diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6dd29b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/ \ No newline at end of file diff --git a/runner.d b/runner.d new file mode 100755 index 0000000..cf96109 --- /dev/null +++ b/runner.d @@ -0,0 +1,211 @@ +#!/usr/local/bin/rdmd + +/** + * Simple wrapper program for managing the various solutions. Each solution is + * defined as a single D source file in `src/sX.d`, where `X` is the number of + * the solution. All executables are placed in a `bin/` directory. + * + * This script can be executed standalone via `./runner.d`, provided that you + * have given the script the execution privilege (usually with `chmod +x runner.d`). + * + * It offers the following capabilities: + * - `./runner.d clean` removes all compiled binaries. + * - `./runner.d run [all | solution...]` Runs one or more solutions. + * - `./runner.d create ` Creates a new solution source file. + */ +module runner; + +import std.stdio; +import std.string; +import std.process; +import std.file; +import std.path; +import std.regex; +import std.algorithm; +import std.digest.md; +import std.base64; + +int main(string[] args) { + if (args.length < 2) { + // By default, run the script with different behavior depending on what's available. + string[] runArgs = []; + string lastSolutionFilePath = buildPath("bin", "last-solution.txt"); + if (exists(lastSolutionFilePath)) { + string solution = readText(lastSolutionFilePath).strip(); + if (exists(buildPath("bin", solution)) && exists(buildPath("src", solution ~ ".d"))) { + writeln("Running the last solution that was previously ran."); + runArgs = [solution]; + } + } + return run(runArgs); + } else { + string command = args[1].strip().toLower(); + if (command == "clean") { + clean(); + return 0; + } else if (command == "run") { + return run(args[2 .. $]); + } else if (command == "create") { + return create(args[2 .. $]); + } else { + writefln!"Unknown command: \"%s\". Should be one of: clean | run [solution...]"(command); + return 1; + } + } +} + +/** + * Removes all files from the `bin/` directory, which forces recompilation of + * any solutions. + */ +void clean() { + if (!exists("bin")) return; + foreach (entry; dirEntries("bin", SpanMode.shallow, false)) { + if (isFile(entry.name)) { + writefln!"Removing file %s"(entry.name); + std.file.remove(entry.name); + } else if (isDir(entry.name)) { + writefln!"Removing directory %s"(entry.name); + rmdirRecurse(entry.name); + } + } +} + +/** + * Runs one or more solutions, compiling them if needed. + * Params: + * args = The arguments provided to this command. + * Returns: A program exit code. + */ +int run(string[] args) { + string[] solutionsToRun; + if (args.length == 0 || args[0].strip().toLower() == "all") { + auto r = ctRegex!(`^(s\d+)\.d$`); + foreach (entry; dirEntries("src", SpanMode.shallow, false)) { + auto c = matchFirst(baseName(entry.name), r); + if (c) solutionsToRun ~= c[1]; + } + } else { + foreach (arg; args) { + string solutionName = arg; + if (!solutionName.startsWith("s")) solutionName = "s" ~ solutionName; + if (solutionName.endsWith(".d")) solutionName = solutionName[0 .. $ - 2]; + string filePath = buildPath("src", solutionName ~ ".d"); + if (!exists(filePath)) { + writefln!"The solution at %s doesn't exist."(filePath); + return 1; + } + solutionsToRun ~= solutionName; + } + } + if (solutionsToRun.length == 0) { + writeln("No solutions to run."); + return 0; + } + solutionsToRun.sort(); + foreach (solution; solutionsToRun) { + bool canRun = true; + if (isSolutionRecompileNeeded(solution)) { + writefln!"Recompiling solution %s..."(solution); + int result = compileSolution(solution); + if (result != 0) canRun = false; + } + if (canRun) { + runSolution(solution); + } + } + return 0; +} + +/** + * Checks if a solution needs to be (re)compiled before it's run. This is the + * case when the executable doesn't exist, or the hash of the solution source + * doesn't match the hash of the current source. + * Params: + * solution = The solution to check. + * Returns: True if we should recompile the solution. + */ +bool isSolutionRecompileNeeded(string solution) { + string solutionFilePath = buildPath("src", solution ~ ".d"); + string hashFilePath = buildPath("bin", solution ~ "-hash.md5"); + string executablePath = buildPath("bin", solution); + if (!exists(hashFilePath) || !exists(executablePath)) return true; + ubyte[] storedHash = Base64.decode(readText(hashFilePath).strip()); + ubyte[16] currentHash = md5Of(readText(solutionFilePath)); + return storedHash != currentHash; +} + +/** + * Compiles a solution using `dmd`, and includes an MD5 hash of the source in + * the `bin/` directory alongside the executable. + * Params: + * solution = The solution to compile. + * Returns: 0 on success, or 1 on failure. + */ +int compileSolution(string solution) { + string solutionFilePath = buildPath("src", solution ~ ".d"); + string hashFilePath = buildPath("bin", solution ~ "-hash.md5"); + if (!exists("bin")) mkdir("bin"); + ubyte[16] hash = md5Of(readText(solutionFilePath)); + std.file.write(hashFilePath, Base64.encode(hash)); + string utilFilePath = buildPath("src", "util.d"); + string outputFilePath = buildPath("bin", solution); + string cmd = format!"dmd %s %s -inline -O -of=%s"(utilFilePath, solutionFilePath, outputFilePath); + auto result = executeShell(cmd); + if (result.status != 0) { + writefln!"Failed to compile solution %s, exit code %d:\n%s"(solution, result.status, result.output); + return 1; + } + return 0; +} + +/** + * Executes a solution, using this script's current working directory as the + * directory for the process, and inheriting all IO streams. + * Params: + * solution = The solution to run. + */ +void runSolution(string solution) { + string processPath = buildPath("bin", solution); + writefln!"-----< %s >-----\n"(solution); + Pid pid = spawnProcess(processPath); + int result = wait(pid); + writefln!"\n-----< %s >-----\n"(solution); + if (result != 0) { + writefln!"Solution %s failed with exit code %d."(solution, result); + } + std.file.write(buildPath("bin", "last-solution.txt"), solution); +} + +/** + * Creates a new solution source file. + * Params: + * args = The command line arguments. + * Returns: An exit code. + */ +int create(string[] args) { + if (args.length < 1) { + writeln("Missing required solution name."); + return 1; + } + string solution = args[0].strip().toLower(); + auto r = ctRegex!(`^s\d+$`); + auto c = matchFirst(solution, r); + if (!c) { + writefln!"Solution name \"%s\" is not valid. Should be \"s\\d+\"."(solution); + return 1; + } + string filePath = buildPath("src", solution ~ ".d"); + bool force = args.length >= 2 && args[1].strip().toLower() == "-f"; + if (exists(filePath) && !force) { + writefln!"Solution \"%s\" already exists."(solution); + return 1; + } + File f = File(filePath, "w"); + f.writefln!"module %s;"(solution); + f.writeln("import util;"); + f.writeln(); + f.writefln!"void main(string[] args) {\n writeln(\"Hello from %s\");\n}"(solution); + f.close(); + return 0; +} \ No newline at end of file diff --git a/src/s1.d b/src/s1.d new file mode 100644 index 0000000..a65bf2c --- /dev/null +++ b/src/s1.d @@ -0,0 +1,6 @@ +module s1; +import util; + +void main() { + writeln("Hello world!!"); +} \ No newline at end of file diff --git a/src/util.d b/src/util.d new file mode 100644 index 0000000..bcf797c --- /dev/null +++ b/src/util.d @@ -0,0 +1,12 @@ +/** + * Standard utilities that are included in any solution program. + */ +module util; + +public import std.stdio; +public import std.file; +public import std.string; +public import std.algorithm; +public import std.conv; +public import std.path; +public import std.uni;