Quellcode durchsuchen

Made a scripted build system that can follow includes automatically.

David Piuva vor 3 Jahren
Ursprung
Commit
640eac18cd

+ 14 - 0
Doc/Files.html

@@ -59,6 +59,20 @@ On Posix, you can have a path beginning with:
 </P><P>
 * A relative path beginning from the current directory, such as folder/files/document.txt
 </P><IMG SRC="Images/Border.png"><P>
+</P><H2> Best practice</H2><P>Store paths in relative form when you want things to work across different computers, but remember that relative paths are usually relative to the location from which the application was called, which is usually the desktop from where the shortcut started the program.
+If your application worked fine while building it from the same folder, but crashes from not finding resources when called from a shortcut, you need to handle your current path somehow.
+
+</P><P>
+Use absolute paths when you are working on one computer but often needs to access a file from different locations.
+
+</P><P>
+Use relative paths when you either want to process files specified by the user, or convert paths into absolute form using a theoretical path before use.
+
+</P><P>
+Use file_setCurrentPath(file_getApplicationFolder()); on start-up if you often forget to convert your relative paths into absolute paths.
+Changing the current path is bad practice because it prevents you from using paths relative to the caller origin, but it is also the easiest way to handle all paths relative to the application if you really don't need to access files from the caller's path.
+A compromise is to convert any paths from command line arguments into absolute paths before overwriting the current path with the application path.
+</P><IMG SRC="Images/Border.png"><P>
 </P><H2> Saving and loading</H2><P>
 </P><P>
 file_loadBuffer and file_saveBuffer are the main functions for saving and loading buffers of binary data.

+ 12 - 0
Doc/Generator/Input/Files.txt

@@ -28,6 +28,18 @@ On Posix, you can have a path beginning with:
 
 * A relative path beginning from the current directory, such as folder/files/document.txt
 ---
+Title2: Best practice
+Store paths in relative form when you want things to work across different computers, but remember that relative paths are usually relative to the location from which the application was called, which is usually the desktop from where the shortcut started the program.
+If your application worked fine while building it from the same folder, but crashes from not finding resources when called from a shortcut, you need to handle your current path somehow.
+
+Use absolute paths when you are working on one computer but often needs to access a file from different locations.
+
+Use relative paths when you either want to process files specified by the user, or convert paths into absolute form using a theoretical path before use.
+
+Use file_setCurrentPath(file_getApplicationFolder()); on start-up if you often forget to convert your relative paths into absolute paths.
+Changing the current path is bad practice because it prevents you from using paths relative to the caller origin, but it is also the easiest way to handle all paths relative to the application if you really don't need to access files from the caller's path.
+A compromise is to convert any paths from command line arguments into absolute paths before overwriting the current path with the application path.
+---
 Title2: Saving and loading
 
 file_loadBuffer and file_saveBuffer are the main functions for saving and loading buffers of binary data.

+ 0 - 1
Doc/Generator/main.cpp

