main.cpp 19 KB

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