main.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. 
  2. // A program for cloning a project from one folder to another, while updating relative paths to headers outside of the folder.
  3. // TODO:
  4. // * Create a visual interface for creating new projects from templates in the Wizard application.
  5. // Choose to create a new project, choose a template, choose a new name and location.
  6. // * Replace file paths in the Batch and Shell scripts.
  7. // * Allow renaming one of the project file, so that references to it will also be updated.
  8. // * Filter out files using patterns, to avoid cloning executable files and descriptions of template projects.
  9. #include "../../../DFPSR/includeEssentials.h"
  10. using namespace dsr;
  11. // Post-condition: Returns a list of entry names in the path, by simply segmentmenting by folder separators.
  12. static List<String> segmentPath(const ReadableString &path) {
  13. List<String> result;
  14. intptr_t startIndex = 0;
  15. for (intptr_t endIndex = 0; endIndex < string_length(path); endIndex++) {
  16. if (file_isSeparator(path[endIndex])) {
  17. if (startIndex < endIndex) {
  18. result.push(string_exclusiveRange(path, startIndex, endIndex));
  19. }
  20. startIndex = endIndex + 1;
  21. }
  22. }
  23. if (string_length(path) > startIndex) {
  24. result.push(string_exclusiveRange(path, startIndex, string_length(path)));
  25. }
  26. return result;
  27. }
  28. // TODO: Make rewrite it to work in more cases by converting oldOrigin to an absolute path before converting it to the new origin.
  29. // Pre-conditions:
  30. // path is either absolute or relative to oldOrigin.
  31. // newOrigin may not be absolute.
  32. // Post-condition:
  33. // Returns a path that refers to the same location but relative to newOrigin.
  34. static String changePathOrigin(const ReadableString &path, const ReadableString &oldOrigin, const ReadableString &newOrigin, PathSyntax pathSyntax) {
  35. // Check if the path is absolute.
  36. if (file_hasRoot(path, true)) {
  37. // The path is absolute, so we will not change it into an absolute path, just clean up any redundancy.
  38. return file_optimizePath(path, pathSyntax);
  39. }
  40. if (file_hasRoot(oldOrigin, true) || file_hasRoot(newOrigin, true)) {
  41. throwError(U"Origins to changePathOrigin may not be absolute!\n");
  42. }
  43. String absoluteOldOrigin = file_getAbsolutePath(oldOrigin);
  44. String absoluteNewOrigin = file_getAbsolutePath(newOrigin);
  45. String pathFromCurrent = file_optimizePath(file_combinePaths(absoluteOldOrigin, path, pathSyntax), pathSyntax);
  46. List<String> pathNames = segmentPath(pathFromCurrent);
  47. List<String> newOriginNames = segmentPath(file_optimizePath(absoluteNewOrigin, pathSyntax));
  48. intptr_t reverseOriginDepth = 0;
  49. List<String> forwardOrigin;
  50. bool identicalRoot = true;
  51. for (intptr_t i = 0; i < pathNames.length() || i < newOriginNames.length(); i++) {
  52. if (i < pathNames.length() && i < newOriginNames.length()) {
  53. if (!string_match(pathNames[i], newOriginNames[i])) {
  54. identicalRoot = false;
  55. }
  56. }
  57. if (!identicalRoot) {
  58. if (i < pathNames.length()) {
  59. forwardOrigin.push(pathNames[i]);
  60. }
  61. if (i < newOriginNames.length()) {
  62. reverseOriginDepth++;
  63. }
  64. }
  65. }
  66. List<String> results;
  67. for (intptr_t i = 0; i < reverseOriginDepth; i++) {
  68. results.push(U"..");
  69. }
  70. for (intptr_t i = 0; i < forwardOrigin.length(); i++) {
  71. results.push(forwardOrigin[i]);
  72. }
  73. String result;
  74. for (intptr_t i = 0; i < results.length(); i++) {
  75. if (string_length(result) > 0) {
  76. string_append(result, file_separator(pathSyntax));
  77. }
  78. string_append(result, results[i]);
  79. }
  80. result = file_optimizePath(result, pathSyntax);
  81. return result;
  82. }
  83. static void testRelocation(const ReadableString &path, const ReadableString &oldOrigin, const ReadableString &newOrigin, PathSyntax pathSyntax, const ReadableString &expectedResult) {
  84. String result = changePathOrigin(path, oldOrigin, newOrigin, pathSyntax);
  85. if (!string_match(result, expectedResult)) {
  86. throwError(U"Converting ", path, U" from ", oldOrigin, U" to ", newOrigin, U" expected ", expectedResult, U" as the result but got ", result, U" instead!\n");
  87. }
  88. }
  89. static void regressionTest() {
  90. printText(U"Running regression tests for the cloning project.\n");
  91. testRelocation(U"../someFile.txt", U"folderA/folderC", U"folderB", PathSyntax::Posix, U"../folderA/someFile.txt");
  92. testRelocation(U"someFile.txt", U"folderA", U"folderB", PathSyntax::Windows, U"..\\folderA\\someFile.txt");
  93. testRelocation(U"../../DFPSR/includeFramework.h", U"../../../templates/basic3D", U"./NewProject", PathSyntax::Posix, U"../../../../DFPSR/includeFramework.h");
  94. testRelocation(U"../../DFPSR/includeFramework.h", U"../../../templates/basic3D", U"../NewProject", PathSyntax::Posix, U"../../../DFPSR/includeFramework.h");
  95. testRelocation(U"../../DFPSR/includeFramework.h", U"../../../templates/basic3D", U"../../NewProject", PathSyntax::Posix, U"../../DFPSR/includeFramework.h");
  96. testRelocation(U"../../DFPSR/includeFramework.h", U"../../../templates/basic3D", U"../../../NewProject", PathSyntax::Posix, U"../DFPSR/includeFramework.h");
  97. testRelocation(U"../../DFPSR/includeFramework.h", U"../../../templates/basic3D", U"../../../../NewProject", PathSyntax::Posix, U"../Source/DFPSR/includeFramework.h");
  98. printText(U"Passed all regression tests for the cloning project.\n");
  99. }
  100. // Update paths after #include and #import in c, cpp, h, hpp, m and mm files.
  101. static String updateSourcePaths(const ReadableString &content, const ReadableString &oldParentFolder, const ReadableString &newParentFolder) {
  102. String result;
  103. intptr_t consumed = 0;
  104. int state = 0;
  105. for (intptr_t characterIndex = 0; characterIndex < string_length(content); characterIndex++) {
  106. DsrChar currentCharacter = content[characterIndex];
  107. if (currentCharacter == U'\n') {
  108. state = 0;
  109. } else if (state == 0 && currentCharacter == U'#') {
  110. state = 1;
  111. } else if (state == 1) {
  112. if (string_match(U"include", string_exclusiveRange(content, characterIndex, characterIndex + 7))) {
  113. characterIndex += 6;
  114. state = 2;
  115. } else if (string_match(U"import", string_exclusiveRange(content, characterIndex, characterIndex + 6))) {
  116. characterIndex += 5;
  117. state = 2;
  118. }
  119. } else if (state == 2 && currentCharacter == U'\"') {
  120. // Begin a quoted path.
  121. state = 3;
  122. // Previous text is appended as is.
  123. string_append(result, string_inclusiveRange(content, consumed, characterIndex));
  124. consumed = characterIndex + 1;
  125. } else if (state == 3 && currentCharacter == U'\"') {
  126. // End a quoted path.
  127. state = -1;
  128. String oldPath = string_inclusiveRange(content, consumed, characterIndex - 1);
  129. String newPath = changePathOrigin(oldPath, oldParentFolder, newParentFolder, PathSyntax::Posix);
  130. string_append(result, newPath);
  131. consumed = characterIndex;
  132. if (string_match(newPath, oldPath)) {
  133. printText(U" Nothing needed to change in ", oldPath, U"\n");
  134. } else {
  135. printText(U" Modified path from ", oldPath, U" to ", newPath, U"\n");
  136. }
  137. } else if (state != 3 && !character_isWhiteSpace(currentCharacter)) {
  138. // Abort patterns when getting unexpected characters.
  139. state = -1;
  140. }
  141. }
  142. // Remaining text is appended as is.
  143. string_append(result, string_exclusiveRange(content, consumed, string_length(content)));
  144. return result;
  145. }
  146. // Update paths after Import in DsrProj and DsrHead files.
  147. static String updateProjectPaths(const ReadableString &content, const ReadableString &oldParentFolder, const ReadableString &newParentFolder) {
  148. String result;
  149. intptr_t consumed = 0;
  150. int state = 0;
  151. for (intptr_t characterIndex = 0; characterIndex < string_length(content); characterIndex++) {
  152. DsrChar currentCharacter = content[characterIndex];
  153. if (currentCharacter == U'\n') {
  154. state = 0;
  155. } else if (state == 0) {
  156. if (string_caseInsensitiveMatch(U"Import", string_exclusiveRange(content, characterIndex, characterIndex + 6))) {
  157. characterIndex += 5;
  158. state = 1;
  159. }
  160. } else if (state == 1 && currentCharacter == U'\"') {
  161. // Begin a quoted path.
  162. state = 2;
  163. // Previous text is appended as is.
  164. string_append(result, string_inclusiveRange(content, consumed, characterIndex));
  165. consumed = characterIndex + 1;
  166. } else if (state == 2 && currentCharacter == U'\"') {
  167. // End a quoted path.
  168. state = -1;
  169. String oldPath = string_inclusiveRange(content, consumed, characterIndex - 1);
  170. String newPath = changePathOrigin(oldPath, oldParentFolder, newParentFolder, PathSyntax::Posix);
  171. string_append(result, newPath);
  172. consumed = characterIndex;
  173. if (string_match(newPath, oldPath)) {
  174. printText(U" Nothing needed to change in ", oldPath, U"\n");
  175. } else {
  176. printText(U" Modified path from ", oldPath, U" to ", newPath, U"\n");
  177. }
  178. } else if (state != 2 && !character_isWhiteSpace(currentCharacter)) {
  179. // Abort patterns when getting unexpected characters.
  180. state = -1;
  181. }
  182. }
  183. // Remaining text is appended as is.
  184. string_append(result, string_exclusiveRange(content, consumed, string_length(content)));
  185. return result;
  186. }
  187. static void copyFile(const ReadableString &sourcePath, const ReadableString &targetPath) {
  188. EntryType sourceEntryType = file_getEntryType(sourcePath);
  189. EntryType targetEntryType = file_getEntryType(targetPath);
  190. if (sourceEntryType != EntryType::File) {
  191. throwError(U"The source file ", sourcePath, U" does not exist!\n");
  192. }
  193. if (targetEntryType != EntryType::NotFound) {
  194. throwError(U"The target file ", targetPath, U" already exists!\n");
  195. } else {
  196. Buffer fileContent = file_loadBuffer(sourcePath);
  197. if (!buffer_exists(fileContent)) {
  198. throwError(U"The source file ", sourcePath, U" could not be loaded!\n");
  199. }
  200. ReadableString extension = file_getExtension(sourcePath);
  201. if (string_caseInsensitiveMatch(extension, U"DsrProj")
  202. || string_caseInsensitiveMatch(extension, U"DsrHead")) {
  203. //patterns.pushConstruct(U"Import \"", U"", U"\"");
  204. fileContent = string_saveToMemory(updateProjectPaths(string_loadFromMemory(fileContent), file_getRelativeParentFolder(sourcePath), file_getRelativeParentFolder(targetPath)), CharacterEncoding::Raw_Latin1);
  205. } else if (string_caseInsensitiveMatch(extension, U"bat")) {
  206. //TODO: Look for paths containing U"\builder\buildProject.bat", segment the whole path, and update path origin.
  207. //fileContent = string_saveToMemory(updateBatchPaths(string_loadFromMemory(fileContent), file_getRelativeParentFolder(sourcePath), file_getRelativeParentFolder(targetPath)), CharacterEncoding::Raw_Latin1);
  208. } else if (string_caseInsensitiveMatch(extension, U"sh")) {
  209. //TODO: Look for paths containing U"/builder/buildProject.sh", segment the whole path, and update path origin.
  210. //fileContent = string_saveToMemory(updateShellPaths(string_loadFromMemory(fileContent), file_getRelativeParentFolder(sourcePath), file_getRelativeParentFolder(targetPath)), CharacterEncoding::Raw_Latin1);
  211. } else if (string_caseInsensitiveMatch(extension, U"c")
  212. || string_caseInsensitiveMatch(extension, U"cpp")
  213. || string_caseInsensitiveMatch(extension, U"h")
  214. || string_caseInsensitiveMatch(extension, U"hpp")
  215. || string_caseInsensitiveMatch(extension, U"m")
  216. || string_caseInsensitiveMatch(extension, U"mm")) {
  217. fileContent = string_saveToMemory(updateSourcePaths(string_loadFromMemory(fileContent), file_getRelativeParentFolder(sourcePath), file_getRelativeParentFolder(targetPath)), CharacterEncoding::BOM_UTF8);
  218. }
  219. if (!file_saveBuffer(targetPath, fileContent)) {
  220. throwError(U"The target file ", targetPath, U" could not be saved!\n");
  221. }
  222. }
  223. }
  224. struct FileConversion {
  225. String sourceFilePath;
  226. String targetFilePath;
  227. FileConversion(const ReadableString &sourceFilePath, const ReadableString &targetFilePath)
  228. : sourceFilePath(sourceFilePath), targetFilePath(targetFilePath) {}
  229. };
  230. struct FileOperations {
  231. List<String> newFolderPaths;
  232. List<FileConversion> clonedFiles;
  233. };
  234. static bool createFolder_deferred(FileOperations &operations, const ReadableString &folderPath) {
  235. EntryType targetEntryType = file_getEntryType(folderPath);
  236. if (targetEntryType == EntryType::Folder) {
  237. return true;
  238. } else if (targetEntryType == EntryType::File) {
  239. printText(U"The folder to create ", folderPath, U" is an pre-existing file and can not be overwritten with a folder!\n");
  240. return false;
  241. } else if (targetEntryType == EntryType::NotFound) {
  242. String parentFolder = file_getRelativeParentFolder(folderPath);
  243. if (string_length(parentFolder) < string_length(folderPath) && createFolder_deferred(operations, parentFolder)) {
  244. operations.newFolderPaths.push(folderPath);
  245. return true;
  246. } else {
  247. printText(U"Failed to create a parent folder at ", parentFolder, U"!\n");
  248. return false;
  249. }
  250. } else {
  251. printText(U"The folder to create ", folderPath, U" can not be overwritten!\n");
  252. return false;
  253. }
  254. }
  255. static void copyFolder_deferred(FileOperations &operations, const ReadableString &sourcePath, const ReadableString &targetPath) {
  256. if (!createFolder_deferred(operations, targetPath)) {
  257. throwError(U"Failed to create a folder at ", targetPath, U"!\n");
  258. } else {
  259. if (!file_getFolderContent(sourcePath, [&operations, targetPath](const ReadableString& entryPath, const ReadableString& entryName, EntryType entryType) {
  260. if (entryType == EntryType::File) {
  261. operations.clonedFiles.pushConstruct(entryPath, file_combinePaths(targetPath, entryName));
  262. } else if (entryType == EntryType::Folder) {
  263. copyFolder_deferred(operations, entryPath, file_combinePaths(targetPath, entryName));
  264. }
  265. })) {
  266. printText("Failed to explore ", sourcePath, "\n");
  267. }
  268. }
  269. }
  270. enum class ExpectedArgument {
  271. Flag, Source, Target
  272. };
  273. DSR_MAIN_CALLER(dsrMain)
  274. void dsrMain(List<String> args) {
  275. if (args.length() <= 1) {
  276. regressionTest();
  277. return;
  278. }
  279. String source;
  280. String target;
  281. ExpectedArgument expectedArgument = ExpectedArgument::Flag;
  282. for (int i = 1; i < args.length(); i++) {
  283. ReadableString argument = args[i];
  284. if (expectedArgument == ExpectedArgument::Flag) {
  285. if (string_caseInsensitiveMatch(argument, U"-s") || string_caseInsensitiveMatch(argument, U"-source")) {
  286. expectedArgument = ExpectedArgument::Source;
  287. } else if (string_caseInsensitiveMatch(argument, U"-t") || string_caseInsensitiveMatch(argument, U"-target")) {
  288. expectedArgument = ExpectedArgument::Target;
  289. } else {
  290. sendWarning(U"Unrecognized flag ", argument, U" given to project cloning!\n");
  291. }
  292. } else if (expectedArgument == ExpectedArgument::Source) {
  293. EntryType sourceEntryType = file_getEntryType(argument);
  294. if (sourceEntryType == EntryType::Folder) {
  295. printText(U"Using ", argument, U" as the source folder path.\n");
  296. source = argument;
  297. } else if (sourceEntryType == EntryType::File) {
  298. throwError(U"The source ", argument, U" is a file and can not be used as a source folder for project cloning!\n");
  299. } else if (sourceEntryType == EntryType::NotFound) {
  300. throwError(U"The source ", argument, U" can not be found! The source path must refer to an existing folder to clone from.\n");
  301. }
  302. expectedArgument = ExpectedArgument::Flag;
  303. } else if (expectedArgument == ExpectedArgument::Target) {
  304. EntryType targetEntryType = file_getEntryType(argument);
  305. if (targetEntryType == EntryType::Folder) {
  306. printText(U"Using ", argument, U" as the target folder path.\n");
  307. target = argument;
  308. } else if (targetEntryType == EntryType::File) {
  309. throwError(U"The target ", argument, U" is a file and can not be used as a target folder for project cloning!\n");
  310. } else if (targetEntryType == EntryType::NotFound) {
  311. printText(U"Using ", argument, U" as the target folder path.\n");
  312. target = argument;
  313. }
  314. expectedArgument = ExpectedArgument::Flag;
  315. }
  316. }
  317. if (string_length(source) == 0 && string_length(target) == 0) {
  318. throwError(U"Cloning project needs both source and target folder paths!\n");
  319. } else if (string_length(source) == 0) {
  320. throwError(U"Missing source folder to clone from!\n");
  321. } else if (string_length(target) == 0) {
  322. throwError(U"Missing target folder to clone to!\n");
  323. }
  324. printText(U"Cloning project from ", source, U" to ", target, U"\n");
  325. // List operations to perform ahead of time to prevent bottomless recursion when cloning into a subfolder of the source folder.
  326. FileOperations operations;
  327. copyFolder_deferred(operations, source, target);
  328. for (intptr_t folderIndex = 0; folderIndex < operations.newFolderPaths.length(); folderIndex++) {
  329. ReadableString newFolderPath = operations.newFolderPaths[folderIndex];
  330. printText(U"Creating a new folder at ", newFolderPath, U"\n");
  331. file_createFolder(newFolderPath);
  332. }
  333. for (intptr_t fileIndex = 0; fileIndex < operations.clonedFiles.length(); fileIndex++) {
  334. FileConversion conversion = operations.clonedFiles[fileIndex];
  335. printText(U"Cloning file from ", conversion.sourceFilePath, U" to ", conversion.targetFilePath, U"\n");
  336. copyFile(conversion.sourceFilePath, conversion.targetFilePath);
  337. }
  338. }