@@ -129,7 +129,6 @@ static ReadableString getExtensionless(const String& filename) {
 
 void processFolder(const ReadableString& sourceFolderPath, const ReadableString& targetFolderPath) {
 	file_getFolderContent(sourceFolderPath, [targetFolderPath](const ReadableString& sourcePath, const ReadableString& entryName, EntryType entryType) {
-		printText("* Entry: ", entryName, " as ", entryType, "\n");
 		if (entryType == EntryType::Folder) {
 			// TODO: Create new output folders if needed for nested output.
 			//processFolder(sourcePath, file_combinePaths(targetFolderPath, entryName));

+ 56 - 0
Source/DFPSR/DFPSR.DsrHead

@@ -0,0 +1,56 @@
+# A project header for using the DFPSR library.
+#   Backends:
+#     * Give the Graphics flag if the application should be able to create a window.
+#     * Give the Sound flag if the application should be able to generate sounds.
+#   Systems:
+#     * Give the Linux flag when compiling on Linux or similar Posix systems having the same dependencies installed.
+#     * Give the Windows flag when compiling on Microsoft Windows.
+#   Strings use a subset of the C standard for mangling, so \\ is used to write \.
+#     You can also use / and let the file abstraction layer convert it into \ automatically when running on Windows.
+
+if Linux
+	message "Building for Linux\n"
+end
+if Windows
+	message "Building for Windows\n"
+end
+
+# Change this if the compiler uses a different prefix for linking a library.
+linkerPrefix = "-l"
+
+# Paths are relative to the current script, even if imported somewhere else
+#   so we use .. to leave the Source/DFPSR folder and then go into the windowManagers folder.
+WindowManager = "../windowManagers/NoWindow.cpp"
+if Graphics
+	message "Building with graphics enabled"
+	if Linux
+		message "  Using X11\n"
+		LinkerFlag linkerPrefix & "X11"
+		WindowManager = "../windowManagers/X11Window.cpp"
+	end
+	if Windows
+		message "  Using Win32\n"
+		LinkerFlag linkerPrefix & "gdi32"
+		LinkerFlag linkerPrefix & "user32"
+		LinkerFlag linkerPrefix & "kernel32"
+		LinkerFlag linkerPrefix & "comctl32"
+		WindowManager = "../windowManagers/Win32Window.cpp"
+	end
+end
+Compile WindowManager
+
+SoundManager = "../soundManagers/NoSound.cpp"
+if Sound
+	message "Building with sound enabled"
+	if Linux
+		message "  Using Alsa\n"
+		LinkerFlag linkerPrefix & "asound"
+		SoundManager = "../soundManagers/AlsaSound.cpp"
+	end
+	if Windows
+		message "  Using WinMM\n"
+		LinkerFlag linkerPrefix & "winmm"
+		SoundManager = "../soundManagers/WinMMSound.cpp"
+	end
+end
+Compile SoundManager

+ 11 - 0
Source/DFPSR/api/fileAPI.cpp

@@ -242,6 +242,17 @@ ReadableString file_getPathlessName(const ReadableString &path) {
 	return string_after(path, getLastSeparator(path, -1));
 }
 
+ReadableString file_getExtension(const String& filename) {
+	int64_t lastDotIndex = string_findLast(filename, U'.');
+	int64_t lastSeparatorIndex = getLastSeparator(filename, -1);
+	// Only use the last dot if there is no folder separator after it.
+	if (lastDotIndex != -1 && lastSeparatorIndex < lastDotIndex) {
+		return string_removeOuterWhiteSpace(string_after(filename, lastDotIndex));
+	} else {
+		return U"";
+	}
+}
+
 String file_getRelativeParentFolder(const ReadableString &path, PathSyntax pathSyntax) {
 	String optimizedPath = file_optimizePath(path, pathSyntax);
 	if (string_length(optimizedPath) == 0) {

+ 7 - 5
Source/DFPSR/api/fileAPI.h

@@ -153,9 +153,15 @@ namespace dsr {
 
 	// Path-syntax: This trivial operation should work the same independent of operating system.
 	//              Otherwise you just have to add a new argument after upgrading the static library.
-	// Returns the local name of the file or folder after the last path separator, or the whole path if no separator was found.
+	// Post-condition: Returns the local name of the file or folder after the last path separator, or the whole path if no separator was found.
 	ReadableString file_getPathlessName(const ReadableString &path);
 
+	// Path-syntax: This trivial operation should work the same independent of operating system.
+	// Post-condition: Returns the filename's extension, or U"" if there is none.
+	// This function can not tell if something is a folder or not, because there are file types on Posix systems that have no extension either.
+	//   Use file_getEntryType instead if you want to know if it's a file or folder.
+	ReadableString file_getExtension(const String& filename);
+
 	// Quickly gets the relative parent folder by removing the last entry from the string or appending .. at the end.
 	// Path-syntax: Depends on pathSyntax argument.
 	// This pure syntax function getting the parent folder does not access the system in any way.
@@ -206,10 +212,6 @@ namespace dsr {
 	// Post-condition: Returns true iff the folder could be found.
 	bool file_getFolderContent(const ReadableString& folderPath, std::function<void(const ReadableString& entryPath, const ReadableString& entryName, EntryType entryType)> action);
 
-
-
-	// Functions below are for testing and simulation of other systems by substituting the current directory and operating system with manual settings.
-
 	// A theoretical version of file_getParentFolder for evaluation on a theoretical system without actually calling file_getCurrentPath or running on the given system.
 	// Path-syntax: Depends on pathSyntax argument.
 	// Post-condition: Returns the absolute parent to the given path, or U"?" if trying to leave the root or use a tilde home alias.

+ 2 - 11
Source/DFPSR/api/imageAPI.cpp

@@ -106,20 +106,11 @@ Buffer dsr::image_encode(const ImageRgbaU8 &image, ImageFileFormat format, int q
 	}
 }
 
-static ReadableString getFileExtension(const String& filename) {
-	int lastDotIndex = string_findLast(filename, U'.');
-	if (lastDotIndex != -1) {
-		return string_removeOuterWhiteSpace(string_after(filename, lastDotIndex));
-	} else {
-		return U"?";
-	}
-}
-
 static ImageFileFormat detectImageFileExtension(const String& filename) {
 	ImageFileFormat result = ImageFileFormat::Unknown;
 	int lastDotIndex = string_findLast(filename, U'.');
 	if (lastDotIndex != -1) {
-		ReadableString extension = string_upperCase(getFileExtension(filename));
+		ReadableString extension = string_upperCase(file_getExtension(filename));
 		if (string_match(extension, U"JPG") || string_match(extension, U"JPEG")) {
 			result = ImageFileFormat::JPG;
 		} else if (string_match(extension, U"PNG")) {
@@ -137,7 +128,7 @@ bool dsr::image_save(const ImageRgbaU8 &image, const String& filename, bool must
 	ImageFileFormat extension = detectImageFileExtension(filename);
 	Buffer buffer;
 	if (extension == ImageFileFormat::Unknown) {
-		ReadableString extension = getFileExtension(filename);
+		ReadableString extension = file_getExtension(filename);
 		if (mustWork) { throwError(U"The extension *.", extension, " in ", filename, " is not a supported image format.\n"); }
 		return false;
 	} else {

+ 1 - 1
Source/soundManagers/AlsaSound.cpp

@@ -3,7 +3,7 @@
 //   Install on Arch: sudo pacman -S libasound-dev
 //   Install on Debian: sudo apt-get install libasound-dev
 
-#include "../../dfpsr/Source/DFPSR/includeFramework.h"
+#include "../DFPSR/includeFramework.h"
 #include "soundManagers.h"
 #include <alsa/asoundlib.h>
 

+ 1 - 1
Source/soundManagers/WinMMSound.cpp

@@ -1,7 +1,7 @@
 
 // Use -lwinmm for linking to the winmm library in GCC/G++
 
-#include "../../dfpsr/Source/DFPSR/includeFramework.h"
+#include "../DFPSR/includeFramework.h"
 #include "soundManagers.h"
 #include <windows.h>
 #include <mmsystem.h>

+ 283 - 0
Source/tools/builder/Machine.cpp

@@ -0,0 +1,283 @@
+
+#include "Machine.h"
+#include "generator.h"
+#include "../../DFPSR/api/fileAPI.h"
+
+using namespace dsr;
+
+int64_t findFlag(const Machine &target, const dsr::ReadableString &key) {
+	for (int64_t f = 0; f < target.variables.length(); f++) {
+		if (string_caseInsensitiveMatch(key, target.variables[f].key)) {
+			return f;
+		}
+	}
+	return -1;
+}
+
+ReadableString getFlag(const Machine &target, const dsr::ReadableString &key, const dsr::ReadableString &defaultValue) {
+	int64_t existingIndex = findFlag(target, key);
+	if (existingIndex == -1) {
+		return defaultValue;
+	} else {
+		return target.variables[existingIndex].value;
+	}
+}
+
+int64_t getFlagAsInteger(const Machine &target, const dsr::ReadableString &key, int64_t defaultValue) {
+	int64_t existingIndex = findFlag(target, key);
+	if (existingIndex == -1) {
+		return defaultValue;
+	} else {
+		return string_toInteger(target.variables[existingIndex].value);
+	}
+}
+
+static String unwrapIfNeeded(const dsr::ReadableString &value) {
+	if (value[0] == U'\"') {
+		return string_unmangleQuote(value);
+	} else {
+		return value;
+	}
+}
+
+void assignValue(Machine &target, const dsr::ReadableString &key, const dsr::ReadableString &value) {
+	int64_t existingIndex = findFlag(target, key);
+	if (existingIndex == -1) {
+		target.variables.pushConstruct(string_upperCase(key), unwrapIfNeeded(value));
+	} else {
+		target.variables[existingIndex].value = unwrapIfNeeded(value);
+	}
+}
+
+static void flushToken(List<String> &targetTokens, String &currentToken) {
+	if (string_length(currentToken) > 0) {
+		targetTokens.push(currentToken);
+		currentToken = U"";
+	}
+}
+
+// Safe access for easy pattern matching.
+static ReadableString getToken(List<String> &tokens, int index) {
+	if (0 <= index && index < tokens.length()) {
+		return tokens[index];
+	} else {
+		return U"";
+	}
+}
+
+static int64_t interpretAsInteger(const dsr::ReadableString &value) {
+	if (string_length(value) == 0) {
+		return 0;
+	} else {
+		return string_toInteger(value);
+	}
+}
+
+#define STRING_EXPR(FIRST_TOKEN, LAST_TOKEN) evaluateExpression(target, tokens, FIRST_TOKEN, LAST_TOKEN)
+#define STRING_LEFT STRING_EXPR(startTokenIndex, opIndex - 1)
+#define STRING_RIGHT STRING_EXPR(opIndex + 1, endTokenIndex)
+
+#define INTEGER_EXPR(FIRST_TOKEN, LAST_TOKEN) interpretAsInteger(STRING_EXPR(FIRST_TOKEN, LAST_TOKEN))
+#define INTEGER_LEFT INTEGER_EXPR(startTokenIndex, opIndex - 1)
+#define INTEGER_RIGHT INTEGER_EXPR(opIndex + 1, endTokenIndex)
+
+#define PATH_EXPR(FIRST_TOKEN, LAST_TOKEN) file_getTheoreticalAbsolutePath(STRING_EXPR(FIRST_TOKEN, LAST_TOKEN), fromPath)
+
+#define MATCH_CIS(TOKEN) string_caseInsensitiveMatch(currentToken, TOKEN)
+#define MATCH_CS(TOKEN) string_match(currentToken, TOKEN)
+
+static String evaluateExpression(Machine &target, List<String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex) {
+	if (startTokenIndex == endTokenIndex) {
+		ReadableString first = getToken(tokens, startTokenIndex);
+		if (string_isInteger(first)) {
+			return first;
+		} else if (first[0] == U'\"') {
+			return string_unmangleQuote(first);
+		} else {
+			// Identifier defaulting to empty.
+			return getFlag(target, first, U"");
+		}
+	} else {
+		int64_t depth = 0;
+		for (int64_t opIndex = 0; opIndex < tokens.length(); opIndex++) {
+			String currentToken = tokens[opIndex];
+			if (MATCH_CS(U"(")) {
+				depth++;
+			} else if (MATCH_CS(U")")) {
+				depth--;
+				if (depth < 0) throwError(U"Negative expression depth!\n");
+			} else if (MATCH_CIS(U"and")) {
+				return string_combine(INTEGER_LEFT && INTEGER_RIGHT);
+			} else if (MATCH_CIS(U"or")) {
+				return string_combine(INTEGER_LEFT || INTEGER_RIGHT);
+			} else if (MATCH_CIS(U"xor")) {
+				return string_combine((!INTEGER_LEFT) != (!INTEGER_RIGHT));
+			} else if (MATCH_CS(U"+")) {
+				return string_combine(INTEGER_LEFT + INTEGER_RIGHT);
+			} else if (MATCH_CS(U"-")) {
+				return string_combine(INTEGER_LEFT - INTEGER_RIGHT);
+			} else if (MATCH_CS(U"*")) {
+				return string_combine(INTEGER_LEFT * INTEGER_RIGHT);
+			} else if (MATCH_CS(U"/")) {
+				return string_combine(INTEGER_LEFT / INTEGER_RIGHT);
+			} else if (MATCH_CS(U"<")) {
+				return string_combine(INTEGER_LEFT < INTEGER_RIGHT);
+			} else if (MATCH_CS(U">")) {
+				return string_combine(INTEGER_LEFT > INTEGER_RIGHT);
+			} else if (MATCH_CS(U">=")) {
+				return string_combine(INTEGER_LEFT >= INTEGER_RIGHT);
+			} else if (MATCH_CS(U"<=")) {
+				return string_combine(INTEGER_LEFT <= INTEGER_RIGHT);
+			} else if (MATCH_CS(U"==")) {
+				return string_combine(INTEGER_LEFT == INTEGER_RIGHT);
+			} else if (MATCH_CS(U"!=")) {
+				return string_combine(INTEGER_LEFT != INTEGER_RIGHT);
+			} else if (MATCH_CS(U"&")) {
+				return string_combine(STRING_LEFT, STRING_RIGHT);
+			}
+		}
+		if (depth != 0) throwError(U"Unbalanced expression depth!\n");
+		if (string_match(tokens[startTokenIndex], U"(") && string_match(tokens[endTokenIndex], U")")) {
+			return evaluateExpression(target, tokens, startTokenIndex + 1, endTokenIndex - 1);
+		}
+	}
+	throwError(U"Failed to evaluate expression!\n");
+	return U"?";
+}
+
+static void analyzeSource(const dsr::ReadableString &absolutePath) {
+	EntryType pathType = file_getEntryType(absolutePath);
+	if (pathType == EntryType::File) {
+		printText(U"  Using source from ", absolutePath, U".\n");
+		analyzeFromFile(absolutePath);
+	} else if (pathType == EntryType::Folder) {
+		// TODO: Being analyzing from each source file in the folder recursively.
+		//       Each file that is already included will quickly be ignored.
+		//       The difficult part is that exploring a folder returns files in non-deterministic order and GNU's compiler is order dependent.
+		printText(U"  Searching for source code from the folder ", absolutePath, U" is not yet supported due to order dependent linking!\n");
+	} else if (pathType == EntryType::SymbolicLink) {
+		// Symbolic links can point to both files and folder, so we need to follow it and find out what it really is.
+		analyzeSource(file_followSymbolicLink(absolutePath));
+	}
+}
+
+static void interpretLine(Machine &target, List<String> &tokens, const dsr::ReadableString &fromPath) {
+	if (tokens.length() > 0) {
+		bool activeLine = target.activeStackDepth >= target.currentStackDepth;
+		/*
+		printText(activeLine ? U"interpret:" : U"ignore:");
+		for (int t = 0; t < tokens.length(); t++) {
+			printText(U" [", tokens[t], U"]");
+		}
+		printText(U"\n");
+		*/
+		ReadableString first = getToken(tokens, 0);
+		ReadableString second = getToken(tokens, 1);
+		if (activeLine) {
+			// TODO: Implement elseif and else cases using a list as a virtual stack,
+			//       to remember at which layer the else cases have already been consumed by a true evaluation.
+			// TODO: Remember at which depth the script entered, so that importing something can't leave the rest inside of a dangling if or else by accident.
+			if (string_caseInsensitiveMatch(first, U"import")) {
+				// Get path relative to importing script's path.
+				String importPath = PATH_EXPR(1, tokens.length() - 1);
+				evaluateScript(target, importPath);
+				if (tokens.length() > 2) { printText(U"Unused tokens after import!\n");}
+			} else if (string_caseInsensitiveMatch(first, U"if")) {
+				// Being if statement
+				bool active = INTEGER_EXPR(1, tokens.length() - 1);
+				if (active) {
+					target.activeStackDepth++;
+				}
+				target.currentStackDepth++;
+			} else if (string_caseInsensitiveMatch(first, U"end")) {
+				// End if statement
+				target.currentStackDepth--;
+				target.activeStackDepth = target.currentStackDepth;
+			} else if (string_caseInsensitiveMatch(first, U"compile")) {
+				// The right hand expression is evaluated into a path relative to the build script and used as the root for searching for source code.
+				analyzeSource(PATH_EXPR(1, tokens.length() - 1));
+			} else if (string_caseInsensitiveMatch(first, U"linkerflag")) {
+				target.linkerFlags.push(STRING_EXPR(1, tokens.length() - 1));
+			} else if (string_caseInsensitiveMatch(first, U"compilerflag")) {
+				target.compilerFlags.push(STRING_EXPR(1, tokens.length() - 1));
+			} else if (string_caseInsensitiveMatch(first, U"message")) {
+				// Print a message while evaluating the build script.
+				//   This is not done while actually compiling, so it will not know if compilation and linking worked or not.
+				printText(STRING_EXPR(1, tokens.length() - 1));
+			} else {
+				if (tokens.length() == 1) {
+					// Mentioning an identifier without assigning anything will assign it to one as a boolean flag.
+					assignValue(target, first, U"1");
+				} else if (string_match(second, U"=")) {
+					// TODO: Create in-place math and string operations with different types of assignments.
+					//       Maybe use a different syntax beginning with a keyword?
+					// TODO: Look for the assignment operator dynamically if references to collection elements are allowed as l-value expressions.
+					// Using an equality sign replaces any previous value of the variable.
+					assignValue(target, first, STRING_EXPR(2, tokens.length() - 1));
+				} else {
+					// TODO: Give better error messages.
+					printText(U"  Ignored unrecognized statement!\n");
+				}
+			}
+		} else {
+			if (string_caseInsensitiveMatch(first, U"if")) {
+				target.currentStackDepth++;
+			} else if (string_caseInsensitiveMatch(first, U"end")) {
+				target.currentStackDepth--;
+			}
+		}
+	}
+	tokens.clear();
+}
+
+void evaluateScript(Machine &target, const ReadableString &scriptPath) {
+	if (file_getEntryType(scriptPath) != EntryType::File) {
+		printText(U"The script path ", scriptPath, U" does not exist!\n");
+	}
+	String projectContent = string_load(scriptPath);
+	// Each new script being imported will have its own simulated current path for accessing files and such.
+	String projectFolderPath = file_getAbsoluteParentFolder(scriptPath);
+	String currentToken;
+	List<String> currentLine; // Keep it fast and simple by only remembering tokens for the current line.
+	bool quoted = false;
+	bool commented = false;
+	for (int i = 0; i <= string_length(projectContent); i++) {
+		DsrChar c = projectContent[i];
+		// The null terminator does not really exist in projectContent,
+		//   but dsr::String returns a null character safely when requesting a character out of bound,
+		//   which allow interpreting the last line without duplicating code.
+		if (c == U'\n' || c == U'\0') {
+			// Comment removing everything else.
+			flushToken(currentLine, currentToken);
+			interpretLine(target, currentLine, projectFolderPath);
+			commented = false; // Automatically end comments at end of line.
+			quoted = false; // Automatically end quotes at end of line.
+		} else if (c == U'\"') {
+			quoted = !quoted;
+			string_appendChar(currentToken, c);
+		} else if (c == U'#') {
+			// Comment removing everything else until a new line comes.
+			flushToken(currentLine, currentToken);
+			interpretLine(target, currentLine, projectFolderPath);
+			commented = true;
+		} else if (!commented) {
+			if (quoted) {
+				// Insert character into quote.
+				string_appendChar(currentToken, c);
+			} else {
+				if (c == U'(' || c == U')' || c == U'[' || c == U']' || c == U'{' || c == U'}' || c == U'=') {
+					// Atomic token of a single character
+					flushToken(currentLine, currentToken);
+					string_appendChar(currentToken, c);
+					flushToken(currentLine, currentToken);
+				} else if (c == U' ' || c == U'\t') {
+					// Whitespace
+					flushToken(currentLine, currentToken);
+				} else {
+					// Insert unquoted character into token.
+					string_appendChar(currentToken, c);
+				}
+			}
+		}
+	}
+}

+ 38 - 0
Source/tools/builder/Machine.h

@@ -0,0 +1,38 @@
+
+#ifndef DSR_BUILDER_MACHINE_MODULE
+#define DSR_BUILDER_MACHINE_MODULE
+
+#include "../../DFPSR/api/stringAPI.h"
+
+using namespace dsr;
+
+struct Flag {
+	dsr::String key, value;
+	Flag() {}
+	Flag(const dsr::ReadableString &key, const dsr::ReadableString &value)
+	: key(key), value(value) {}
+};
+
+struct Machine {
+	List<Flag> variables;
+	List<String> compilerFlags, linkerFlags;
+	// When activeStackDepth < currentStackDepth, we are skipping false cases.
+	int64_t currentStackDepth = 0; // How many scopes we are inside of, from the root script including all the others.
+	int64_t activeStackDepth = 0;
+};
+
+// Returns the first case insensitive match for key in target, or -1 if not found.
+int64_t findFlag(const Machine &target, const dsr::ReadableString &key);
+// Returns the value of key in target, or defaultValue if not found.
+ReadableString getFlag(const Machine &target, const dsr::ReadableString &key, const dsr::ReadableString &defaultValue);
+// Returns the value of key in target, defaultValue if not found, or 0 if not an integer.
+int64_t getFlagAsInteger(const Machine &target, const dsr::ReadableString &key, int64_t defaultValue = 0);
+
+// Assigns value to key in target. Allocates key in target if it does not already exist.
+void assignValue(Machine &target, const dsr::ReadableString &key, const dsr::ReadableString &value);
+
+// Modifies the flags in target using the script in scriptPath.
+// Recursively including other scripts using the script's folder as the origin for relative paths.
+void evaluateScript(Machine &target, const ReadableString &scriptPath);
+
+#endif

+ 16 - 0
Source/tools/builder/build.sh

@@ -0,0 +1,16 @@
+#!/bin/bash
+
+LIBRARY_PATH=../../DFPSR
+DEPENDENCIES="${LIBRARY_PATH}/collection/collections.cpp ${LIBRARY_PATH}/api/fileAPI.cpp ${LIBRARY_PATH}/api/bufferAPI.cpp ${LIBRARY_PATH}/api/stringAPI.cpp ${LIBRARY_PATH}/base/SafePointer.cpp Machine.cpp generator.cpp"
+
+# Compile the analyzer
+g++ main.cpp -o builder ${DEPENDENCIES} -std=c++14;
+
+# Compile the wizard project to test if the build system works
+echo "Generating dfpsr_compile.sh from Wizard.DsrProj"
+./builder ../wizard/Wizard.DsrProj Graphics Sound Linux ScriptPath="/tmp/dfpsr_compile.sh";
+echo "Generating dfpsr_win32_test.bat from Wizard.DsrProj"
+./builder ../wizard/Wizard.DsrProj Graphics Sound Windows ScriptPath="/tmp/dfpsr_win32_test.bat";
+echo "Running compile.sh"
+chmod +x /tmp/dfpsr_compile.sh
+/tmp/dfpsr_compile.sh

+ 307 - 0
Source/tools/builder/generator.cpp

@@ -0,0 +1,307 @@
+
+#include "generator.h"
+
+using namespace dsr;
+
+struct Connection {
+	String path;
+	int64_t lineNumber = -1;
+	int64_t dependencyIndex = -1;
+	Connection(const ReadableString& path)
+	: path(path) {}
+	Connection(const ReadableString& path, int64_t lineNumber)
+	: path(path), lineNumber(lineNumber) {}
+};
+
+enum class Extension {
+	Unknown, H, Hpp, C, Cpp
+};
+static Extension extensionFromString(const ReadableString& extensionName) {
+	String upperName = string_upperCase(string_removeOuterWhiteSpace(extensionName));
+	Extension result = Extension::Unknown;
+	if (string_match(upperName, U"H")) {
+		result = Extension::H;
+	} else if (string_match(upperName, U"HPP")) {
+		result = Extension::Hpp;
+	} else if (string_match(upperName, U"C")) {
+		result = Extension::C;
+	} else if (string_match(upperName, U"CPP")) {
+		result = Extension::Cpp;
+	}
+	return result;
+}
+
+struct Dependency {
+	String path;
+	Extension extension;
+	List<Connection> links; // Depends on having these linked after compiling.
+	List<Connection> includes; // Depends on having these included in pre-processing.
+	Dependency(const ReadableString& path, Extension extension)
+	: path(path), extension(extension) {}
+};
+List<Dependency> dependencies;
+
+static int64_t findDependency(const ReadableString& findPath);
+static void resolveConnection(Connection &connection);
+static void resolveDependency(Dependency &dependency);
+static String findSourceFile(const ReadableString& headerPath, bool acceptC, bool acceptCpp);
+static void flushToken(List<String> &target, String &currentToken);
+static void tokenize(List<String> &target, const ReadableString& line);
+static void interpretPreprocessing(int64_t parentIndex, const List<String> &tokens, const ReadableString &parentFolder, int64_t lineNumber);
+static void interpretPreprocessing(int64_t parentIndex, const List<String> &tokens, const ReadableString &parentFolder, int64_t lineNumber);
+static void analyzeCode(int64_t parentIndex, String content, const ReadableString &parentFolder);
+
+static int64_t findDependency(const ReadableString& findPath) {
+	for (int d = 0; d < dependencies.length(); d++) {
+		if (string_match(dependencies[d].path, findPath)) {
+			return d;
+		}
+	}
+	return -1;
+}
+
+static void resolveConnection(Connection &connection) {
+	connection.dependencyIndex = findDependency(connection.path);
+}
+
+static void resolveDependency(Dependency &dependency) {
+	for (int l = 0; l < dependency.links.length(); l++) {
+		resolveConnection(dependency.links[l]);
+	}
+	for (int i = 0; i < dependency.includes.length(); i++) {
+		resolveConnection(dependency.includes[i]);
+	}
+}
+
+void resolveDependencies() {
+	for (int d = 0; d < dependencies.length(); d++) {
+		resolveDependency(dependencies[d]);
+	}
+}
+
+static String findSourceFile(const ReadableString& headerPath, bool acceptC, bool acceptCpp) {
+	int lastDotIndex = string_findLast(headerPath, U'.');
+	if (lastDotIndex != -1) {
+		ReadableString extensionlessPath = string_removeOuterWhiteSpace(string_before(headerPath, lastDotIndex));
+		String cPath = extensionlessPath + U".c";
+		String cppPath = extensionlessPath + U".cpp";
+		if (acceptC && file_getEntryType(cPath) == EntryType::File) {
+			return cPath;
+		} else if (acceptCpp && file_getEntryType(cppPath) == EntryType::File) {
+			return cppPath;
+		}
+	}
+	return U"";
+}
+
+static void flushToken(List<String> &target, String &currentToken) {
+	if (string_length(currentToken) > 0) {
+		target.push(currentToken);
+		currentToken = U"";
+	}
+}
+
+static void tokenize(List<String> &target, const ReadableString& line) {
+	String currentToken;
+	for (int i = 0; i < string_length(line); i++) {
+		DsrChar c = line[i];
+		DsrChar nextC = line[i + 1];
+		if (c == U'#' && nextC == U'#') {
+			// Appending tokens using ##
+			i++;
+		} else if (c == U'#' || c == U'(' || c == U')' || c == U'[' || c == U']' || c == U'{' || c == U'}') {
+			// Atomic token of a single character
+			flushToken(target, currentToken);
+			string_appendChar(currentToken, c);
+			flushToken(target, currentToken);
+		} else if (c == U' ' || c == U'\t') {
+			// Whitespace
+			flushToken(target, currentToken);
+		} else {
+			string_appendChar(currentToken, c);
+		}
+	}
+	flushToken(target, currentToken);
+}
+
+static void interpretPreprocessing(int64_t parentIndex, const List<String> &tokens, const ReadableString &parentFolder, int64_t lineNumber) {
+	if (tokens.length() >= 3) {
+		if (string_match(tokens[1], U"include")) {
+			if (tokens[2][0] == U'\"') {
+				String relativePath = string_unmangleQuote(tokens[2]);
+				String absolutePath = file_getTheoreticalAbsolutePath(relativePath, parentFolder, LOCAL_PATH_SYNTAX);
+				dependencies[parentIndex].includes.pushConstruct(absolutePath, lineNumber);
+				analyzeFromFile(absolutePath);
+			}
+		}
+	}
+}
+
+static void analyzeCode(int64_t parentIndex, String content, const ReadableString &parentFolder) {
+	List<String> tokens;
+	bool continuingLine = false;
+	int64_t lineNumber = 0;
+	string_split_callback(content, U'\n', true, [&parentIndex, &parentFolder, &tokens, &continuingLine, &lineNumber](ReadableString line) {
+		lineNumber++;
+		if (line[0] == U'#' || continuingLine) {
+			tokenize(tokens, line);
+			// Continuing pre-processing line using \ at the end.
+			continuingLine = line[string_length(line) - 1] == U'\\';
+		} else {
+			continuingLine = false;
+		}
+		if (!continuingLine && tokens.length() > 0) {
+			interpretPreprocessing(parentIndex, tokens, parentFolder, lineNumber);
+			tokens.clear();
+		}
+	});
+}
+
+void analyzeFromFile(const ReadableString& absolutePath) {
+	if (findDependency(absolutePath) != -1) {
+		// Already analyzed the current entry. Abort to prevent duplicate dependencies.
+		return;
+	}
+	int lastDotIndex = string_findLast(absolutePath, U'.');
+	if (lastDotIndex != -1) {
+		Extension extension = extensionFromString(string_after(absolutePath, lastDotIndex));
+		if (extension != Extension::Unknown) {
+			int64_t parentIndex = dependencies.length();
+			dependencies.pushConstruct(absolutePath, extension);
+			if (extension == Extension::H || extension == Extension::Hpp) {
+				// The current file is a header, so look for an implementation with the corresponding name.
+				String sourcePath = findSourceFile(absolutePath, extension == Extension::H, true);
+				// If found:
+				if (string_length(sourcePath) > 0) {
+					// Remember that anything using the header will have to link with the implementation.
+					dependencies[parentIndex].links.pushConstruct(sourcePath);
+					// Look for included headers in the implementation file.
+					analyzeFromFile(sourcePath);
+				}
+			}
+			// Get the file's binary content for checksums.
+			Buffer fileBuffer = file_loadBuffer(absolutePath);
+			// TODO: Get a checksum of fileBuffer and compare with the previous state. Files that changed should recompile all object files that depend on it.
+			// Interpret the file's content.
+			analyzeCode(parentIndex, string_loadFromMemory(fileBuffer), file_getRelativeParentFolder(absolutePath));
+		}
+	}
+}
+
+static void debugPrintDependencyList(const List<Connection> &connnections, const ReadableString verb) {
+	for (int c = 0; c < connnections.length(); c++) {
+		int64_t lineNumber = connnections[c].lineNumber;
+		if (lineNumber != -1) {
+			printText(U"  @", lineNumber, U"\t");
+		} else {
+			printText(U"    \t");
+		}
+		printText(U" ", verb, U" ", file_getPathlessName(connnections[c].path), U"\n");
+	}
+}
+
+void printDependencies() {
+	for (int d = 0; d < dependencies.length(); d++) {
+		printText(U"* ", file_getPathlessName(dependencies[d].path), U"\n");
+		debugPrintDependencyList(dependencies[d].includes, U"including");
+		debugPrintDependencyList(dependencies[d].links, U"linking");
+	}
+}
+
+static ScriptLanguage identifyLanguage(const ReadableString filename) {
+	String scriptExtension = string_upperCase(file_getExtension(filename));
+	if (string_match(scriptExtension, U"BAT")) {
+		return ScriptLanguage::Batch;
+	} else if (string_match(scriptExtension, U"SH")) {
+		return ScriptLanguage::Bash;
+	} else {
+		throwError(U"Could not identify the scripting language of ", filename, U". Use *.bat or *.sh.\n");
+		return ScriptLanguage::Unknown;
+	}
+}
+
+static void script_printMessage(String &output, ScriptLanguage language, const ReadableString message) {
+	if (language == ScriptLanguage::Batch) {
+		string_append(output, U"echo ", message, U"\n");
+	} else if (language == ScriptLanguage::Bash) {
+		string_append(output, U"echo ", message, U"\n");
+	}
+}
+
+static void script_executeLocalBinary(String &output, ScriptLanguage language, const ReadableString code) {
+	if (language == ScriptLanguage::Batch) {
+		string_append(output, code, ".exe\n");
+	} else if (language == ScriptLanguage::Bash) {
+		string_append(output, file_combinePaths(U".", code), U"\n");
+	}
+}
+
+void generateCompilationScript(const Machine &settings, const ReadableString& projectPath) {
+	ReadableString scriptPath = getFlag(settings, U"ScriptPath", U"");
+	if (string_length(scriptPath) == 0) {
+		printText(U"No script path was given, skipping script generation");
+		return;
+	}
+	ScriptLanguage language = identifyLanguage(scriptPath);
+	scriptPath = file_getTheoreticalAbsolutePath(scriptPath, projectPath);
+	// The compiler is often a global alias, so the user must supply either an alias or an absolute path.
+	ReadableString compilerName = getFlag(settings, U"Compiler", U"g++"); // Assume g++ as the compiler if not specified.
+
+	// Convert lists of linker and compiler flags into strings.
+	// TODO: Give a warning if two contradictory flags are used, such as optimization levels and language versions.
+	// TODO: Make sure that no spaces are inside of the flags, because that can mess up detection of pre-existing and contradictory arguments.
+	String compilerFlags;
+	for (int i = 0; i < settings.compilerFlags.length(); i++) {
+		string_append(compilerFlags, " ", settings.compilerFlags[i]);
+	}
+	String linkerFlags;
+	for (int i = 0; i < settings.linkerFlags.length(); i++) {
+		string_append(linkerFlags, " ", settings.linkerFlags[i]);
+	}
+
+	// Interpret ProgramPath relative to the project path.
+	ReadableString binaryPath = getFlag(settings, U"ProgramPath", language == ScriptLanguage::Batch ? U"program.exe" : U"program"); 
+	binaryPath = file_getTheoreticalAbsolutePath(binaryPath, projectPath);
+
+	String output;
+	if (language == ScriptLanguage::Batch) {
+		string_append(output, U"@echo off\n\n");
+	} else if (language == ScriptLanguage::Bash) {
+		string_append(output, U"#!/bin/bash\n\n");
+	} else {
+		printText(U"The type of script could not be identified for ", scriptPath, U"!\nUse *.bat for Batch or *.sh for Bash.\n");
+		return;
+	}
+	String compiledFiles;
+	bool needCppCompiler = false;
+	for (int d = 0; d < dependencies.length(); d++) {
+		Extension extension = dependencies[d].extension;
+		if (extension == Extension::Cpp) {
+			needCppCompiler = true;
+		}
+		if (extension == Extension::C || extension == Extension::Cpp) {
+			// Dependency paths are already absolute from the recursive search.
+			String sourcePath = dependencies[d].path;
+			string_append(compiledFiles, U" ", sourcePath);
+			if (file_getEntryType(sourcePath) != EntryType::File) {
+				throwError(U"The source file ", sourcePath, U" could not be found!\n");
+			}
+		}
+	}
+	// TODO: Give a warning if a known C compiler incapable of handling C++ is given C++ source code when needCppCompiler is true.
+	script_printMessage(output, language, string_combine(U"Compiling with", compilerFlags, linkerFlags));
+	string_append(output, compilerName, U" -o ", binaryPath, compilerFlags, linkerFlags, " ", compiledFiles, U"\n");
+	script_printMessage(output, language, U"Done compiling.");
+	script_printMessage(output, language, string_combine(U"Starting ", binaryPath));
+	script_executeLocalBinary(output, language, binaryPath);
+	script_printMessage(output, language, U"The program terminated.");
+	if (language == ScriptLanguage::Batch) {
+		// Windows might close the window before you have time to read the results or error messages of a CLI application, so pause at the end.
+		string_append(output, U"pause\n");
+	}
+	if (language == ScriptLanguage::Batch) {
+		string_save(scriptPath, output);
+	} else if (language == ScriptLanguage::Bash) {
+		string_save(scriptPath, output, CharacterEncoding::BOM_UTF8, LineEncoding::Lf);
+	}
+}

+ 26 - 0
Source/tools/builder/generator.h

@@ -0,0 +1,26 @@
+
+#ifndef DSR_BUILDER_GENERATOR_MODULE
+#define DSR_BUILDER_GENERATOR_MODULE
+
+#include "../../DFPSR/api/fileAPI.h"
+#include "Machine.h"
+
+using namespace dsr;
+
+// Analyze using calls from the machine
+void analyzeFromFile(const ReadableString& entryPath);
+// Call from main when done analyzing source files
+void resolveDependencies();
+
+// Visualize
+void printDependencies();
+
+// Generate
+enum class ScriptLanguage {
+	Unknown,
+	Batch,
+	Bash
+};
+void generateCompilationScript(const Machine &settings, const ReadableString& projectPath);
+
+#endif

+ 55 - 0
Source/tools/builder/main.cpp

@@ -0,0 +1,55 @@
+
+// TODO:
+//  * Let the build script define a temporary folder path for lazy compilation with reused *.o files.
+//    The /tmp folder is erased when shutting down the computer, which would force recompilation of each library after each time the computer has rebooted.
+//  * Find a way to check if a file is truly unique using a combination of pathless filenames, content checksums and visibility of surrounding files from the folder.
+//    This makes sure that including the same file twice using alternative ways of writing the path can be detected and trigger a warning.
+//    Can one convert the file ID from each platform into a string or 64-bit hash sum to quickly make sure that the file is unique?
+//  * Create scripts compiling the build system when it does not exist and managing creation of a temporary folder, calling the generated script...
+//  * Implement more features for the machine, such as:
+//    * Unary negation.
+//    * else and elseif cases.
+//    * Temporarily letting the theoretical path go into another folder within a scope, similar to if statements but only affecting the path.
+//      Like writing (cd path; stmt;) in Bash but with fast parsed Basic-like syntax.
+
+#include "../../DFPSR/api/fileAPI.h"
+#include "generator.h"
+
+using namespace dsr;
+
+// List dependencies for main.cpp on Linux: ./builder main.cpp --depend
+DSR_MAIN_CALLER(dsrMain)
+void dsrMain(List<String> args) {
+	if (args.length() <= 2) {
+		printText(U"To use the DFPSR build system, pass a path to the project file and the flags you want assigned before running the build script.\n");
+	} else {
+		// Get the project file path from the first argument after the program name.
+		String projectFilePath = args[1];
+		String platform = args[2];
+		Machine settings;
+		// Begin reading input arguments after the project's path, as named integers assigned to ones.
+		// Calling builder with the extra arguments "Graphics" and "Linux" will then create and assign both variables to 1.
+		// Other values can be assigned using an equality sign.
+		//   Avoid spaces around the equality sign, because quotes are already used for string arguments in assignments.
+		for (int a = 2; a < args.length(); a++) {
+			String argument = args[a];
+			int64_t assignmentIndex = string_findFirst(argument, U'=');
+			if (assignmentIndex == -1) {
+				assignValue(settings, argument, U"1");
+			} else {
+				String key = string_removeOuterWhiteSpace(string_before(argument, assignmentIndex));
+				String value = string_removeOuterWhiteSpace(string_after(argument, assignmentIndex));
+				assignValue(settings, key, value);
+			}
+		}
+		// Evaluate compiler settings while searching for source code mentioned in the project and imported headers.
+		printText(U"Executing project file from ", projectFilePath, U".\n");
+		evaluateScript(settings, projectFilePath);
+		// Once we are done finding all source files, we can resolve the dependencies to create a graph connected by indices.
+		resolveDependencies();
+		if (getFlagAsInteger(settings, U"ListDependencies")) {
+			printDependencies();
+		}
+		generateCompilationScript(settings, file_getAbsoluteParentFolder(projectFilePath));
+	}
+}

+ 17 - 0
Source/tools/wizard/Wizard.DsrProj

@@ -0,0 +1,17 @@
+Import "../../DFPSR/DFPSR.DsrHead"
+
+# The library uses C++14 by default, but you can override it with a newer version as well.
+CompilerFlag "-std=c++14"
+
+if Linux
+	CompilerPath = "g++"
+end
+if Windows
+	# Replace CompilerPath if you did not install the 64-bit MinGW distribution of CodeBlocks in C:\Program\CodeBlocks
+	CompilerPath = "C:/Program/CodeBlocks/MinGW/bin/x86_64-w64-mingw32-g++.exe"
+end
+
+Compile "main.cpp"
+ProgramPath = "wizard"
+
+ListDependencies = 0

+ 23 - 0
Source/tools/wizard/build.sh

@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# Assuming that you called build.sh from its own folder, you should already be in the project folder.
+PROJECT_FOLDER=.
+# Placing your executable in the project folder allow using the same relative paths in the final release.
+TARGET_FILE=./application
+# The root folder is where DFPSR, SDK and tools are located.
+ROOT_PATH=../..
+# Select where to place temporary files.
+TEMP_DIR=${ROOT_PATH}/../../temporary
+# Select a window manager
+WINDOW_MANAGER=X11
+# Select safe debug mode or fast release mode
+#MODE=-DDEBUG #Debug mode
+MODE=-DNDEBUG #Release mode
+COMPILER_FLAGS="${MODE} -std=c++14 -O2"
+# Select external libraries
+LINKER_FLAGS=""
+
+# Give execution permission
+chmod +x ${ROOT_PATH}/tools/buildAndRun.sh;
+# Compile everything
+${ROOT_PATH}/tools/buildAndRun.sh "${PROJECT_FOLDER}" "${TARGET_FILE}" "${ROOT_PATH}" "${TEMP_DIR}" "${WINDOW_MANAGER}" "${COMPILER_FLAGS}" "${LINKER_FLAGS}";

+ 64 - 0
Source/tools/wizard/main.cpp

@@ -0,0 +1,64 @@
+
+// TODO:
+// * Make this project into an application that starts automatically to test GUI and sound after building the Builder build system.
+// * Let the user browse a file system and select a location for a new or existing project.
+// * Explain how everything works when starting for the first time, using a command line argument.
+// * A catalogue of SDK examples with images and descriptions loaded automatically from their folder.
+//     * Offer one-click build and execution of SDK examples on multiple platforms, while explaining how the building works.
+
+#include "../../DFPSR/includeFramework.h"
+
+using namespace dsr;
+
+// Embedding your interface's layout is the simplest way to get started
+// It works even if the application is called from another folder
+String interfaceContent =
+UR"QUOTE(
+Begin : Panel
+	Name = "mainPanel"
+	Color = 150,160,170
+	Solid = 1
+End
+)QUOTE";
+
+// Global
+bool running = true;
+
+// GUI handles
+Window window;
+
+DSR_MAIN_CALLER(dsrMain)
+void dsrMain(List<String> args) {
+	// Create a window
+	window = window_create(U"Project wizard", 1000, 700);
+
+	// Register your custom components here
+	//REGISTER_PERSISTENT_CLASS(className);
+
+	// Load an interface to the window
+	window_loadInterfaceFromString(window, interfaceContent);
+
+	// Bind methods to events
+	window_setCloseEvent(window, []() {
+		running = false;
+	});
+
+	// Get your component handles here
+	//myComponent = window_findComponentByName(window, U"myComponent");
+
+	// Bind your components to events here
+	//component_setPressedEvent(myButton, []() {});
+
+	// Execute
+	while(running) {
+		// Wait for actions so that we don't render until an action has been recieved
+		// This will save battery on laptops for applications that don't require animation
+		while (!window_executeEvents(window)) {
+			time_sleepSeconds(0.01);
+		}
+		// Draw interface
+		window_drawComponents(window);
+		// Show the final image
+		window_showCanvas(window);
+	}
+}